From fb0c86da3ff21e48fd09021c6d89a0b9b6db5591 Mon Sep 17 00:00:00 2001 From: TO Date: Mon, 31 Aug 2026 00:23:09 +0200 Subject: [PATCH] fix(ui): hide planning details by default --- .../calendar-year-planning.spec.ts | 25 +- .../calendar-year/calendar-year.html | 630 +++++++++--------- .../calendar-year/calendar-year.scss | 8 + .../components/calendar-year/calendar-year.ts | 15 +- 4 files changed, 362 insertions(+), 316 deletions(-) diff --git a/src/app/components/calendar-year/calendar-year-planning.spec.ts b/src/app/components/calendar-year/calendar-year-planning.spec.ts index 8864263..620dde8 100644 --- a/src/app/components/calendar-year/calendar-year-planning.spec.ts +++ b/src/app/components/calendar-year/calendar-year-planning.spec.ts @@ -235,7 +235,7 @@ describe('CalendarYear planning outcome boundary', () => { expect(summaryValues).toEqual(['2', '4', '6']); }); - it('keeps an incomplete incumbent visible with its interrupted-stage explanation', async () => { + it('keeps an incomplete incumbent visible and exposes its interrupted-stage details', async () => { const outcome: PlanningIncompleteOutcome = { kind: 'PlanningIncomplete', incumbent: plan, @@ -266,13 +266,17 @@ describe('CalendarYear planning outcome boundary', () => { await new Promise((resolve) => setTimeout(resolve, 0)); fixture.detectChanges(); + const details = fixture.nativeElement.querySelector('.plan-details') as HTMLDetailsElement; + details.open = true; + details.dispatchEvent(new Event('toggle')); + fixture.detectChanges(); expect(fixture.nativeElement.querySelector('.incomplete-incumbent').textContent).toContain( 'holiday-week-value', ); expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(2); }); - it('shows the stable placement explanation by default and keeps the full plan visible', async () => { + it('keeps the simple result visible and reveals placement details on demand', async () => { const { calendar, calculation } = calendarFor(plan, explanation); await TestBed.configureTestingModule({ imports: [CalendarYear], @@ -287,10 +291,18 @@ describe('CalendarYear planning outcome boundary', () => { await new Promise((resolve) => setTimeout(resolve, 0)); fixture.detectChanges(); - expect(fixture.nativeElement.querySelector('.placement-explanation')).not.toBeNull(); - expect(fixture.nativeElement.textContent).toContain('Paid leave'); + const details = fixture.nativeElement.querySelector('.plan-details') as HTMLDetailsElement; + expect(details.open).toBe(false); expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(2); + expect(fixture.nativeElement.querySelector('button.current')).toBeNull(); + expect(fixture.nativeElement.querySelectorAll('.legend-symbol')).toHaveLength(0); + expect(calculation.analyzeCounterfactual).not.toHaveBeenCalled(); + + details.open = true; + details.dispatchEvent(new Event('toggle')); + fixture.detectChanges(); expect(calculation.analyzeCounterfactual).toHaveBeenCalledWith(plan.placements[0]); + expect(fixture.nativeElement.textContent).toContain('Paid leave'); const next = fixture.nativeElement.querySelector( 'button[data-step="next"]', @@ -300,6 +312,11 @@ describe('CalendarYear planning outcome boundary', () => { expect(fixture.nativeElement.querySelector('button.current')?.dataset.date).toBe( '2024-05-06', ); + + details.open = false; + details.dispatchEvent(new Event('toggle')); + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('button.current')).toBeNull(); }); it('renders conflict causes and replays an atomic trace decision', async () => { diff --git a/src/app/components/calendar-year/calendar-year.html b/src/app/components/calendar-year/calendar-year.html index 9e14ddb..12dc68d 100644 --- a/src/app/components/calendar-year/calendar-year.html +++ b/src/app/components/calendar-year/calendar-year.html @@ -137,322 +137,333 @@ } @if (selectedExplanation(); as selected) { -
-
-
-

{{ tr().planning.whyPlacement }}

-

- {{ selected.placement.date }} · {{ balanceLabel(selected) }} -

-
- -
- @if (planExplanation()!.incomplete; as interrupted) { -
- {{ tr().planning.incumbentExplanation }} -

{{ interrupted.reason.message }}

-

- {{ tr().planning.completedStages }}: - {{ - interrupted.completedStages.length - ? interrupted.completedStages.join(', ') - : tr().planning.none - }} -

-

- {{ tr().planning.uncompletedStages }}: - {{ - interrupted.uncompletedStages.length - ? interrupted.uncompletedStages.join(', ') - : tr().planning.none - }} -

-
- } + + + + {{ tr().planning.placement }} {{ selectedPlacementIndex() + 1 }} + {{ tr().planning.of }} {{ planExplanation()!.placements.length }} + + + + + + @if (planExplanation()!.incomplete; as interrupted) { +
+ {{ tr().planning.incumbentExplanation }} +

{{ interrupted.reason.message }}

+

+ {{ tr().planning.completedStages }}: + {{ + interrupted.completedStages.length + ? interrupted.completedStages.join(', ') + : tr().planning.none + }} +

+

+ {{ tr().planning.uncompletedStages }}: + {{ + interrupted.uncompletedStages.length + ? interrupted.uncompletedStages.join(', ') + : tr().planning.none + }} +

+
+ } -
-
-
{{ tr().planning.balance }}
-
- {{ balanceLabel(selected) }} {{ selected.placement.balanceId }} - @if (selected.balance) { -
{{ planningDate(selected.balance.availableFrom) }} – - {{ planningDate(selected.balance.expiryDate) }} - } -
-
-
-
{{ tr().planning.rules }}
-
- @if (selected.ruleIds.length) { - @for (ruleId of selected.ruleIds; track ruleId; let last = $last) { - {{ ruleLabel(ruleId) }} {{ ruleId }}{{ last ? '' : ', ' }} +
+
+
{{ tr().planning.balance }}
+
+ {{ balanceLabel(selected) }} + {{ selected.placement.balanceId }} + @if (selected.balance) { +
{{ planningDate(selected.balance.availableFrom) }} – + {{ planningDate(selected.balance.expiryDate) }} } - } @else { - {{ tr().planning.noRules }} - } -
-
-
-
{{ tr().planning.supporting }}
-
{{ selected.supporting ? tr().planning.yes : tr().planning.no }}
-
-
-
{{ tr().planning.remaining }}
-
- {{ selected.remainingQuantity }} - @if (selected.remainingBalance) { -
- {{ formatIdentifier(selected.remainingBalance.classification) }} · - {{ selected.remainingBalance.reason.message }} - - } -
-
- @if (selected.timeOffPeriod; as period) { -
-
{{ tr().planning.timeOffPeriod }}
-
{{ period.start }} – {{ period.end }} ({{ period.duration }})
+
- } - @if (selected.firstDecisiveObjective; as decisive) {
-
{{ tr().planning.firstDecisive }}
-
{{ formatIdentifier(decisive) }}
+
{{ tr().planning.rules }}
+
+ @if (selected.ruleIds.length) { + @for (ruleId of selected.ruleIds; track ruleId; let last = $last) { + {{ ruleLabel(ruleId) }} {{ ruleId }}{{ last ? '' : ', ' }} + } + } @else { + {{ tr().planning.noRules }} + } +
- } -
+
+
{{ tr().planning.supporting }}
+
{{ selected.supporting ? tr().planning.yes : tr().planning.no }}
+
+
+
{{ tr().planning.remaining }}
+
+ {{ selected.remainingQuantity }} + @if (selected.remainingBalance) { +
+ {{ + formatIdentifier(selected.remainingBalance.classification) + }} + · + {{ selected.remainingBalance.reason.message }} + + } +
+
+ @if (selected.timeOffPeriod; as period) { +
+
{{ tr().planning.timeOffPeriod }}
+
{{ period.start }} – {{ period.end }} ({{ period.duration }})
+
+ } + @if (selected.firstDecisiveObjective; as decisive) { +
+
{{ tr().planning.firstDecisive }}
+
{{ formatIdentifier(decisive) }}
+
+ } + -
-
-

{{ tr().planning.contributions }}

+
+
+

{{ tr().planning.contributions }}

+
    + @for ( + contribution of selected.contributions; + track contribution.kind + contribution.code + ) { +
  • + {{ contribution.label }} + @if (contribution.value !== undefined) { + · {{ contribution.value }} + } + @if (contribution.dates?.length) { + · {{ contribution.dates!.join(', ') }} + } +
  • + } +
+
+
+

{{ tr().planning.objectiveVector }}

+
    + @for ( + objective of selected.objectiveVector.objectives; + track objective.id + ) { +
  1. + {{ objectiveLabel(objective) }} + {{ objective.value }} + {{ formatIdentifier(objective.direction) }} +
  2. + } +
+
+
+

{{ tr().planning.finalMetrics }}

+
+
+
{{ tr().planning.unplacedAtRisk }}
+
{{ selected.finalMetrics.unplacedAtRisk }}
+
+
+
{{ tr().planning.holidayWeekValue }}
+
{{ selected.finalMetrics.holidayWeekValue }}
+
+
+
{{ tr().planning.daysGained }}
+
{{ selected.finalMetrics.nonWorkingDayGain }}
+
+
+
{{ tr().planning.longestWorkSequence }}
+
{{ selected.finalMetrics.longestWorkSequence }}
+
+
+
{{ tr().planning.shortSequenceDeficit }}
+
{{ selected.finalMetrics.shortSequenceDeficit }}
+
+
+
{{ tr().planning.supportingPlacements }}
+
{{ selected.finalMetrics.supportingPlacements }}
+
+
+
{{ tr().planning.preferenceSatisfaction }}
+
+ {{ + selected.finalMetrics.preferenceSatisfaction.join(', ') || + tr().planning.none + }} +
+
+
+
+
+ +
+ {{ tr().planning.removedRestoredComparison }} +

{{ tr().planning.comparisonHelp }}

    - @for ( - contribution of selected.contributions; - track contribution.kind + contribution.code - ) { + @for (delta of selected.comparison.delta; track delta.id) {
  • - {{ contribution.label }} - @if (contribution.value !== undefined) { - · {{ contribution.value }} - } - @if (contribution.dates?.length) { - · {{ contribution.dates!.join(', ') }} - } + {{ formatIdentifier(delta.id) }}: {{ delta.value }}
  • }
-
-
-

{{ tr().planning.objectiveVector }}

-
    - @for ( - objective of selected.objectiveVector.objectives; - track objective.id - ) { -
  1. - {{ objectiveLabel(objective) }} - {{ objective.value }} - {{ formatIdentifier(objective.direction) }} -
  2. - } -
-
-
-

{{ tr().planning.finalMetrics }}

-
-
-
{{ tr().planning.unplacedAtRisk }}
-
{{ selected.finalMetrics.unplacedAtRisk }}
-
-
-
{{ tr().planning.holidayWeekValue }}
-
{{ selected.finalMetrics.holidayWeekValue }}
-
-
-
{{ tr().planning.daysGained }}
-
{{ selected.finalMetrics.nonWorkingDayGain }}
-
-
-
{{ tr().planning.longestWorkSequence }}
-
{{ selected.finalMetrics.longestWorkSequence }}
-
-
-
{{ tr().planning.shortSequenceDeficit }}
-
{{ selected.finalMetrics.shortSequenceDeficit }}
-
-
-
{{ tr().planning.supportingPlacements }}
-
{{ selected.finalMetrics.supportingPlacements }}
-
-
-
{{ tr().planning.preferenceSatisfaction }}
-
- {{ - selected.finalMetrics.preferenceSatisfaction.join(', ') || - tr().planning.none - }} -
-
-
-
-
+ -
- {{ tr().planning.removedRestoredComparison }} -

{{ tr().planning.comparisonHelp }}

-
    - @for (delta of selected.comparison.delta; track delta.id) { -
  • - {{ formatIdentifier(delta.id) }}: {{ delta.value }} -
  • - } -
-
- -
-

{{ tr().planning.counterfactual }}

-

{{ tr().planning.hypothetical }}

- @if (counterfactual(); as analysis) { -

- {{ analysis.placement.date }} · - {{ analysis.placement.balanceId }} -

- } - @if (counterfactualLoading()) { -

{{ tr().planning.counterfactualLoading }}

- } @else if (counterfactualError()) { - - } @else if (selectedCounterfactualResult(); as result) { -
-
{{ result.date }}
-

- {{ - result.status === 'eligible' - ? tr().planning.eligible - : tr().planning.ineligible - }} +

+

{{ tr().planning.counterfactual }}

+

{{ tr().planning.hypothetical }}

+ @if (counterfactual(); as analysis) { +

+ {{ analysis.placement.date }} · + {{ analysis.placement.balanceId }}

- @if (result.historicalStatus) { -

- {{ tr().planning.historical }}: - {{ traceStatusLabel(result.historicalStatus) }} -

- } - @if (result.reason) { -

- {{ tr().planning.reason }}: - {{ result.reason.message }} -

- } - @if (result.comparison; as comparison) { -

- {{ tr().planning.firstDecisive }}: + } + @if (counterfactualLoading()) { +

{{ tr().planning.counterfactualLoading }}

+ } @else if (counterfactualError()) { + + } @else if (selectedCounterfactualResult(); as result) { +
+
{{ result.date }}
+

{{ - comparison.firstDecisiveObjective - ? formatIdentifier(comparison.firstDecisiveObjective) - : tr().planning.none + result.status === 'eligible' + ? tr().planning.eligible + : tr().planning.ineligible }}

-
    - @for (delta of comparison.delta; track delta.id) { -
  • - {{ formatIdentifier(delta.id) }}: - {{ delta.value }} -
  • - } -
-
- {{ tr().planning.fullComparison }} -
-
- {{ tr().planning.before }} -
    - @for ( - objective of comparison.before.objectives; - track objective.id - ) { -
  1. - {{ objectiveLabel(objective) }}: - {{ objective.value }} -
  2. - } -
+ @if (result.historicalStatus) { +

+ {{ tr().planning.historical }}: + {{ traceStatusLabel(result.historicalStatus) }} +

+ } + @if (result.reason) { +

+ {{ tr().planning.reason }}: + {{ result.reason.message }} +

+ } + @if (result.comparison; as comparison) { +

+ {{ tr().planning.firstDecisive }}: + {{ + comparison.firstDecisiveObjective + ? formatIdentifier(comparison.firstDecisiveObjective) + : tr().planning.none + }} +

+
    + @for (delta of comparison.delta; track delta.id) { +
  • + {{ formatIdentifier(delta.id) }}: + {{ delta.value }} +
  • + } +
+
+ {{ tr().planning.fullComparison }} +
+
+ {{ tr().planning.before }} +
    + @for ( + objective of comparison.before.objectives; + track objective.id + ) { +
  1. + {{ objectiveLabel(objective) }}: + {{ objective.value }} +
  2. + } +
+
+
+ {{ tr().planning.after }} +
    + @for ( + objective of comparison.after.objectives; + track objective.id + ) { +
  1. + {{ objectiveLabel(objective) }}: + {{ objective.value }} +
  2. + } +
+
-
- {{ tr().planning.after }} -
    - @for ( - objective of comparison.after.objectives; - track objective.id - ) { -
  1. - {{ objectiveLabel(objective) }}: - {{ objective.value }} -
  2. - } -
-
-
-
- } -
- } + + } +
+ } +
- + } @else if (explanationError()) {
  • - {{ tr().planning.currentPlacement }} -
  • -
  • - {{ tr().planning.eligible }} -
  • -
  • - {{ tr().planning.ineligible }} -
  • + @if (planDetailsOpen() || traceReplay()) { +
  • + {{ tr().planning.currentPlacement }} +
  • +
  • + {{ tr().planning.eligible }} +
  • +
  • + {{ tr().planning.ineligible }} +
  • + } diff --git a/src/app/components/calendar-year/calendar-year.scss b/src/app/components/calendar-year/calendar-year.scss index 824c8fe..d5548a1 100644 --- a/src/app/components/calendar-year/calendar-year.scss +++ b/src/app/components/calendar-year/calendar-year.scss @@ -160,6 +160,7 @@ .reason-list, .outcome-explanation, +.plan-details, .placement-explanation, .trace-explorer { margin: 0; @@ -169,6 +170,12 @@ background: var(--ui-card-bg, #fff); } +.plan-details .placement-explanation { + margin-block-start: 1rem; + padding: 0; + border: 0; +} + .outcome-explanation, .placement-explanation { display: grid; @@ -360,6 +367,7 @@ code { } .comparison-details summary, +.plan-details > summary, .trace-explorer > summary, .candidate-list summary { cursor: pointer; diff --git a/src/app/components/calendar-year/calendar-year.ts b/src/app/components/calendar-year/calendar-year.ts index 5978157..f07f02d 100644 --- a/src/app/components/calendar-year/calendar-year.ts +++ b/src/app/components/calendar-year/calendar-year.ts @@ -61,6 +61,7 @@ export class CalendarYear { protected readonly traceLoading = signal(false); protected readonly traceResolved = signal(false); protected readonly selectedPlacementIndex = signal(0); + protected readonly planDetailsOpen = signal(false); protected readonly counterfactual = signal(null); protected readonly selectedCounterfactualDate = signal(null); protected readonly counterfactualLoading = signal(false); @@ -146,7 +147,7 @@ export class CalendarYear { if (this.counterfactual()) return this.selectedCounterfactualDate() ?? this.counterfactual()!.placement.date; if (this.traceReplay()) return this.selectedTraceDecision()?.dates[0] ?? null; - return this.selectedExplanation()?.placement.date ?? null; + return this.planDetailsOpen() ? (this.selectedExplanation()?.placement.date ?? null) : null; }); protected readonly dateStatuses = computed>>(() => { @@ -356,6 +357,7 @@ export class CalendarYear { this.traceLoading.set(true); this.traceResolved.set(false); this.selectedPlacementIndex.set(0); + this.planDetailsOpen.set(false); this.clearCounterfactual(); this.tracePhaseIndex.set(0); this.traceDecisionIndex.set(0); @@ -370,8 +372,6 @@ export class CalendarYear { void calculation.explanation.then((explanation) => { if (version !== this.calculationVersion) return; this.planningExplanation.set(explanation ?? null); - if (explanation?.kind === 'PlanExplanation' && explanation.placements.length) - this.selectPlacement(0); }); void calculation.explanationError.then((reason) => { if (version === this.calculationVersion) this.explanationError.set(reason ?? null); @@ -433,6 +433,13 @@ export class CalendarYear { void this.analyzeSelection(explanation.placements[selected].placement); } + protected togglePlanDetails(event: Event): void { + const open = (event.currentTarget as HTMLDetailsElement).open; + this.planDetailsOpen.set(open); + if (open) this.selectPlacement(this.selectedPlacementIndex()); + else this.clearCounterfactual(); + } + protected stepPlacement(step: 'first' | 'previous' | 'next' | 'last'): void { const last = (this.planExplanation()?.placements.length ?? 1) - 1; const current = this.selectedPlacementIndex(); @@ -448,7 +455,7 @@ export class CalendarYear { } protected selectCalendarDate(date: string): void { - if (!this.traceReplay()) { + if (!this.traceReplay() && this.planDetailsOpen()) { const index = this.planExplanation()?.placements.findIndex( (explanation) => explanation.placement.date === date, );