fix(ui): hide planning details by default
This commit is contained in:
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']);
|
||||
});
|
||||
|
||||
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,322 +137,333 @@
|
||||
}
|
||||
|
||||
@if (selectedExplanation(); as selected) {
|
||||
<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>
|
||||
</div>
|
||||
<nav
|
||||
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')"
|
||||
<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>
|
||||
<h3 id="placement-explanation-title">
|
||||
{{ selected.placement.date }} · {{ balanceLabel(selected) }}
|
||||
</h3>
|
||||
</div>
|
||||
<nav
|
||||
class="placement-stepper"
|
||||
[attr.aria-label]="tr().planning.placementNavigation"
|
||||
>
|
||||
<span aria-hidden="true">⏮</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-step="previous"
|
||||
[disabled]="selectedPlacementIndex() === 0"
|
||||
[attr.aria-label]="tr().planning.previous"
|
||||
(click)="stepPlacement('previous')"
|
||||
>
|
||||
<span aria-hidden="true">◀</span>
|
||||
</button>
|
||||
<output>
|
||||
{{ tr().planning.placement }} {{ selectedPlacementIndex() + 1 }}
|
||||
{{ tr().planning.of }} {{ planExplanation()!.placements.length }}
|
||||
</output>
|
||||
<button
|
||||
type="button"
|
||||
data-step="next"
|
||||
[disabled]="
|
||||
selectedPlacementIndex() === planExplanation()!.placements.length - 1
|
||||
"
|
||||
[attr.aria-label]="tr().planning.next"
|
||||
(click)="stepPlacement('next')"
|
||||
>
|
||||
<span aria-hidden="true">▶</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-step="last"
|
||||
[disabled]="
|
||||
selectedPlacementIndex() === planExplanation()!.placements.length - 1
|
||||
"
|
||||
[attr.aria-label]="tr().planning.last"
|
||||
(click)="stepPlacement('last')"
|
||||
>
|
||||
<span aria-hidden="true">⏭</span>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
@if (planExplanation()!.incomplete; as interrupted) {
|
||||
<div class="incomplete-incumbent" role="status">
|
||||
<strong>{{ tr().planning.incumbentExplanation }}</strong>
|
||||
<p>{{ interrupted.reason.message }}</p>
|
||||
<p>
|
||||
{{ tr().planning.completedStages }}:
|
||||
{{
|
||||
interrupted.completedStages.length
|
||||
? interrupted.completedStages.join(', ')
|
||||
: tr().planning.none
|
||||
}}
|
||||
</p>
|
||||
<p>
|
||||
{{ tr().planning.uncompletedStages }}:
|
||||
{{
|
||||
interrupted.uncompletedStages.length
|
||||
? interrupted.uncompletedStages.join(', ')
|
||||
: tr().planning.none
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
<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"
|
||||
data-step="previous"
|
||||
[disabled]="selectedPlacementIndex() === 0"
|
||||
[attr.aria-label]="tr().planning.previous"
|
||||
(click)="stepPlacement('previous')"
|
||||
>
|
||||
<span aria-hidden="true">◀</span>
|
||||
</button>
|
||||
<output>
|
||||
{{ tr().planning.placement }} {{ selectedPlacementIndex() + 1 }}
|
||||
{{ tr().planning.of }} {{ planExplanation()!.placements.length }}
|
||||
</output>
|
||||
<button
|
||||
type="button"
|
||||
data-step="next"
|
||||
[disabled]="
|
||||
selectedPlacementIndex() ===
|
||||
planExplanation()!.placements.length - 1
|
||||
"
|
||||
[attr.aria-label]="tr().planning.next"
|
||||
(click)="stepPlacement('next')"
|
||||
>
|
||||
<span aria-hidden="true">▶</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-step="last"
|
||||
[disabled]="
|
||||
selectedPlacementIndex() ===
|
||||
planExplanation()!.placements.length - 1
|
||||
"
|
||||
[attr.aria-label]="tr().planning.last"
|
||||
(click)="stepPlacement('last')"
|
||||
>
|
||||
<span aria-hidden="true">⏭</span>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
@if (planExplanation()!.incomplete; as interrupted) {
|
||||
<div class="incomplete-incumbent" role="status">
|
||||
<strong>{{ tr().planning.incumbentExplanation }}</strong>
|
||||
<p>{{ interrupted.reason.message }}</p>
|
||||
<p>
|
||||
{{ tr().planning.completedStages }}:
|
||||
{{
|
||||
interrupted.completedStages.length
|
||||
? interrupted.completedStages.join(', ')
|
||||
: tr().planning.none
|
||||
}}
|
||||
</p>
|
||||
<p>
|
||||
{{ tr().planning.uncompletedStages }}:
|
||||
{{
|
||||
interrupted.uncompletedStages.length
|
||||
? interrupted.uncompletedStages.join(', ')
|
||||
: tr().planning.none
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<dl class="explanation-facts compact-facts">
|
||||
<div>
|
||||
<dt>{{ tr().planning.balance }}</dt>
|
||||
<dd>
|
||||
{{ balanceLabel(selected) }} <code>{{ selected.placement.balanceId }}</code>
|
||||
@if (selected.balance) {
|
||||
<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 ? '' : ', ' }}
|
||||
<dl class="explanation-facts compact-facts">
|
||||
<div>
|
||||
<dt>{{ tr().planning.balance }}</dt>
|
||||
<dd>
|
||||
{{ balanceLabel(selected) }}
|
||||
<code>{{ selected.placement.balanceId }}</code>
|
||||
@if (selected.balance) {
|
||||
<br />{{ planningDate(selected.balance.availableFrom) }} –
|
||||
{{ planningDate(selected.balance.expiryDate) }}
|
||||
}
|
||||
} @else {
|
||||
{{ 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>
|
||||
</dd>
|
||||
</div>
|
||||
}
|
||||
@if (selected.firstDecisiveObjective; as decisive) {
|
||||
<div>
|
||||
<dt>{{ tr().planning.firstDecisive }}</dt>
|
||||
<dd>{{ formatIdentifier(decisive) }}</dd>
|
||||
<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 {
|
||||
{{ tr().planning.noRules }}
|
||||
}
|
||||
</dd>
|
||||
</div>
|
||||
}
|
||||
</dl>
|
||||
<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>
|
||||
}
|
||||
@if (selected.firstDecisiveObjective; as decisive) {
|
||||
<div>
|
||||
<dt>{{ tr().planning.firstDecisive }}</dt>
|
||||
<dd>{{ formatIdentifier(decisive) }}</dd>
|
||||
</div>
|
||||
}
|
||||
</dl>
|
||||
|
||||
<div class="explanation-columns">
|
||||
<section>
|
||||
<h4>{{ tr().planning.contributions }}</h4>
|
||||
<div class="explanation-columns">
|
||||
<section>
|
||||
<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>
|
||||
@for (
|
||||
contribution of selected.contributions;
|
||||
track contribution.kind + contribution.code
|
||||
) {
|
||||
@for (delta of selected.comparison.delta; track delta.id) {
|
||||
<li>
|
||||
<strong>{{ contribution.label }}</strong>
|
||||
@if (contribution.value !== undefined) {
|
||||
<span> · {{ contribution.value }}</span>
|
||||
}
|
||||
@if (contribution.dates?.length) {
|
||||
<span> · {{ contribution.dates!.join(', ') }}</span>
|
||||
}
|
||||
<strong>{{ formatIdentifier(delta.id) }}:</strong> {{ delta.value }}
|
||||
</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>
|
||||
|
||||
<details class="comparison-details">
|
||||
<summary>{{ tr().planning.removedRestoredComparison }}</summary>
|
||||
<p>{{ tr().planning.comparisonHelp }}</p>
|
||||
<ul>
|
||||
@for (delta of selected.comparison.delta; track delta.id) {
|
||||
<li>
|
||||
<strong>{{ formatIdentifier(delta.id) }}:</strong> {{ delta.value }}
|
||||
</li>
|
||||
}
|
||||
</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
|
||||
}}
|
||||
<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 (result.historicalStatus) {
|
||||
<p>
|
||||
<strong>{{ tr().planning.historical }}:</strong>
|
||||
{{ traceStatusLabel(result.historicalStatus) }}
|
||||
</p>
|
||||
}
|
||||
@if (result.reason) {
|
||||
<p>
|
||||
<strong>{{ tr().planning.reason }}:</strong>
|
||||
{{ result.reason.message }}
|
||||
</p>
|
||||
}
|
||||
@if (result.comparison; as comparison) {
|
||||
<p>
|
||||
<strong>{{ tr().planning.firstDecisive }}:</strong>
|
||||
}
|
||||
@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'"
|
||||
>
|
||||
{{
|
||||
comparison.firstDecisiveObjective
|
||||
? formatIdentifier(comparison.firstDecisiveObjective)
|
||||
: tr().planning.none
|
||||
result.status === 'eligible'
|
||||
? tr().planning.eligible
|
||||
: tr().planning.ineligible
|
||||
}}
|
||||
</p>
|
||||
<ul>
|
||||
@for (delta of comparison.delta; track delta.id) {
|
||||
<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>
|
||||
@if (result.historicalStatus) {
|
||||
<p>
|
||||
<strong>{{ tr().planning.historical }}:</strong>
|
||||
{{ traceStatusLabel(result.historicalStatus) }}
|
||||
</p>
|
||||
}
|
||||
@if (result.reason) {
|
||||
<p>
|
||||
<strong>{{ tr().planning.reason }}:</strong>
|
||||
{{ result.reason.message }}
|
||||
</p>
|
||||
}
|
||||
@if (result.comparison; as comparison) {
|
||||
<p>
|
||||
<strong>{{ tr().planning.firstDecisive }}:</strong>
|
||||
{{
|
||||
comparison.firstDecisiveObjective
|
||||
? formatIdentifier(comparison.firstDecisiveObjective)
|
||||
: tr().planning.none
|
||||
}}
|
||||
</p>
|
||||
<ul>
|
||||
@for (delta of comparison.delta; track delta.id) {
|
||||
<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>
|
||||
<strong>{{ tr().planning.after }}</strong>
|
||||
<ol>
|
||||
@for (
|
||||
objective of comparison.after.objectives;
|
||||
track objective.id
|
||||
) {
|
||||
<li>
|
||||
{{ objectiveLabel(objective) }}:
|
||||
{{ objective.value }}
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
</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>
|
||||
<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 }}
|
||||
</li>
|
||||
<li>
|
||||
<span class="legend-symbol" aria-hidden="true">×</span
|
||||
>{{ tr().planning.ineligible }}
|
||||
</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 }}
|
||||
</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,
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user