fix(ui): hide planning details by default

This commit is contained in:
nessar committed 2026-08-31 00:23:09 +02:00
1 parent d7a13b0b1e
commit fb0c86da3f
4 files changed
+61 -15

No files matched your search

@@ -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<void>((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<void>((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 () => {
@@ -137,10 +137,11 @@
}
@if (selectedExplanation(); as selected) {
<details class="plan-details" (toggle)="togglePlanDetails($event)">
<summary>{{ tr().planning.whyPlacement }}</summary>
<section class="placement-explanation" aria-labelledby="placement-explanation-title">
<header class="explanation-heading">
<div>
<p class="eyebrow">{{ tr().planning.whyPlacement }}</p>
<h3 id="placement-explanation-title">
{{ selected.placement.date }} · {{ balanceLabel(selected) }}
</h3>
@@ -175,7 +176,8 @@
type="button"
data-step="next"
[disabled]="
selectedPlacementIndex() === planExplanation()!.placements.length - 1
selectedPlacementIndex() ===
planExplanation()!.placements.length - 1
"
[attr.aria-label]="tr().planning.next"
(click)="stepPlacement('next')"
@@ -186,7 +188,8 @@
type="button"
data-step="last"
[disabled]="
selectedPlacementIndex() === planExplanation()!.placements.length - 1
selectedPlacementIndex() ===
planExplanation()!.placements.length - 1
"
[attr.aria-label]="tr().planning.last"
(click)="stepPlacement('last')"
@@ -222,7 +225,8 @@
<div>
<dt>{{ tr().planning.balance }}</dt>
<dd>
{{ balanceLabel(selected) }} <code>{{ selected.placement.balanceId }}</code>
{{ balanceLabel(selected) }}
<code>{{ selected.placement.balanceId }}</code>
@if (selected.balance) {
<br />{{ planningDate(selected.balance.availableFrom) }} –
{{ planningDate(selected.balance.expiryDate) }}
@@ -252,7 +256,10 @@
{{ selected.remainingQuantity }}
@if (selected.remainingBalance) {
<br /><small>
{{ formatIdentifier(selected.remainingBalance.classification) }} ·
{{
formatIdentifier(selected.remainingBalance.classification)
}}
·
{{ selected.remainingBalance.reason.message }}
</small>
}
@@ -379,7 +386,10 @@
} @else if (selectedCounterfactualResult(); as result) {
<div class="counterfactual-result">
<h5>{{ result.date }}</h5>
<p class="status-badge" [class.ineligible]="result.status === 'ineligible'">
<p
class="status-badge"
[class.ineligible]="result.status === 'ineligible'"
>
{{
result.status === 'eligible'
? tr().planning.eligible
@@ -453,6 +463,7 @@
}
</section>
</section>
</details>
} @else if (explanationError()) {
<p class="inline-error" role="alert">
{{ tr().planning.explanationError }} {{ explanationError()!.message }}
@@ -482,17 +493,20 @@
<span class="legend-swatch today" aria-hidden="true"></span
>{{ tr().dayTypes.today }}
</li>
@if (planDetailsOpen() || traceReplay()) {
<li>
<span class="legend-symbol" aria-hidden="true">◎</span
>{{ tr().planning.currentPlacement }}
</li>
<li>
<span class="legend-symbol" aria-hidden="true">✓</span>{{ tr().planning.eligible }}
<span class="legend-symbol" aria-hidden="true">✓</span
>{{ tr().planning.eligible }}
</li>
<li>
<span class="legend-symbol" aria-hidden="true">×</span
>{{ tr().planning.ineligible }}
</li>
}
</ul>
</div>
@@ -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;
@@ -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<CounterfactualAnalysis | null>(null);
protected readonly selectedCounterfactualDate = signal<string | null>(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<Readonly<Record<string, CalendarDayStatus>>>(() => {
@@ -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,
);