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..39d4d6bcd 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, []);
}
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/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.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..5db44f0c9 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)));
diff --git a/src/app/service/team-selection.service.ts b/src/app/service/team-selection.service.ts
new file mode 100644
index 000000000..0562b7db4
--- /dev/null
+++ b/src/app/service/team-selection.service.ts
@@ -0,0 +1,86 @@
+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();
+ return selected.length === 0 ? this.allTeams() : selected;
+ });
+
+ 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([]);
+ }
+}