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([]); + } +}