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
+362 -316

No files matched your search

@@ -235,7 +235,7 @@ describe('CalendarYear planning outcome boundary', () => {
expect(summaryValues).toEqual(['2', '4', '6']); 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 = { const outcome: PlanningIncompleteOutcome = {
kind: 'PlanningIncomplete', kind: 'PlanningIncomplete',
incumbent: plan, incumbent: plan,
@@ -266,13 +266,17 @@ describe('CalendarYear planning outcome boundary', () => {
await new Promise<void>((resolve) => setTimeout(resolve, 0)); await new Promise<void>((resolve) => setTimeout(resolve, 0));
fixture.detectChanges(); 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( expect(fixture.nativeElement.querySelector('.incomplete-incumbent').textContent).toContain(
'holiday-week-value', 'holiday-week-value',
); );
expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(2); 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); const { calendar, calculation } = calendarFor(plan, explanation);
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CalendarYear], imports: [CalendarYear],
@@ -287,10 +291,18 @@ describe('CalendarYear planning outcome boundary', () => {
await new Promise<void>((resolve) => setTimeout(resolve, 0)); await new Promise<void>((resolve) => setTimeout(resolve, 0));
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.placement-explanation')).not.toBeNull(); const details = fixture.nativeElement.querySelector('.plan-details') as HTMLDetailsElement;
expect(fixture.nativeElement.textContent).toContain('Paid leave'); expect(details.open).toBe(false);
expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(2); 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(calculation.analyzeCounterfactual).toHaveBeenCalledWith(plan.placements[0]);
expect(fixture.nativeElement.textContent).toContain('Paid leave');
const next = fixture.nativeElement.querySelector( const next = fixture.nativeElement.querySelector(
'button[data-step="next"]', 'button[data-step="next"]',
@@ -300,6 +312,11 @@ describe('CalendarYear planning outcome boundary', () => {
expect(fixture.nativeElement.querySelector('button.current')?.dataset.date).toBe( expect(fixture.nativeElement.querySelector('button.current')?.dataset.date).toBe(
'2024-05-06', '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 () => { it('renders conflict causes and replays an atomic trace decision', async () => {
@@ -137,322 +137,333 @@
} }
@if (selectedExplanation(); as selected) { @if (selectedExplanation(); as selected) {
<section class="placement-explanation" aria-labelledby="placement-explanation-title"> <details class="plan-details" (toggle)="togglePlanDetails($event)">
<header class="explanation-heading"> <summary>{{ tr().planning.whyPlacement }}</summary>
<div> <section class="placement-explanation" aria-labelledby="placement-explanation-title">
<p class="eyebrow">{{ tr().planning.whyPlacement }}</p> <header class="explanation-heading">
<h3 id="placement-explanation-title"> <div>
{{ selected.placement.date }} · {{ balanceLabel(selected) }} <h3 id="placement-explanation-title">
</h3> {{ selected.placement.date }} · {{ balanceLabel(selected) }}
</div> </h3>
<nav </div>
class="placement-stepper" <nav
[attr.aria-label]="tr().planning.placementNavigation" class="placement-stepper"
> [attr.aria-label]="tr().planning.placementNavigation"
<button
type="button"
data-step="first"
[disabled]="selectedPlacementIndex() === 0"
[attr.aria-label]="tr().planning.first"
(click)="stepPlacement('first')"
> >
<span aria-hidden="true">⏮</span> <button
</button> type="button"
<button data-step="first"
type="button" [disabled]="selectedPlacementIndex() === 0"
data-step="previous" [attr.aria-label]="tr().planning.first"
[disabled]="selectedPlacementIndex() === 0" (click)="stepPlacement('first')"
[attr.aria-label]="tr().planning.previous" >
(click)="stepPlacement('previous')" <span aria-hidden="true">⏮</span>
> </button>
<span aria-hidden="true">◀</span> <button
</button> type="button"
<output> data-step="previous"
{{ tr().planning.placement }} {{ selectedPlacementIndex() + 1 }} [disabled]="selectedPlacementIndex() === 0"
{{ tr().planning.of }} {{ planExplanation()!.placements.length }} [attr.aria-label]="tr().planning.previous"
</output> (click)="stepPlacement('previous')"
<button >
type="button" <span aria-hidden="true">◀</span>
data-step="next" </button>
[disabled]=" <output>
selectedPlacementIndex() === planExplanation()!.placements.length - 1 {{ tr().planning.placement }} {{ selectedPlacementIndex() + 1 }}
" {{ tr().planning.of }} {{ planExplanation()!.placements.length }}
[attr.aria-label]="tr().planning.next" </output>
(click)="stepPlacement('next')" <button
> type="button"
<span aria-hidden="true">▶</span> data-step="next"
</button> [disabled]="
<button selectedPlacementIndex() ===
type="button" planExplanation()!.placements.length - 1
data-step="last" "
[disabled]=" [attr.aria-label]="tr().planning.next"
selectedPlacementIndex() === planExplanation()!.placements.length - 1 (click)="stepPlacement('next')"
" >
[attr.aria-label]="tr().planning.last" <span aria-hidden="true">▶</span>
(click)="stepPlacement('last')" </button>
> <button
<span aria-hidden="true">⏭</span> type="button"
</button> data-step="last"
</nav> [disabled]="
</header> selectedPlacementIndex() ===
@if (planExplanation()!.incomplete; as interrupted) { planExplanation()!.placements.length - 1
<div class="incomplete-incumbent" role="status"> "
<strong>{{ tr().planning.incumbentExplanation }}</strong> [attr.aria-label]="tr().planning.last"
<p>{{ interrupted.reason.message }}</p> (click)="stepPlacement('last')"
<p> >
{{ tr().planning.completedStages }}: <span aria-hidden="true">⏭</span>
{{ </button>
interrupted.completedStages.length </nav>
? interrupted.completedStages.join(', ') </header>
: tr().planning.none @if (planExplanation()!.incomplete; as interrupted) {
}} <div class="incomplete-incumbent" role="status">
</p> <strong>{{ tr().planning.incumbentExplanation }}</strong>
<p> <p>{{ interrupted.reason.message }}</p>
{{ tr().planning.uncompletedStages }}: <p>
{{ {{ tr().planning.completedStages }}:
interrupted.uncompletedStages.length {{
? interrupted.uncompletedStages.join(', ') interrupted.completedStages.length
: tr().planning.none ? interrupted.completedStages.join(', ')
}} : tr().planning.none
</p> }}
</div> </p>
} <p>
{{ tr().planning.uncompletedStages }}:
{{
interrupted.uncompletedStages.length
? interrupted.uncompletedStages.join(', ')
: tr().planning.none
}}
</p>
</div>
}
<dl class="explanation-facts compact-facts"> <dl class="explanation-facts compact-facts">
<div> <div>
<dt>{{ tr().planning.balance }}</dt> <dt>{{ tr().planning.balance }}</dt>
<dd> <dd>
{{ balanceLabel(selected) }} <code>{{ selected.placement.balanceId }}</code> {{ balanceLabel(selected) }}
@if (selected.balance) { <code>{{ selected.placement.balanceId }}</code>
<br />{{ planningDate(selected.balance.availableFrom) }} – @if (selected.balance) {
{{ planningDate(selected.balance.expiryDate) }} <br />{{ planningDate(selected.balance.availableFrom) }} –
} {{ planningDate(selected.balance.expiryDate) }}
</dd>
</div>
<div>
<dt>{{ tr().planning.rules }}</dt>
<dd>
@if (selected.ruleIds.length) {
@for (ruleId of selected.ruleIds; track ruleId; let last = $last) {
{{ ruleLabel(ruleId) }} <code>{{ ruleId }}</code
>{{ last ? '' : ', ' }}
} }
} @else { </dd>
{{ tr().planning.noRules }}
}
</dd>
</div>
<div>
<dt>{{ tr().planning.supporting }}</dt>
<dd>{{ selected.supporting ? tr().planning.yes : tr().planning.no }}</dd>
</div>
<div>
<dt>{{ tr().planning.remaining }}</dt>
<dd>
{{ selected.remainingQuantity }}
@if (selected.remainingBalance) {
<br /><small>
{{ formatIdentifier(selected.remainingBalance.classification) }} ·
{{ selected.remainingBalance.reason.message }}
</small>
}
</dd>
</div>
@if (selected.timeOffPeriod; as period) {
<div>
<dt>{{ tr().planning.timeOffPeriod }}</dt>
<dd>{{ period.start }} – {{ period.end }} ({{ period.duration }})</dd>
</div> </div>
}
@if (selected.firstDecisiveObjective; as decisive) {
<div> <div>
<dt>{{ tr().planning.firstDecisive }}</dt> <dt>{{ tr().planning.rules }}</dt>
<dd>{{ formatIdentifier(decisive) }}</dd> <dd>
@if (selected.ruleIds.length) {
@for (ruleId of selected.ruleIds; track ruleId; let last = $last) {
{{ ruleLabel(ruleId) }} <code>{{ ruleId }}</code
>{{ last ? '' : ', ' }}
}
} @else {
{{ tr().planning.noRules }}
}
</dd>
</div> </div>
} <div>
</dl> <dt>{{ tr().planning.supporting }}</dt>
<dd>{{ selected.supporting ? tr().planning.yes : tr().planning.no }}</dd>
</div>
<div>
<dt>{{ tr().planning.remaining }}</dt>
<dd>
{{ selected.remainingQuantity }}
@if (selected.remainingBalance) {
<br /><small>
{{
formatIdentifier(selected.remainingBalance.classification)
}}
·
{{ selected.remainingBalance.reason.message }}
</small>
}
</dd>
</div>
@if (selected.timeOffPeriod; as period) {
<div>
<dt>{{ tr().planning.timeOffPeriod }}</dt>
<dd>{{ period.start }} – {{ period.end }} ({{ period.duration }})</dd>
</div>
}
@if (selected.firstDecisiveObjective; as decisive) {
<div>
<dt>{{ tr().planning.firstDecisive }}</dt>
<dd>{{ formatIdentifier(decisive) }}</dd>
</div>
}
</dl>
<div class="explanation-columns"> <div class="explanation-columns">
<section> <section>
<h4>{{ tr().planning.contributions }}</h4> <h4>{{ tr().planning.contributions }}</h4>
<ul>
@for (
contribution of selected.contributions;
track contribution.kind + contribution.code
) {
<li>
<strong>{{ contribution.label }}</strong>
@if (contribution.value !== undefined) {
<span> · {{ contribution.value }}</span>
}
@if (contribution.dates?.length) {
<span> · {{ contribution.dates!.join(', ') }}</span>
}
</li>
}
</ul>
</section>
<section>
<h4>{{ tr().planning.objectiveVector }}</h4>
<ol class="objective-vector">
@for (
objective of selected.objectiveVector.objectives;
track objective.id
) {
<li>
<span>{{ objectiveLabel(objective) }}</span>
<strong>{{ objective.value }}</strong>
<small>{{ formatIdentifier(objective.direction) }}</small>
</li>
}
</ol>
</section>
<section>
<h4>{{ tr().planning.finalMetrics }}</h4>
<dl class="metric-list">
<div>
<dt>{{ tr().planning.unplacedAtRisk }}</dt>
<dd>{{ selected.finalMetrics.unplacedAtRisk }}</dd>
</div>
<div>
<dt>{{ tr().planning.holidayWeekValue }}</dt>
<dd>{{ selected.finalMetrics.holidayWeekValue }}</dd>
</div>
<div>
<dt>{{ tr().planning.daysGained }}</dt>
<dd>{{ selected.finalMetrics.nonWorkingDayGain }}</dd>
</div>
<div>
<dt>{{ tr().planning.longestWorkSequence }}</dt>
<dd>{{ selected.finalMetrics.longestWorkSequence }}</dd>
</div>
<div>
<dt>{{ tr().planning.shortSequenceDeficit }}</dt>
<dd>{{ selected.finalMetrics.shortSequenceDeficit }}</dd>
</div>
<div>
<dt>{{ tr().planning.supportingPlacements }}</dt>
<dd>{{ selected.finalMetrics.supportingPlacements }}</dd>
</div>
<div>
<dt>{{ tr().planning.preferenceSatisfaction }}</dt>
<dd>
{{
selected.finalMetrics.preferenceSatisfaction.join(', ') ||
tr().planning.none
}}
</dd>
</div>
</dl>
</section>
</div>
<details class="comparison-details">
<summary>{{ tr().planning.removedRestoredComparison }}</summary>
<p>{{ tr().planning.comparisonHelp }}</p>
<ul> <ul>
@for ( @for (delta of selected.comparison.delta; track delta.id) {
contribution of selected.contributions;
track contribution.kind + contribution.code
) {
<li> <li>
<strong>{{ contribution.label }}</strong> <strong>{{ formatIdentifier(delta.id) }}:</strong> {{ delta.value }}
@if (contribution.value !== undefined) {
<span> · {{ contribution.value }}</span>
}
@if (contribution.dates?.length) {
<span> · {{ contribution.dates!.join(', ') }}</span>
}
</li> </li>
} }
</ul> </ul>
</section> </details>
<section>
<h4>{{ tr().planning.objectiveVector }}</h4>
<ol class="objective-vector">
@for (
objective of selected.objectiveVector.objectives;
track objective.id
) {
<li>
<span>{{ objectiveLabel(objective) }}</span>
<strong>{{ objective.value }}</strong>
<small>{{ formatIdentifier(objective.direction) }}</small>
</li>
}
</ol>
</section>
<section>
<h4>{{ tr().planning.finalMetrics }}</h4>
<dl class="metric-list">
<div>
<dt>{{ tr().planning.unplacedAtRisk }}</dt>
<dd>{{ selected.finalMetrics.unplacedAtRisk }}</dd>
</div>
<div>
<dt>{{ tr().planning.holidayWeekValue }}</dt>
<dd>{{ selected.finalMetrics.holidayWeekValue }}</dd>
</div>
<div>
<dt>{{ tr().planning.daysGained }}</dt>
<dd>{{ selected.finalMetrics.nonWorkingDayGain }}</dd>
</div>
<div>
<dt>{{ tr().planning.longestWorkSequence }}</dt>
<dd>{{ selected.finalMetrics.longestWorkSequence }}</dd>
</div>
<div>
<dt>{{ tr().planning.shortSequenceDeficit }}</dt>
<dd>{{ selected.finalMetrics.shortSequenceDeficit }}</dd>
</div>
<div>
<dt>{{ tr().planning.supportingPlacements }}</dt>
<dd>{{ selected.finalMetrics.supportingPlacements }}</dd>
</div>
<div>
<dt>{{ tr().planning.preferenceSatisfaction }}</dt>
<dd>
{{
selected.finalMetrics.preferenceSatisfaction.join(', ') ||
tr().planning.none
}}
</dd>
</div>
</dl>
</section>
</div>
<details class="comparison-details"> <section class="counterfactual-panel" aria-labelledby="counterfactual-title">
<summary>{{ tr().planning.removedRestoredComparison }}</summary> <h4 id="counterfactual-title">{{ tr().planning.counterfactual }}</h4>
<p>{{ tr().planning.comparisonHelp }}</p> <p>{{ tr().planning.hypothetical }}</p>
<ul> @if (counterfactual(); as analysis) {
@for (delta of selected.comparison.delta; track delta.id) { <p>
<li> <strong
<strong>{{ formatIdentifier(delta.id) }}:</strong> {{ delta.value }} >{{ analysis.placement.date }} ·
</li> {{ analysis.placement.balanceId }}</strong
} >
</ul>
</details>
<section class="counterfactual-panel" aria-labelledby="counterfactual-title">
<h4 id="counterfactual-title">{{ tr().planning.counterfactual }}</h4>
<p>{{ tr().planning.hypothetical }}</p>
@if (counterfactual(); as analysis) {
<p>
<strong
>{{ analysis.placement.date }} ·
{{ analysis.placement.balanceId }}</strong
>
</p>
}
@if (counterfactualLoading()) {
<p role="status">{{ tr().planning.counterfactualLoading }}</p>
} @else if (counterfactualError()) {
<p class="inline-error" role="alert">
{{ tr().planning.counterfactualError }} {{ counterfactualError() }}
</p>
} @else if (selectedCounterfactualResult(); as result) {
<div class="counterfactual-result">
<h5>{{ result.date }}</h5>
<p class="status-badge" [class.ineligible]="result.status === 'ineligible'">
{{
result.status === 'eligible'
? tr().planning.eligible
: tr().planning.ineligible
}}
</p> </p>
@if (result.historicalStatus) { }
<p> @if (counterfactualLoading()) {
<strong>{{ tr().planning.historical }}:</strong> <p role="status">{{ tr().planning.counterfactualLoading }}</p>
{{ traceStatusLabel(result.historicalStatus) }} } @else if (counterfactualError()) {
</p> <p class="inline-error" role="alert">
} {{ tr().planning.counterfactualError }} {{ counterfactualError() }}
@if (result.reason) { </p>
<p> } @else if (selectedCounterfactualResult(); as result) {
<strong>{{ tr().planning.reason }}:</strong> <div class="counterfactual-result">
{{ result.reason.message }} <h5>{{ result.date }}</h5>
</p> <p
} class="status-badge"
@if (result.comparison; as comparison) { [class.ineligible]="result.status === 'ineligible'"
<p> >
<strong>{{ tr().planning.firstDecisive }}:</strong>
{{ {{
comparison.firstDecisiveObjective result.status === 'eligible'
? formatIdentifier(comparison.firstDecisiveObjective) ? tr().planning.eligible
: tr().planning.none : tr().planning.ineligible
}} }}
</p> </p>
<ul> @if (result.historicalStatus) {
@for (delta of comparison.delta; track delta.id) { <p>
<li> <strong>{{ tr().planning.historical }}:</strong>
<strong>{{ formatIdentifier(delta.id) }}:</strong> {{ traceStatusLabel(result.historicalStatus) }}
{{ delta.value }} </p>
</li> }
} @if (result.reason) {
</ul> <p>
<details> <strong>{{ tr().planning.reason }}:</strong>
<summary>{{ tr().planning.fullComparison }}</summary> {{ result.reason.message }}
<div class="comparison-vectors"> </p>
<div> }
<strong>{{ tr().planning.before }}</strong> @if (result.comparison; as comparison) {
<ol> <p>
@for ( <strong>{{ tr().planning.firstDecisive }}:</strong>
objective of comparison.before.objectives; {{
track objective.id comparison.firstDecisiveObjective
) { ? formatIdentifier(comparison.firstDecisiveObjective)
<li> : tr().planning.none
{{ objectiveLabel(objective) }}: }}
{{ objective.value }} </p>
</li> <ul>
} @for (delta of comparison.delta; track delta.id) {
</ol> <li>
<strong>{{ formatIdentifier(delta.id) }}:</strong>
{{ delta.value }}
</li>
}
</ul>
<details>
<summary>{{ tr().planning.fullComparison }}</summary>
<div class="comparison-vectors">
<div>
<strong>{{ tr().planning.before }}</strong>
<ol>
@for (
objective of comparison.before.objectives;
track objective.id
) {
<li>
{{ objectiveLabel(objective) }}:
{{ objective.value }}
</li>
}
</ol>
</div>
<div>
<strong>{{ tr().planning.after }}</strong>
<ol>
@for (
objective of comparison.after.objectives;
track objective.id
) {
<li>
{{ objectiveLabel(objective) }}:
{{ objective.value }}
</li>
}
</ol>
</div>
</div> </div>
<div> </details>
<strong>{{ tr().planning.after }}</strong> }
<ol> </div>
@for ( }
objective of comparison.after.objectives; </section>
track objective.id
) {
<li>
{{ objectiveLabel(objective) }}:
{{ objective.value }}
</li>
}
</ol>
</div>
</div>
</details>
}
</div>
}
</section> </section>
</section> </details>
} @else if (explanationError()) { } @else if (explanationError()) {
<p class="inline-error" role="alert"> <p class="inline-error" role="alert">
{{ tr().planning.explanationError }} {{ explanationError()!.message }} {{ tr().planning.explanationError }} {{ explanationError()!.message }}
@@ -482,17 +493,20 @@
<span class="legend-swatch today" aria-hidden="true"></span <span class="legend-swatch today" aria-hidden="true"></span
>{{ tr().dayTypes.today }} >{{ tr().dayTypes.today }}
</li> </li>
<li> @if (planDetailsOpen() || traceReplay()) {
<span class="legend-symbol" aria-hidden="true">◎</span <li>
>{{ tr().planning.currentPlacement }} <span class="legend-symbol" aria-hidden="true">◎</span
</li> >{{ tr().planning.currentPlacement }}
<li> </li>
<span class="legend-symbol" aria-hidden="true">✓</span>{{ tr().planning.eligible }} <li>
</li> <span class="legend-symbol" aria-hidden="true">✓</span
<li> >{{ tr().planning.eligible }}
<span class="legend-symbol" aria-hidden="true">×</span </li>
>{{ tr().planning.ineligible }} <li>
</li> <span class="legend-symbol" aria-hidden="true">×</span
>{{ tr().planning.ineligible }}
</li>
}
</ul> </ul>
</div> </div>
@@ -160,6 +160,7 @@
.reason-list, .reason-list,
.outcome-explanation, .outcome-explanation,
.plan-details,
.placement-explanation, .placement-explanation,
.trace-explorer { .trace-explorer {
margin: 0; margin: 0;
@@ -169,6 +170,12 @@
background: var(--ui-card-bg, #fff); background: var(--ui-card-bg, #fff);
} }
.plan-details .placement-explanation {
margin-block-start: 1rem;
padding: 0;
border: 0;
}
.outcome-explanation, .outcome-explanation,
.placement-explanation { .placement-explanation {
display: grid; display: grid;
@@ -360,6 +367,7 @@ code {
} }
.comparison-details summary, .comparison-details summary,
.plan-details > summary,
.trace-explorer > summary, .trace-explorer > summary,
.candidate-list summary { .candidate-list summary {
cursor: pointer; cursor: pointer;
@@ -61,6 +61,7 @@ export class CalendarYear {
protected readonly traceLoading = signal(false); protected readonly traceLoading = signal(false);
protected readonly traceResolved = signal(false); protected readonly traceResolved = signal(false);
protected readonly selectedPlacementIndex = signal(0); protected readonly selectedPlacementIndex = signal(0);
protected readonly planDetailsOpen = signal(false);
protected readonly counterfactual = signal<CounterfactualAnalysis | null>(null); protected readonly counterfactual = signal<CounterfactualAnalysis | null>(null);
protected readonly selectedCounterfactualDate = signal<string | null>(null); protected readonly selectedCounterfactualDate = signal<string | null>(null);
protected readonly counterfactualLoading = signal(false); protected readonly counterfactualLoading = signal(false);
@@ -146,7 +147,7 @@ export class CalendarYear {
if (this.counterfactual()) if (this.counterfactual())
return this.selectedCounterfactualDate() ?? this.counterfactual()!.placement.date; return this.selectedCounterfactualDate() ?? this.counterfactual()!.placement.date;
if (this.traceReplay()) return this.selectedTraceDecision()?.dates[0] ?? null; 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>>>(() => { protected readonly dateStatuses = computed<Readonly<Record<string, CalendarDayStatus>>>(() => {
@@ -356,6 +357,7 @@ export class CalendarYear {
this.traceLoading.set(true); this.traceLoading.set(true);
this.traceResolved.set(false); this.traceResolved.set(false);
this.selectedPlacementIndex.set(0); this.selectedPlacementIndex.set(0);
this.planDetailsOpen.set(false);
this.clearCounterfactual(); this.clearCounterfactual();
this.tracePhaseIndex.set(0); this.tracePhaseIndex.set(0);
this.traceDecisionIndex.set(0); this.traceDecisionIndex.set(0);
@@ -370,8 +372,6 @@ export class CalendarYear {
void calculation.explanation.then((explanation) => { void calculation.explanation.then((explanation) => {
if (version !== this.calculationVersion) return; if (version !== this.calculationVersion) return;
this.planningExplanation.set(explanation ?? null); this.planningExplanation.set(explanation ?? null);
if (explanation?.kind === 'PlanExplanation' && explanation.placements.length)
this.selectPlacement(0);
}); });
void calculation.explanationError.then((reason) => { void calculation.explanationError.then((reason) => {
if (version === this.calculationVersion) this.explanationError.set(reason ?? null); if (version === this.calculationVersion) this.explanationError.set(reason ?? null);
@@ -433,6 +433,13 @@ export class CalendarYear {
void this.analyzeSelection(explanation.placements[selected].placement); 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 { protected stepPlacement(step: 'first' | 'previous' | 'next' | 'last'): void {
const last = (this.planExplanation()?.placements.length ?? 1) - 1; const last = (this.planExplanation()?.placements.length ?? 1) - 1;
const current = this.selectedPlacementIndex(); const current = this.selectedPlacementIndex();
@@ -448,7 +455,7 @@ export class CalendarYear {
} }
protected selectCalendarDate(date: string): void { protected selectCalendarDate(date: string): void {
if (!this.traceReplay()) { if (!this.traceReplay() && this.planDetailsOpen()) {
const index = this.planExplanation()?.placements.findIndex( const index = this.planExplanation()?.placements.findIndex(
(explanation) => explanation.placement.date === date, (explanation) => explanation.placement.date === date,
); );