diff --git a/src/app/component/add-evidence-modal/add-evidence-modal.component.html b/src/app/component/add-evidence-modal/add-evidence-modal.component.html
index 30b55ff07..9d49f134d 100644
--- a/src/app/component/add-evidence-modal/add-evidence-modal.component.html
+++ b/src/app/component/add-evidence-modal/add-evidence-modal.component.html
@@ -2,9 +2,9 @@
Add Evidence
diff --git a/src/app/component/add-evidence-modal/add-evidence-modal.component.ts b/src/app/component/add-evidence-modal/add-evidence-modal.component.ts
index 580aa1ff0..677eadfa9 100644
--- a/src/app/component/add-evidence-modal/add-evidence-modal.component.ts
+++ b/src/app/component/add-evidence-modal/add-evidence-modal.component.ts
@@ -1,7 +1,7 @@
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
import { EvidenceEntry, EvidenceStore } from '../../model/evidence-store';
-import { TeamGroups } from '../../model/types';
+import { TeamSelectionService } from '../../service/team-selection.service';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatButtonModule } from '@angular/material/button';
@@ -16,8 +16,6 @@ import { TeamSelectorComponent } from '../team-selector/team-selector.component'
export interface AddEvidenceModalData {
activityUuid: string;
- allTeams: string[];
- teamGroups: TeamGroups;
}
@Component({
@@ -42,10 +40,9 @@ export interface AddEvidenceModalData {
export class AddEvidenceModalComponent {
dialogRef = inject>(MatDialogRef);
data = inject(MAT_DIALOG_DATA);
+ readonly teamSelection = inject(TeamSelectionService);
activityUuid: string;
- allTeams: string[];
- teamGroups: TeamGroups;
// Form fields
selectedTeams: string[] = [];
@@ -66,8 +63,7 @@ export class AddEvidenceModalComponent {
const data = this.data;
this.activityUuid = data.activityUuid;
- this.allTeams = data.allTeams;
- this.teamGroups = data.teamGroups || {};
+ this.selectedTeams = [...this.teamSelection.effectiveTeams()];
}
onSelectedTeamsChange(teams: string[]): void {
diff --git a/src/app/component/evidence-panel/evidence-panel.component.ts b/src/app/component/evidence-panel/evidence-panel.component.ts
index 52882e623..533b3ec19 100644
--- a/src/app/component/evidence-panel/evidence-panel.component.ts
+++ b/src/app/component/evidence-panel/evidence-panel.component.ts
@@ -8,6 +8,7 @@ import {
} from '@angular/core';
import { EvidenceEntry } from '../../model/evidence-store';
import { LoaderService } from '../../service/loader/data-loader.service';
+import { TeamSelectionService } from '../../service/team-selection.service';
import { MatIconModule } from '@angular/material/icon';
import { MatExpansionModule } from '@angular/material/expansion';
import { DatePipe } from '@angular/common';
@@ -21,6 +22,7 @@ import { DatePipe } from '@angular/common';
})
export class EvidencePanelComponent implements OnChanges {
private loader = inject(LoaderService);
+ private teamSelection = inject(TeamSelectionService);
@Input() activityUuid: string = '';
@Input() expanded: boolean = false;
@@ -53,9 +55,12 @@ export class EvidencePanelComponent implements OnChanges {
this.evidenceEntries = evidenceStore.getEvidence(this.activityUuid);
+ const effectiveTeams = new Set(this.teamSelection.effectiveTeams());
+
// Group evidence entries by team name
for (const entry of this.evidenceEntries) {
for (const teamName of entry.teams) {
+ if (!effectiveTeams.has(teamName)) continue;
if (!this.evidenceByTeam.has(teamName)) {
this.evidenceByTeam.set(teamName, []);
}
@@ -63,6 +68,10 @@ export class EvidencePanelComponent implements OnChanges {
}
}
- this.teamsWithEvidence = Array.from(this.evidenceByTeam.keys());
+ // Evidence entries can be entered in any order. Display the teams using the
+ // configured order, while omitting teams without evidence for this activity.
+ this.teamsWithEvidence = this.teamSelection
+ .effectiveTeams()
+ .filter(teamName => this.evidenceByTeam.has(teamName));
}
}
diff --git a/src/app/component/report-config-modal/report-config-modal.component.html b/src/app/component/report-config-modal/report-config-modal.component.html
index f9842d48d..9f253e94e 100644
--- a/src/app/component/report-config-modal/report-config-modal.component.html
+++ b/src/app/component/report-config-modal/report-config-modal.component.html
@@ -256,9 +256,10 @@ Individual Activities
+ [selectedTeams]="selectedTeams"
+ (selectedTeamsChange)="onSelectedTeamsChange($event)"
+ [teamGroups]="teamGroups">
+
diff --git a/src/app/component/report-config-modal/report-config-modal.component.ts b/src/app/component/report-config-modal/report-config-modal.component.ts
index 05ab7d587..9d69f6b1d 100644
--- a/src/app/component/report-config-modal/report-config-modal.component.ts
+++ b/src/app/component/report-config-modal/report-config-modal.component.ts
@@ -23,6 +23,7 @@ import { MatButtonToggleModule } from '@angular/material/button-toggle';
export interface ReportConfigModalData {
config: ReportConfig;
+ selectedTeams: string[];
allActivities: Activity[];
allTeams: string[];
allDimensions: string[];
@@ -31,6 +32,11 @@ export interface ReportConfigModalData {
teamGroups: TeamGroups;
}
+export interface ReportConfigModalResult {
+ config: ReportConfig;
+ selectedTeams: string[];
+}
+
@Component({
selector: 'app-report-config-modal',
templateUrl: './report-config-modal.component.html',
@@ -57,11 +63,12 @@ export class ReportConfigModalComponent {
config = signal(JSON.parse(JSON.stringify({})));
allActivities: Activity[];
- allTeams: string[];
+ allTeams: string[] = [];
allDimensions: string[];
allSubdimensions: string[];
allProgressTitles: ProgressTitle[];
- teamGroups: TeamGroups;
+ teamGroups: TeamGroups = {};
+ selectedTeams: string[] = [];
activitySearchQuery = signal('');
maxWordCap: number = MAX_DESCRIPTION_WORD_CAP;
@@ -86,11 +93,12 @@ export class ReportConfigModalComponent {
// Deep copy config to avoid mutating the original until save
this.config.set(JSON.parse(JSON.stringify(data.config)));
this.allActivities = data.allActivities;
- this.allTeams = data.allTeams;
+ this.allTeams = [...data.allTeams];
this.allDimensions = data.allDimensions;
this.allSubdimensions = data.allSubdimensions;
this.allProgressTitles = data.allProgressTitles || [];
- this.teamGroups = data.teamGroups || {};
+ this.teamGroups = { ...data.teamGroups };
+ this.selectedTeams = [...data.selectedTeams];
}
setColumnGrouping(grouping: ColumnGrouping): void {
@@ -172,13 +180,13 @@ export class ReportConfigModalComponent {
});
}
- onTeamsChanged(teams: string[]): void {
- this.config.update(c => ({ ...c, selectedTeams: teams }));
+ onSelectedTeamsChange(teams: string[]): void {
+ this.selectedTeams = teams;
}
// --- Actions ---
onSave(): void {
- this.dialogRef.close(this.config());
+ this.dialogRef.close({ config: this.config(), selectedTeams: this.selectedTeams });
}
onCancel(): void {
diff --git a/src/app/component/team-selector/team-selector.component.ts b/src/app/component/team-selector/team-selector.component.ts
index e437f6399..8842ea675 100644
--- a/src/app/component/team-selector/team-selector.component.ts
+++ b/src/app/component/team-selector/team-selector.component.ts
@@ -21,42 +21,45 @@ export class TeamSelectorComponent {
@Output() selectedTeamsChange = new EventEmitter();
- selectedGroupName: string = '';
-
isTeamSelected(team: string): boolean {
return this.selectedTeams.includes(team);
}
toggleTeam(team: string): void {
- const idx = this.selectedTeams.indexOf(team);
- if (idx >= 0) {
- this.selectedTeams.splice(idx, 1);
+ const teams = [...this.selectedTeams];
+ const index = teams.indexOf(team);
+ if (index >= 0) {
+ teams.splice(index, 1);
} else {
- this.selectedTeams.push(team);
+ teams.push(team);
}
- this.selectedGroupName = '';
- this.selectedTeamsChange.emit([...this.selectedTeams]);
+ this.selectedTeamsChange.emit(teams);
}
selectAllTeams(): void {
- this.selectedTeams = [...this.allTeams];
- this.selectedGroupName = '';
- this.selectedTeamsChange.emit([...this.selectedTeams]);
+ this.selectedTeamsChange.emit([...this.allTeams]);
}
deselectAllTeams(): void {
- this.selectedTeams = [];
- this.selectedGroupName = '';
- this.selectedTeamsChange.emit([...this.selectedTeams]);
+ this.selectedTeamsChange.emit([]);
}
get groupNames(): string[] {
- return Object.keys(this.teamGroups);
+ return Object.keys(this.teamGroups).filter(group => this.teamGroups[group].length > 0);
}
selectGroup(group: string): void {
- this.selectedTeams = [...(this.teamGroups[group] || [])];
- this.selectedGroupName = group;
- this.selectedTeamsChange.emit([...this.selectedTeams]);
+ this.selectedTeamsChange.emit([...(this.teamGroups[group] || [])]);
+ }
+
+ get selectedGroupName(): string {
+ const selected = new Set(this.selectedTeams);
+ for (const group of this.groupNames) {
+ const teams = this.teamGroups[group];
+ if (teams.length === selected.size && teams.every(team => selected.has(team))) {
+ return group;
+ }
+ }
+ return '';
}
}
diff --git a/src/app/component/teams-groups-editor/selectable-list.component.css b/src/app/component/teams-groups-editor/selectable-list.component.css
index 9d90d91dd..88fe82819 100644
--- a/src/app/component/teams-groups-editor/selectable-list.component.css
+++ b/src/app/component/teams-groups-editor/selectable-list.component.css
@@ -40,6 +40,10 @@
margin-left: 0.5rem;
}
+.selectable-list .item-actions button {
+ margin-left: 0.25rem;
+}
+
.selectable-list .edit-hint {
font-weight: normal;
font-size: 0.8em;
diff --git a/src/app/component/teams-groups-editor/selectable-list.component.html b/src/app/component/teams-groups-editor/selectable-list.component.html
index cc53d9856..1a69052ec 100644
--- a/src/app/component/teams-groups-editor/selectable-list.component.html
+++ b/src/app/component/teams-groups-editor/selectable-list.component.html
@@ -45,7 +45,21 @@ {{ title }}
{{ name }}
}
@if (editMode) {
-
+
+
+
diff --git a/src/app/component/teams-groups-editor/selectable-list.component.ts b/src/app/component/teams-groups-editor/selectable-list.component.ts
index 752e0be60..c8a61a685 100644
--- a/src/app/component/teams-groups-editor/selectable-list.component.ts
+++ b/src/app/component/teams-groups-editor/selectable-list.component.ts
@@ -12,6 +12,13 @@ import { FormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
+export type MoveDirection = 'up' | 'down';
+
+export interface MoveItemEvent {
+ name: string;
+ direction: MoveDirection;
+}
+
@Component({
selector: 'app-selectable-list',
templateUrl: './selectable-list.component.html',
@@ -35,6 +42,7 @@ export class SelectableListComponent {
@Output() save = new EventEmitter();
@Output() renameItem = new EventEmitter<{ oldName: string; newName: string }>();
@Output() deleteItem = new EventEmitter();
+ @Output() moveItem = new EventEmitter();
@Output() relationshipToggle = new EventEmitter();
@Output() editModeChange = new EventEmitter();
@@ -102,4 +110,12 @@ export class SelectableListComponent {
this.onItemClicked(this.items[index - 1]);
}
}
+
+ moveItemUp(name: string): void {
+ this.moveItem.emit({ name, direction: 'up' });
+ }
+
+ moveItemDown(name: string): void {
+ this.moveItem.emit({ name, direction: 'down' });
+ }
}
diff --git a/src/app/component/teams-groups-editor/teams-groups-editor.component.html b/src/app/component/teams-groups-editor/teams-groups-editor.component.html
index a266ed31e..c5e112389 100644
--- a/src/app/component/teams-groups-editor/teams-groups-editor.component.html
+++ b/src/app/component/teams-groups-editor/teams-groups-editor.component.html
@@ -13,6 +13,7 @@
(addItem)="onAddGroup()"
(renameItem)="onRenameGroup($event)"
(deleteItem)="onDeleteGroup($event)"
+ (moveItem)="onMoveGroup($event)"
(editModeChange)="onGroupsEditModeChange($event)"
(relationshipToggle)="onTeamGroupToggle(selectedTeam, $event)"
(save)="onSave()"
@@ -31,6 +32,7 @@
(addItem)="onAddTeam()"
(renameItem)="onRenameTeam($event)"
(deleteItem)="onDeleteTeam($event)"
+ (moveItem)="onMoveTeam($event)"
(editModeChange)="onTeamsEditModeChange($event)"
(relationshipToggle)="onTeamGroupToggle($event, selectedGroup)"
(save)="onSave()"
diff --git a/src/app/component/teams-groups-editor/teams-groups-editor.component.ts b/src/app/component/teams-groups-editor/teams-groups-editor.component.ts
index 9944c5db3..57bbcb0f1 100644
--- a/src/app/component/teams-groups-editor/teams-groups-editor.component.ts
+++ b/src/app/component/teams-groups-editor/teams-groups-editor.component.ts
@@ -9,7 +9,7 @@ import {
} from '@angular/core';
import { GroupName, TeamGroups, TeamName, TeamNames } from 'src/app/model/types';
import { perfNow, renameArrayElement } from 'src/app/util/util';
-import { SelectableListComponent } from './selectable-list.component';
+import { MoveItemEvent, SelectableListComponent } from './selectable-list.component';
enum EditMode {
NONE,
@@ -73,6 +73,7 @@ export class TeamsGroupsEditorComponent implements OnChanges {
makeLocalCopy() {
this.localCopyTeams = this.teams.slice();
this.localCopyTeamGroups = this.cloneTeamGroups(this.teamGroups);
+ this.localCopyTeamsRenamed = {};
}
saveLocalCopy() {
@@ -199,6 +200,7 @@ export class TeamsGroupsEditorComponent implements OnChanges {
event.newName
);
}
+ this.recordTeamRename(event.oldName, event.newName);
this.onTeamSelected(event.newName);
}
@@ -218,8 +220,12 @@ export class TeamsGroupsEditorComponent implements OnChanges {
onRenameGroup(event: { oldName: string; newName: string }) {
console.log(`${perfNow()}: Rename team: ${event.oldName} to ${event.newName}`);
- this.localCopyTeamGroups[event.newName] = this.localCopyTeamGroups[event.oldName] || [];
- delete this.localCopyTeamGroups[event.oldName];
+ const renamedGroups: TeamGroups = {};
+ for (const group of Object.keys(this.localCopyTeamGroups)) {
+ renamedGroups[group === event.oldName ? event.newName : group] =
+ this.localCopyTeamGroups[group];
+ }
+ this.localCopyTeamGroups = renamedGroups;
this.onGroupSelected(event.newName);
}
@@ -227,16 +233,21 @@ export class TeamsGroupsEditorComponent implements OnChanges {
delete this.localCopyTeamGroups[group];
}
- onSave() {
- console.log(`${perfNow()}: Saving teams and groups`);
+ onMoveTeam(event: MoveItemEvent): void {
+ this.localCopyTeams = this.moveArrayItem(this.localCopyTeams, event);
+ }
- // Identify the teams that have changed names
- let teamsRenamed: Record = {};
- for (let i = 0; i < this.teams.length; i++) {
- if (this.teams[i] !== this.localCopyTeams[i]) {
- teamsRenamed[this.teams[i]] = this.localCopyTeams[i];
- }
+ onMoveGroup(event: MoveItemEvent): void {
+ const groupNames = this.moveArrayItem(Object.keys(this.localCopyTeamGroups), event);
+ const reorderedGroups: TeamGroups = {};
+ for (const group of groupNames) {
+ reorderedGroups[group] = this.localCopyTeamGroups[group];
}
+ this.localCopyTeamGroups = reorderedGroups;
+ }
+
+ onSave() {
+ console.log(`${perfNow()}: Saving teams and groups`);
this.editMode = EditMode.NONE;
@@ -246,8 +257,9 @@ export class TeamsGroupsEditorComponent implements OnChanges {
this.namesChanged.emit({
teams: this.teams,
teamGroups: this.teamGroups,
- teamsRenamed: teamsRenamed,
+ teamsRenamed: { ...this.localCopyTeamsRenamed },
});
+ this.localCopyTeamsRenamed = {};
}
onCancelEdit() {
@@ -286,4 +298,31 @@ export class TeamsGroupsEditorComponent implements OnChanges {
keys(obj: any): string[] {
return Object.keys(obj);
}
+
+ private moveArrayItem(items: string[], event: MoveItemEvent): string[] {
+ const currentIndex = items.indexOf(event.name);
+ const destinationIndex = event.direction === 'up' ? currentIndex - 1 : currentIndex + 1;
+
+ if (currentIndex === -1 || destinationIndex < 0 || destinationIndex >= items.length) {
+ return items;
+ }
+
+ const reordered = items.slice();
+ [reordered[currentIndex], reordered[destinationIndex]] = [
+ reordered[destinationIndex],
+ reordered[currentIndex],
+ ];
+ return reordered;
+ }
+
+ private recordTeamRename(oldName: TeamName, newName: TeamName): void {
+ const originalName =
+ Object.keys(this.localCopyTeamsRenamed).find(
+ name => this.localCopyTeamsRenamed[name] === oldName
+ ) || oldName;
+
+ if (this.teams.includes(originalName)) {
+ this.localCopyTeamsRenamed[originalName] = newName;
+ }
+ }
}
diff --git a/src/app/model/report-config.ts b/src/app/model/report-config.ts
index 79b4d801b..268a336ef 100644
--- a/src/app/model/report-config.ts
+++ b/src/app/model/report-config.ts
@@ -26,7 +26,6 @@ export interface ActivityAttributes {
export interface ReportConfig {
columnGrouping: ColumnGrouping;
descriptionWordCap: number;
- selectedTeams: string[];
excludedDimensions: string[];
excludedSubdimensions: string[];
excludedActivities: string[];
@@ -66,7 +65,6 @@ export function getDefaultReportConfig(): ReportConfig {
return {
columnGrouping: 'byProgress',
descriptionWordCap: DEFAULT_DESCRIPTION_WORD_CAP,
- selectedTeams: [],
excludedDimensions: [],
excludedSubdimensions: [],
excludedActivities: [],
@@ -118,15 +116,21 @@ export function getReportConfig(): ReportConfig {
showTags: parsedAttrs.showTags ?? defaultAttrs.showTags,
};
- return {
+ const config: ReportConfig = {
columnGrouping: parsed.columnGrouping ?? defaults.columnGrouping,
descriptionWordCap: parsed.descriptionWordCap ?? defaults.descriptionWordCap,
- selectedTeams: parsed.selectedTeams ?? defaults.selectedTeams,
excludedDimensions: parsed.excludedDimensions ?? defaults.excludedDimensions,
excludedSubdimensions: parsed.excludedSubdimensions ?? defaults.excludedSubdimensions,
excludedActivities: parsed.excludedActivities ?? defaults.excludedActivities,
activityAttributes,
};
+
+ // Remove the legacy team selection from saved report settings.
+ if ('selectedTeams' in parsed) {
+ saveReportConfig(config);
+ }
+
+ return config;
}
} catch (e) {
console.error('Error reading ReportConfig from localStorage:', e);
diff --git a/src/app/pages/circular-heatmap/circular-heatmap.component.html b/src/app/pages/circular-heatmap/circular-heatmap.component.html
index 8527c78a7..f91b830f0 100644
--- a/src/app/pages/circular-heatmap/circular-heatmap.component.html
+++ b/src/app/pages/circular-heatmap/circular-heatmap.component.html
@@ -46,7 +46,7 @@ Nothing to show
- @for (team of filtersTeams() | keyvalue; track team.key) {
+ @for (team of filtersTeams() | keyvalue: unsorted; track team.key) {
{{ team.key }}
@@ -86,7 +86,7 @@ Nothing to show
- @for (team of filtersTeams() | keyvalue; track team) {
+ @for (team of filtersTeams() | keyvalue: unsorted; track team.key) {
@if (team.value || !hasTeamsFilter()) {
-
diff --git a/src/app/pages/circular-heatmap/circular-heatmap.component.ts b/src/app/pages/circular-heatmap/circular-heatmap.component.ts
index a33ec5e0f..92a60ee11 100644
--- a/src/app/pages/circular-heatmap/circular-heatmap.component.ts
+++ b/src/app/pages/circular-heatmap/circular-heatmap.component.ts
@@ -7,6 +7,7 @@ import {
DestroyRef,
afterNextRender,
ChangeDetectionStrategy,
+ untracked,
} from '@angular/core';
import { LoaderService } from 'src/app/service/loader/data-loader.service';
import * as d3 from 'd3';
@@ -33,6 +34,7 @@ import {
TeamGroups,
} from 'src/app/model/types';
import { SectorService } from '../../service/sector-service';
+import { TeamSelectionService } from '../../service/team-selection.service';
import { DataStore } from 'src/app/model/data-store';
import { Sector } from 'src/app/model/sector';
import { perfNow } from 'src/app/util/util';
@@ -80,6 +82,7 @@ import { ActivityDescriptionComponent } from '../../component/activity-descripti
export class CircularHeatmapComponent {
private loader = inject(LoaderService);
private sectorService = inject(SectorService);
+ readonly teamSelection = inject(TeamSelectionService);
private settings = inject(SettingsService);
private themeService = inject(ThemeService);
private titleService = inject(TitleService);
@@ -103,14 +106,33 @@ export class CircularHeatmapComponent {
readonly showActivityCard = signal(null);
readonly showActivityDetails = signal(null);
readonly dataStore = signal(null);
- readonly filtersTeams = signal>({});
- readonly filtersTeamGroups = signal>({});
- readonly teamGroups = signal({});
readonly allSectors = signal([]);
readonly selectedSector = signal(null);
- // ── Computed ──
- readonly hasTeamsFilter = computed(() => Object.values(this.filtersTeams()).some(v => v));
+ readonly filtersTeams = computed>(() => {
+ const all = this.teamSelection.allTeams();
+ const selected = new Set(this.teamSelection.selectedTeams());
+ const result: Record = {};
+ for (const team of all) {
+ result[team] = selected.has(team);
+ }
+ return result;
+ });
+
+ readonly filtersTeamGroups = computed>(() => {
+ const groups = this.teamSelection.teamGroups();
+ const selectedGroup = this.teamSelection.selectedGroupName();
+ const isAll = this.teamSelection.isAllSelected();
+ const result: Record = {};
+ for (const key of Object.keys(groups)) {
+ result[key] = isAll ? key === Object.keys(groups)[0] : key === selectedGroup;
+ }
+ return result;
+ });
+
+ readonly teamGroups = computed(() => this.teamSelection.teamGroups());
+
+ readonly hasTeamsFilter = computed(() => this.teamSelection.selectedTeams().length > 0);
constructor() {
this.destroyRef.onDestroy(() => this.titleService.clearTitle());
@@ -130,13 +152,12 @@ export class CircularHeatmapComponent {
throw Error('No progressStore available');
}
- this.filtersTeams.set(this.buildFilters(dataStore.meta?.teams as string[]));
- // Insert key: 'All' with value: [], in the first position of the meta.teamGroups Record
const allTeamsGroupName: string = dataStore.getMetaString('allTeamsGroupName') || 'All';
- this.teamGroups.set({ [allTeamsGroupName]: [], ...(dataStore.meta?.teamGroups || {}) });
- const groupFilters = this.buildFilters(Object.keys(this.teamGroups()));
- groupFilters[allTeamsGroupName] = true;
- this.filtersTeamGroups.set(groupFilters);
+ const groups: TeamGroups = {
+ [allTeamsGroupName]: [],
+ ...(dataStore.meta?.teamGroups || {}),
+ };
+ this.teamSelection.init(dataStore.meta?.teams || [], groups);
let progressDefinition: ProgressDefinitions = dataStore.meta?.progressDefinition || {};
this.sectorService.init(
@@ -178,6 +199,17 @@ export class CircularHeatmapComponent {
this.reColorHeatmap();
}
});
+
+ // Reactively sync SectorService when global team selection changes
+ effect(() => {
+ const selected = this.teamSelection.selectedTeams();
+ untracked(() => {
+ this.sectorService.setVisibleTeams(selected);
+ if (this.allSectors().length > 0) {
+ this.reColorHeatmap();
+ }
+ });
+ });
}
private readThemeColors(): void {
@@ -229,16 +261,6 @@ export class CircularHeatmapComponent {
this.allSectors.set(sectors);
}
- buildFilters(names: string[]): Record {
- let filters: Record = {};
- if (names) {
- for (let name of names) {
- filters[name] = false;
- }
- }
- return filters;
- }
-
onGroupChipChange(event: MatChipSelectionChange, groupKey: string) {
if (!event.selected && event.isUserInput) {
event.source.select();
@@ -249,50 +271,19 @@ export class CircularHeatmapComponent {
console.log(`${perfNow()}: Heat: Chip flip Group '${groupKey}'`);
- const newGroupFilters: Record = {};
- Object.keys(this.filtersTeamGroups()).forEach(key => {
- newGroupFilters[key] = key === groupKey;
- });
- this.filtersTeamGroups.set(newGroupFilters);
-
const groups = this.teamGroups();
- const selectedTeams: TeamName[] = [];
- const newTeamFilters: Record = {};
- Object.keys(this.filtersTeams()).forEach(key => {
- newTeamFilters[key] = groups[groupKey]?.includes(key) || false;
- if (newTeamFilters[key]) selectedTeams.push(key);
- });
- this.filtersTeams.set(newTeamFilters);
- this.sectorService.setVisibleTeams(selectedTeams);
- this.reColorHeatmap();
+ const groupTeams = groups[groupKey];
+ if (groupTeams && groupTeams.length > 0) {
+ this.teamSelection.selectGroup(groupKey);
+ } else {
+ this.teamSelection.selectAll();
+ }
}
toggleTeamFilter(event: MatChipListboxChange) {
const selectedTeams: string[] = event.value || [];
console.log(`${perfNow()}: Heat: Team filter changed: [${selectedTeams.join(', ')}]`);
-
- const newTeamFilters: Record = {};
- Object.keys(this.filtersTeams()).forEach(key => {
- newTeamFilters[key] = selectedTeams.includes(key);
- });
- this.filtersTeams.set(newTeamFilters);
-
- this.sectorService.setVisibleTeams(selectedTeams);
-
- // Set-based comparison for group highlight (fixes order-sensitive bug)
- const selectedSet = new Set(selectedTeams);
- const groups = this.teamGroups();
- const newGroupFilters: Record = {};
- Object.keys(groups).forEach(group => {
- const groupTeams = groups[group];
- newGroupFilters[group] =
- groupTeams.length > 0 &&
- groupTeams.length === selectedSet.size &&
- groupTeams.every(t => selectedSet.has(t));
- });
- this.filtersTeamGroups.set(newGroupFilters);
-
- this.reColorHeatmap();
+ this.teamSelection.setTeams(selectedTeams);
}
getTeamProgressState(activityUuid: string, teamName: string): string {
@@ -874,12 +865,9 @@ export class CircularHeatmapComponent {
openAddEvidenceModal(activityUuid: string): void {
const ds = this.dataStore();
- const teams = ds?.meta?.teams || [];
const dialogData: AddEvidenceModalData = {
activityUuid,
- allTeams: teams,
- teamGroups: this.teamGroups(),
};
const dialogRef = this.dialog.open(AddEvidenceModalComponent, {
diff --git a/src/app/pages/report/report.component.html b/src/app/pages/report/report.component.html
index 5cc130e99..d44ed2c6d 100644
--- a/src/app/pages/report/report.component.html
+++ b/src/app/pages/report/report.component.html
@@ -13,8 +13,8 @@
@if (!isLoading()) {
{{ totalFilteredActivities() }} activities
- @if (reportConfig().selectedTeams.length > 0) {
- · {{ reportConfig().selectedTeams.length }} teams
+ @if (teamSelection.effectiveTeams().length > 0) {
+ · {{ teamSelection.effectiveTeams().length }} teams
}
}
@@ -22,9 +22,9 @@
@@ -101,7 +101,7 @@
{{ subDimension.name }}
}
@if (reportConfig().columnGrouping === 'byTeam') {
- @for (team of reportConfig().selectedTeams; track team) {
+ @for (team of teamSelection.effectiveTeams(); track team) {
{{ team }}
|
@@ -175,7 +175,7 @@ {{ subDimension.name }}
}
@if (reportConfig().columnGrouping === 'byTeam') {
- @for (team of reportConfig().selectedTeams; track team) {
+ @for (team of teamSelection.effectiveTeams(); track team) {
{{ getTeamProgressName(activity, team) }}
|
diff --git a/src/app/pages/report/report.component.ts b/src/app/pages/report/report.component.ts
index 9b37c18ba..18b20ef73 100644
--- a/src/app/pages/report/report.component.ts
+++ b/src/app/pages/report/report.component.ts
@@ -9,6 +9,7 @@ import {
import { MatDialog } from '@angular/material/dialog';
import { LoaderService } from '../../service/loader/data-loader.service';
import { SettingsService } from '../../service/settings/settings.service';
+import { TeamSelectionService } from '../../service/team-selection.service';
import {
Activity,
DifficultyOfImplementation,
@@ -22,6 +23,7 @@ import { ReportConfig, getReportConfig, saveReportConfig } from '../../model/rep
import {
ReportConfigModalComponent,
ReportConfigModalData,
+ ReportConfigModalResult,
} from '../../component/report-config-modal/report-config-modal.component';
import { ProgressTitle, TeamGroups } from '../../model/types';
import { EvidenceEntry } from '../../model/evidence-store';
@@ -77,6 +79,7 @@ export class ReportComponent implements OnInit {
private settings = inject(SettingsService);
private dialog = inject(MatDialog);
private datePipe = inject(DatePipe);
+ readonly teamSelection = inject(TeamSelectionService);
reportConfig = signal(getReportConfig());
allActivities = signal([]);
@@ -141,7 +144,7 @@ export class ReportComponent implements OnInit {
});
levelByLevelOverviewFromActivties = computed(() => {
- const config = this.reportConfig();
+ const selectedTeams = this.teamSelection.effectiveTeams();
const dims = this.filteredDimensions();
const activities: Activity[] = [];
@@ -160,8 +163,8 @@ export class ReportComponent implements OnInit {
const entry = levelMap.get(activity.level)!;
entry.total++;
- if (config.selectedTeams.length > 0) {
- const allCompleted = config.selectedTeams.every(team =>
+ if (selectedTeams.length > 0) {
+ const allCompleted = selectedTeams.every(team =>
this.isActivityCompletedByTeam(activity, team)
);
if (allCompleted) {
@@ -226,6 +229,7 @@ export class ReportComponent implements OnInit {
this.allSubdimensionNames = Array.from(subdimensionSet).sort();
this.allTeams = dataStore?.meta?.teams || [];
this.teamGroups = dataStore?.meta?.teamGroups || {};
+ this.teamSelection.init(this.allTeams, this.teamGroups);
if (dataStore.progressStore) {
const inProgress = dataStore.progressStore.getInProgressTitles();
@@ -233,11 +237,6 @@ export class ReportComponent implements OnInit {
this.allProgressTitles = [...inProgress, completed].filter(t => !!t);
}
- const currentConfig = this.reportConfig();
- if (currentConfig.selectedTeams.length === 0 && this.allTeams.length > 0) {
- this.reportConfig.set({ ...currentConfig, selectedTeams: [...this.allTeams] });
- }
-
this.allActivities.set(activities);
this.isLoading.set(false);
})
@@ -266,7 +265,7 @@ export class ReportComponent implements OnInit {
getTeamsForProgress(activity: Activity, progressTitle: ProgressTitle): string {
if (!this.progressStore || !activity.uuid) return '';
const teams: string[] = [];
- for (const team of this.reportConfig().selectedTeams) {
+ for (const team of this.teamSelection.effectiveTeams()) {
const teamTitle = this.progressStore.getTeamProgressTitle(activity.uuid, team);
if (teamTitle === progressTitle) {
teams.push(team);
@@ -329,12 +328,13 @@ export class ReportComponent implements OnInit {
openConfigModal(): void {
const modalData: ReportConfigModalData = {
config: this.reportConfig(),
+ allTeams: this.teamSelection.allTeams(),
+ teamGroups: this.teamSelection.teamGroups(),
+ selectedTeams: this.teamSelection.effectiveTeams(),
allActivities: this.allActivities(),
- allTeams: this.allTeams,
allDimensions: this.allDimensionNames,
allSubdimensions: this.allSubdimensionNames,
allProgressTitles: this.allProgressTitles,
- teamGroups: this.loader.datastore?.meta?.teamGroups || {},
};
const dialogRef = this.dialog.open(ReportConfigModalComponent, {
@@ -342,18 +342,17 @@ export class ReportComponent implements OnInit {
data: modalData,
});
- dialogRef.afterClosed().subscribe((result: ReportConfig | null) => {
+ dialogRef.afterClosed().subscribe((result: ReportConfigModalResult | null) => {
if (result) {
- this.reportConfig.set(result);
- saveReportConfig(result);
+ this.teamSelection.setTeams(result.selectedTeams);
+ this.reportConfig.set(result.config);
+ saveReportConfig(result.config);
}
});
}
onTeamsChanged(teams: string[]): void {
- const updated = { ...this.reportConfig(), selectedTeams: teams };
- this.reportConfig.set(updated);
- saveReportConfig(updated);
+ this.teamSelection.setTeams(teams);
}
printReport(): void {
@@ -449,7 +448,7 @@ export class ReportComponent implements OnInit {
const allEntries: EvidenceEntry[] = evidenceStore.getEvidence(activity.uuid);
const attrs = this.reportConfig().activityAttributes;
- const selectedTeams = this.reportConfig().selectedTeams;
+ const selectedTeams = this.teamSelection.effectiveTeams();
const entries = allEntries.filter(entry => entry.teams.some(t => selectedTeams.includes(t)));
@@ -486,7 +485,7 @@ export class ReportComponent implements OnInit {
}
// Show only the selected teams for this entry
- const matchingTeams = entry.teams.filter(t => selectedTeams.includes(t));
+ const matchingTeams = selectedTeams.filter(team => entry.teams.includes(team));
const teamsLabel =
matchingTeams.length > 0
? `[${matchingTeams.join(', ')}] `
diff --git a/src/app/pages/teams/teams.component.ts b/src/app/pages/teams/teams.component.ts
index be78aae6c..36889d148 100644
--- a/src/app/pages/teams/teams.component.ts
+++ b/src/app/pages/teams/teams.component.ts
@@ -27,6 +27,7 @@ import {
import { TeamGroups, TeamName, TeamNames, TeamProgress, Uuid } from 'src/app/model/types';
import { LoaderService } from 'src/app/service/loader/data-loader.service';
import { SettingsService } from 'src/app/service/settings/settings.service';
+import { TeamSelectionService } from 'src/app/service/team-selection.service';
import { downloadYamlFile } from 'src/app/util/download';
import { isEmptyObj, perfNow, dateStr, uniqueCount } from 'src/app/util/util';
import { MatIconModule } from '@angular/material/icon';
@@ -53,6 +54,7 @@ import { TopHeaderComponent } from '../../component/top-header/top-header.compon
})
export class TeamsComponent implements OnInit, AfterViewInit {
private loader = inject(LoaderService);
+ private teamSelection = inject(TeamSelectionService);
settings = inject(SettingsService);
modal = inject(ModalMessageComponent);
@@ -172,9 +174,17 @@ export class TeamsComponent implements OnInit, AfterViewInit {
console.log(`${perfNow()}: Saving groups: ${JSON.stringify(event.teamGroups)}`);
const ds = this.dataStore();
ds?.meta?.updateTeamsAndGroups(event.teams, event.teamGroups);
+ if (ds) {
+ const allTeamsGroupName = ds.getMetaString('allTeamsGroupName') || 'All';
+ this.teamSelection.init(event.teams, {
+ [allTeamsGroupName]: [],
+ ...event.teamGroups,
+ });
+ }
if (!isEmptyObj(event.teamsRenamed)) {
for (let oldName in event.teamsRenamed) {
ds?.progressStore?.renameTeam(oldName, event.teamsRenamed[oldName]);
+ ds?.evidenceStore?.renameTeam(oldName, event.teamsRenamed[oldName]);
}
ds?.progressStore?.saveToLocalStorage();
}
diff --git a/src/app/service/team-selection.service.ts b/src/app/service/team-selection.service.ts
new file mode 100644
index 000000000..0d1a49cf8
--- /dev/null
+++ b/src/app/service/team-selection.service.ts
@@ -0,0 +1,89 @@
+import { Injectable, signal, computed } from '@angular/core';
+import { TeamGroups, TeamNames } from '../model/types';
+
+@Injectable({ providedIn: 'root' })
+export class TeamSelectionService {
+ readonly allTeams = signal([]);
+ readonly teamGroups = signal({});
+ readonly selectedTeams = signal([]);
+ readonly initialised = signal(false);
+
+ readonly selectedGroupName = computed(() => {
+ const selected = new Set(this.selectedTeams());
+ if (selected.size === 0) return '';
+
+ const groups = this.teamGroups();
+ for (const [name, members] of Object.entries(groups)) {
+ if (
+ members.length > 0 &&
+ members.length === selected.size &&
+ members.every(t => selected.has(t))
+ ) {
+ return name;
+ }
+ }
+ return '';
+ });
+
+ readonly isAllSelected = computed(() => {
+ const selected = this.selectedTeams();
+ const allTeams = this.allTeams();
+ return (
+ selected.length === 0 ||
+ (allTeams.length > 0 &&
+ selected.length === allTeams.length &&
+ allTeams.every(team => selected.includes(team)))
+ );
+ });
+
+ readonly effectiveTeams = computed(() => {
+ const selected = this.selectedTeams();
+ const allTeams = this.allTeams();
+ if (selected.length === 0) return allTeams;
+ const selectedSet = new Set(selected);
+ return allTeams.filter(team => selectedSet.has(team));
+ });
+
+ init(teams: TeamNames, groups: TeamGroups): void {
+ this.allTeams.set([...teams]);
+ this.teamGroups.set({ ...groups });
+
+ this.initialised.set(true);
+
+ const validTeams = this.selectedTeams().filter(t => teams.includes(t));
+ if (validTeams.length !== this.selectedTeams().length) {
+ this.setTeams(validTeams);
+ }
+ }
+
+ setTeams(teams: string[]): void {
+ this.selectedTeams.set([...teams]);
+ }
+
+ toggleTeam(team: string): void {
+ const current = this.selectedTeams();
+ const idx = current.indexOf(team);
+ const next = [...current];
+ if (idx >= 0) {
+ next.splice(idx, 1);
+ } else {
+ next.push(team);
+ }
+ this.selectedTeams.set(next);
+ }
+
+ selectGroup(groupName: string): void {
+ const members = this.teamGroups()[groupName];
+ if (members) {
+ this.selectedTeams.set([...members]);
+ }
+ }
+
+ selectAll(): void {
+ this.selectedTeams.set([]);
+ }
+
+ deselectAll(): void {
+ this.selectedTeams.set([]);
+ }
+}