From 1be4dff69d07c8ff9c9d54e15a62d2f71c3f1ecb Mon Sep 17 00:00:00 2001 From: sawankshrma Date: Wed, 25 Feb 2026 02:26:14 +0530 Subject: [PATCH 1/5] add evidence data model in the new EvidenceStore --- src/app/model/data-store.ts | 6 + src/app/model/evidence-store.ts | 199 +++++++++++++++++++++++++++++ src/assets/YAML/team-evidence.yaml | 2 + 3 files changed, 207 insertions(+) create mode 100644 src/app/model/evidence-store.ts create mode 100644 src/assets/YAML/team-evidence.yaml diff --git a/src/app/model/data-store.ts b/src/app/model/data-store.ts index 78834609..9d71c842 100644 --- a/src/app/model/data-store.ts +++ b/src/app/model/data-store.ts @@ -2,16 +2,19 @@ import { ActivityStore } from './activity-store'; import { Progress } from './types'; import { MetaStore, MetaStrings } from './meta-store'; import { ProgressStore } from './progress-store'; +import { EvidenceData, EvidenceStore } from './evidence-store'; export class DataStore { public meta: MetaStore | null = null; public activityStore: ActivityStore | null = null; public progressStore: ProgressStore | null = null; + public evidenceStore: EvidenceStore | null = null; constructor() { this.meta = new MetaStore(); this.activityStore = new ActivityStore(); this.progressStore = new ProgressStore(); + this.evidenceStore = new EvidenceStore(); } public addActivities(activities: ActivityStore): void { @@ -20,6 +23,9 @@ export class DataStore { public addProgressData(progress: Progress): void { this.progressStore?.addProgressData(progress); } + public addEvidenceData(evidence: EvidenceData): void { + this.evidenceStore?.addEvidenceData(evidence); + } public getMetaStrings(): MetaStrings { if (this.meta == null) { diff --git a/src/app/model/evidence-store.ts b/src/app/model/evidence-store.ts new file mode 100644 index 00000000..8d25a131 --- /dev/null +++ b/src/app/model/evidence-store.ts @@ -0,0 +1,199 @@ +import { YamlService } from '../service/yaml-loader/yaml-loader.service'; +import { Uuid } from './types'; + +export interface EvidenceAttachment { + type: string; // e.g. 'document', 'image', 'link' + externalLink: string; // URL +} + +export interface EvidenceEntry { + id: string; // stable UUID for this entry + teams: string[]; + title: string; + evidenceRecorded: string; // ISO date string + reviewer?: string; + description: string; + attachment?: EvidenceAttachment[]; +} + +export type EvidenceData = Record; + +const LOCALSTORAGE_KEY: string = 'evidence'; + +export class EvidenceStore { + private yamlService: YamlService = new YamlService(); + private _evidence: EvidenceData = {}; + + // ─── Lifecycle ──────────────────────────────────────────── + + public initFromLocalStorage(): void { + const stored = this.retrieveStoredEvidence(); + if (stored) { + this.addEvidenceData(stored); + } + } + + // ─── Accessors ──────────────────────────────────────────── + + public getEvidenceData(): EvidenceData { + return this._evidence; + } + + public getEvidence(activityUuid: Uuid): EvidenceEntry[] { + return this._evidence[activityUuid] || []; + } + + public hasEvidence(activityUuid: Uuid): boolean { + return (this._evidence[activityUuid]?.length || 0) > 0; + } + + public getEvidenceCount(activityUuid: Uuid): number { + return this._evidence[activityUuid]?.length ?? 0; + } + + public getTotalEvidenceCount(): number { + let count = 0; + for (const uuid in this._evidence) { + count += this._evidence[uuid].length; + } + return count; + } + + public getActivityUuidsWithEvidence(): Uuid[] { + return Object.keys(this._evidence).filter(uuid => this._evidence[uuid].length > 0); + } + + // ─── Mutators ──────────────────────────────────────────── + + public addEvidenceData(newEvidence: EvidenceData): void { + if (!newEvidence) return; + + for (const activityUuid in newEvidence) { + if (!this._evidence[activityUuid]) { + this._evidence[activityUuid] = []; + } + + const newEntries = newEvidence[activityUuid]; + if (Array.isArray(newEntries)) { + for (const entry of newEntries) { + if (!this.isDuplicateEntry(activityUuid, entry)) { + this._evidence[activityUuid].push(entry); + } + } + } + } + } + + public replaceEvidenceData(data: EvidenceData): void { + this._evidence = data; + this.saveToLocalStorage(); + } + + public addEvidence(activityUuid: Uuid, entry: EvidenceEntry): void { + if (!this._evidence[activityUuid]) { + this._evidence[activityUuid] = []; + } + this._evidence[activityUuid].push(entry); + this.saveToLocalStorage(); + } + + public updateEvidence( + activityUuid: Uuid, + entryId: string, + updatedEntry: Partial + ): void { + const entries = this._evidence[activityUuid]; + if (!entries) { + console.warn(`No evidence found for activity ${activityUuid}`); + return; + } + const index = entries.findIndex(e => e.id === entryId); + if (index === -1) { + console.warn(`Cannot find evidence with id ${entryId} for activity ${activityUuid}`); + return; + } + // Immutable update for Angular change detection + entries[index] = { ...entries[index], ...updatedEntry }; + this.saveToLocalStorage(); + } + + public deleteEvidence(activityUuid: Uuid, entryId: string): void { + const entries = this._evidence[activityUuid]; + if (!entries) { + console.warn(`No evidence found for activity ${activityUuid}`); + return; + } + const index = entries.findIndex(e => e.id === entryId); + if (index === -1) { + console.warn(`Cannot find evidence with id ${entryId} for activity ${activityUuid}`); + return; + } + entries.splice(index, 1); + + if (entries.length === 0) { + delete this._evidence[activityUuid]; + } + this.saveToLocalStorage(); + } + + public renameTeam(oldName: string, newName: string): void { + console.log(`Renaming team '${oldName}' to '${newName}' in evidence store`); + for (const uuid in this._evidence) { + this._evidence[uuid].forEach(entry => { + entry.teams = entry.teams.map(t => (t === oldName ? newName : t)); + }); + } + this.saveToLocalStorage(); + } + + // ─── Serialization ────────────────────────────────────── + + public asYamlString(): string { + return this.yamlService.stringify({ evidence: this._evidence }); + } + + public saveToLocalStorage(): void { + const yamlStr = this.asYamlString(); + localStorage.setItem(LOCALSTORAGE_KEY, yamlStr); + } + + public deleteBrowserStoredEvidence(): void { + console.log('Deleting evidence from browser storage'); + localStorage.removeItem(LOCALSTORAGE_KEY); + } + + public retrieveStoredEvidenceYaml(): string | null { + return localStorage.getItem(LOCALSTORAGE_KEY); + } + + public retrieveStoredEvidence(): EvidenceData | null { + const yamlStr = this.retrieveStoredEvidenceYaml(); + if (!yamlStr) return null; + + const parsed = this.yamlService.parse(yamlStr); + return parsed?.evidence ?? null; + } + + // ─── Helpers ───────────────────────────────────────────── + + private isDuplicateEntry(activityUuid: Uuid, entry: EvidenceEntry): boolean { + const existing = this._evidence[activityUuid]; + if (!existing) return false; + + return existing.some( + e => + e.description === entry.description && + e.evidenceRecorded === entry.evidenceRecorded && + e.reviewer === entry.reviewer + ); + } + public static todayDateString(): string { + const now = new Date(); + return now.toISOString().substring(0, 10); + } + + // to be used when creating new evidence entries to ensure they have a stable UUID + public static generateId(): string { + return crypto.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + } +} diff --git a/src/assets/YAML/team-evidence.yaml b/src/assets/YAML/team-evidence.yaml new file mode 100644 index 00000000..c6a846fd --- /dev/null +++ b/src/assets/YAML/team-evidence.yaml @@ -0,0 +1,2 @@ + # Export team evidence from the browser, and replace this file +evidence: From 656d972157792b94c8999d18daa94dd77cfb56e9 Mon Sep 17 00:00:00 2001 From: sawankshrma Date: Thu, 26 Feb 2026 09:11:30 +0530 Subject: [PATCH 2/5] implement loading team evidence from YAML and localStorage --- src/app/model/evidence-store.ts | 9 ++------- src/app/model/meta-store.ts | 2 ++ src/app/service/loader/data-loader.service.ts | 17 +++++++++++++++++ src/assets/YAML/meta.yaml | 1 + 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/app/model/evidence-store.ts b/src/app/model/evidence-store.ts index 8d25a131..f5b88f44 100644 --- a/src/app/model/evidence-store.ts +++ b/src/app/model/evidence-store.ts @@ -179,14 +179,9 @@ export class EvidenceStore { private isDuplicateEntry(activityUuid: Uuid, entry: EvidenceEntry): boolean { const existing = this._evidence[activityUuid]; if (!existing) return false; - - return existing.some( - e => - e.description === entry.description && - e.evidenceRecorded === entry.evidenceRecorded && - e.reviewer === entry.reviewer - ); + return existing.some(e => e.id === entry.id); } + public static todayDateString(): string { const now = new Date(); return now.toISOString().substring(0, 10); diff --git a/src/app/model/meta-store.ts b/src/app/model/meta-store.ts index d36ab238..3a0a26b6 100644 --- a/src/app/model/meta-store.ts +++ b/src/app/model/meta-store.ts @@ -35,6 +35,7 @@ export class MetaStore { teams: TeamNames = []; activityFiles: string[] = []; teamProgressFile: string = ''; + teamEvidenceFile: string = ''; allowChangeTeamNameInBrowser: boolean = true; dimensionIcons: Record = { @@ -67,6 +68,7 @@ export class MetaStore { this.teams = metaData.teams || this.teams || []; this.activityFiles = metaData.activityFiles || this.activityFiles || []; this.teamProgressFile = metaData.teamProgressFile || this.teamProgressFile || ''; + this.teamEvidenceFile = metaData.teamEvidenceFile || this.teamEvidenceFile || ''; if (metaData.allowChangeTeamNameInBrowser !== undefined) this.allowChangeTeamNameInBrowser = metaData.allowChangeTeamNameInBrowser; } diff --git a/src/app/service/loader/data-loader.service.ts b/src/app/service/loader/data-loader.service.ts index 79c49ddc..9fe4acf2 100644 --- a/src/app/service/loader/data-loader.service.ts +++ b/src/app/service/loader/data-loader.service.ts @@ -84,6 +84,14 @@ export class LoaderService { this.dataStore.addProgressData(browserProgress?.progress); } + // Load evidence data + const evidenceData = await this.loadEvidence(this.dataStore.meta); + this.dataStore.addEvidenceData(evidenceData.evidence); + this.dataStore.evidenceStore?.initFromLocalStorage(); + + // DEBUG ONLY + console.log('Merged EvidenceStore:', this.dataStore.evidenceStore?.getEvidenceData()); + console.log(`${perfNow()}: YAML: All YAML files loaded`); return this.dataStore; @@ -134,6 +142,10 @@ export class LoaderService { meta.activityFiles = meta.activityFiles.map(file => this.yamlService.makeFullPath(file, this.META_FILE) ); + if (!meta.teamProgressFile) { + throw Error("The meta.yaml has no 'teamEvidenceFile' to be loaded"); + } + meta.teamEvidenceFile = this.yamlService.makeFullPath(meta.teamEvidenceFile, this.META_FILE); if (this.debug) console.log(`${perfNow()} s: meta loaded`); console.log(`${perfNow()} s: Loaded teams: ${meta.teams.join(', ')}`); @@ -145,6 +157,11 @@ export class LoaderService { return this.yamlService.loadYaml(meta.teamProgressFile); } + private async loadEvidence(meta: MetaStore): Promise<{ evidence: any }> { + if (this.debug) console.log(`${perfNow()}s: Loading Team Evidence: ${meta.teamEvidenceFile}`); + return this.yamlService.loadYaml(meta.teamEvidenceFile); + } + private async loadActivities(meta: MetaStore): Promise { const activityStore = new ActivityStore(); const errors: string[] = []; diff --git a/src/assets/YAML/meta.yaml b/src/assets/YAML/meta.yaml index f655a1d9..81c1c5fb 100644 --- a/src/assets/YAML/meta.yaml +++ b/src/assets/YAML/meta.yaml @@ -5,6 +5,7 @@ browserSettings: teamProgressFile: 'team-progress.yaml' +teamEvidenceFile: 'team-evidence.yaml' progressDefinition: Not implemented: score: 0% From 1169c45b1edfac772aa63fe8e9ec197d2fbfbac1 Mon Sep 17 00:00:00 2001 From: sawankshrma Date: Thu, 5 Mar 2026 14:59:40 +0530 Subject: [PATCH 3/5] Changed -addEvidenceData- from skip-on-duplicate to replace-on-duplicate semantics --- src/app/model/evidence-store.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/model/evidence-store.ts b/src/app/model/evidence-store.ts index f5b88f44..f6cba85a 100644 --- a/src/app/model/evidence-store.ts +++ b/src/app/model/evidence-store.ts @@ -76,7 +76,10 @@ export class EvidenceStore { const newEntries = newEvidence[activityUuid]; if (Array.isArray(newEntries)) { for (const entry of newEntries) { - if (!this.isDuplicateEntry(activityUuid, entry)) { + const existingIndex = this._evidence[activityUuid].findIndex(e => e.id === entry.id); + if (existingIndex !== -1) { + this._evidence[activityUuid][existingIndex] = entry; + } else { this._evidence[activityUuid].push(entry); } } From 82e7bc1c221a61cb88eb88f81ad52692acfc8316 Mon Sep 17 00:00:00 2001 From: sawankshrma Date: Wed, 11 Mar 2026 02:03:57 +0530 Subject: [PATCH 4/5] new uuid types --- src/app/model/evidence-store.ts | 24 ++++++++++++------------ src/app/model/types.ts | 2 ++ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/app/model/evidence-store.ts b/src/app/model/evidence-store.ts index f6cba85a..b24eaac0 100644 --- a/src/app/model/evidence-store.ts +++ b/src/app/model/evidence-store.ts @@ -1,5 +1,5 @@ import { YamlService } from '../service/yaml-loader/yaml-loader.service'; -import { Uuid } from './types'; +import { ActivityId, EvidenceId } from './types'; export interface EvidenceAttachment { type: string; // e.g. 'document', 'image', 'link' @@ -7,7 +7,7 @@ export interface EvidenceAttachment { } export interface EvidenceEntry { - id: string; // stable UUID for this entry + id: EvidenceId; // stable UUID for this entry teams: string[]; title: string; evidenceRecorded: string; // ISO date string @@ -16,7 +16,7 @@ export interface EvidenceEntry { attachment?: EvidenceAttachment[]; } -export type EvidenceData = Record; +export type EvidenceData = Record; const LOCALSTORAGE_KEY: string = 'evidence'; @@ -39,15 +39,15 @@ export class EvidenceStore { return this._evidence; } - public getEvidence(activityUuid: Uuid): EvidenceEntry[] { + public getEvidence(activityUuid: ActivityId): EvidenceEntry[] { return this._evidence[activityUuid] || []; } - public hasEvidence(activityUuid: Uuid): boolean { + public hasEvidence(activityUuid: ActivityId): boolean { return (this._evidence[activityUuid]?.length || 0) > 0; } - public getEvidenceCount(activityUuid: Uuid): number { + public getEvidenceCount(activityUuid: ActivityId): number { return this._evidence[activityUuid]?.length ?? 0; } @@ -59,7 +59,7 @@ export class EvidenceStore { return count; } - public getActivityUuidsWithEvidence(): Uuid[] { + public getActivityUuidsWithEvidence(): ActivityId[] { return Object.keys(this._evidence).filter(uuid => this._evidence[uuid].length > 0); } @@ -92,7 +92,7 @@ export class EvidenceStore { this.saveToLocalStorage(); } - public addEvidence(activityUuid: Uuid, entry: EvidenceEntry): void { + public addEvidence(activityUuid: ActivityId, entry: EvidenceEntry): void { if (!this._evidence[activityUuid]) { this._evidence[activityUuid] = []; } @@ -101,8 +101,8 @@ export class EvidenceStore { } public updateEvidence( - activityUuid: Uuid, - entryId: string, + activityUuid: ActivityId, + entryId: EvidenceId, updatedEntry: Partial ): void { const entries = this._evidence[activityUuid]; @@ -120,7 +120,7 @@ export class EvidenceStore { this.saveToLocalStorage(); } - public deleteEvidence(activityUuid: Uuid, entryId: string): void { + public deleteEvidence(activityUuid: ActivityId, entryId: EvidenceId): void { const entries = this._evidence[activityUuid]; if (!entries) { console.warn(`No evidence found for activity ${activityUuid}`); @@ -179,7 +179,7 @@ export class EvidenceStore { // ─── Helpers ───────────────────────────────────────────── - private isDuplicateEntry(activityUuid: Uuid, entry: EvidenceEntry): boolean { + private isDuplicateEntry(activityUuid: ActivityId, entry: EvidenceEntry): boolean { const existing = this._evidence[activityUuid]; if (!existing) return false; return existing.some(e => e.id === entry.id); diff --git a/src/app/model/types.ts b/src/app/model/types.ts index 75f1f1a5..0cbbf54d 100644 --- a/src/app/model/types.ts +++ b/src/app/model/types.ts @@ -15,6 +15,8 @@ export type Progress = Record; export type ActivityProgress = Record; export type TeamProgress = Record; export type Uuid = string; +export type ActivityId = Uuid; +export type EvidenceId = Uuid; export type TeamName = string; export type GroupName = string; export type ProgressTitle = string; From 402c5ab0311a2d267368adbfa8d97ce3c0f690f7 Mon Sep 17 00:00:00 2001 From: sawankshrma Date: Wed, 11 Mar 2026 02:16:14 +0530 Subject: [PATCH 5/5] fix: check teamEvidenceFile instead of teamProgressFile Co-authored-by: vbakke --- src/app/service/loader/data-loader.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/loader/data-loader.service.ts b/src/app/service/loader/data-loader.service.ts index 9fe4acf2..e2c02da5 100644 --- a/src/app/service/loader/data-loader.service.ts +++ b/src/app/service/loader/data-loader.service.ts @@ -142,7 +142,7 @@ export class LoaderService { meta.activityFiles = meta.activityFiles.map(file => this.yamlService.makeFullPath(file, this.META_FILE) ); - if (!meta.teamProgressFile) { + if (!meta.teamEvidenceFile) { throw Error("The meta.yaml has no 'teamEvidenceFile' to be loaded"); } meta.teamEvidenceFile = this.yamlService.makeFullPath(meta.teamEvidenceFile, this.META_FILE);