fix(ui): hide planning details by default
This commit is contained in:
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']);
|
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,10 +137,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (selectedExplanation(); as selected) {
|
@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">
|
<section class="placement-explanation" aria-labelledby="placement-explanation-title">
|
||||||
<header class="explanation-heading">
|
<header class="explanation-heading">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">{{ tr().planning.whyPlacement }}</p>
|
|
||||||
<h3 id="placement-explanation-title">
|
<h3 id="placement-explanation-title">
|
||||||
{{ selected.placement.date }} · {{ balanceLabel(selected) }}
|
{{ selected.placement.date }} · {{ balanceLabel(selected) }}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -175,7 +176,8 @@
|
|||||||
type="button"
|
type="button"
|
||||||
data-step="next"
|
data-step="next"
|
||||||
[disabled]="
|
[disabled]="
|
||||||
selectedPlacementIndex() === planExplanation()!.placements.length - 1
|
selectedPlacementIndex() ===
|
||||||
|
planExplanation()!.placements.length - 1
|
||||||
"
|
"
|
||||||
[attr.aria-label]="tr().planning.next"
|
[attr.aria-label]="tr().planning.next"
|
||||||
(click)="stepPlacement('next')"
|
(click)="stepPlacement('next')"
|
||||||
@@ -186,7 +188,8 @@
|
|||||||
type="button"
|
type="button"
|
||||||
data-step="last"
|
data-step="last"
|
||||||
[disabled]="
|
[disabled]="
|
||||||
selectedPlacementIndex() === planExplanation()!.placements.length - 1
|
selectedPlacementIndex() ===
|
||||||
|
planExplanation()!.placements.length - 1
|
||||||
"
|
"
|
||||||
[attr.aria-label]="tr().planning.last"
|
[attr.aria-label]="tr().planning.last"
|
||||||
(click)="stepPlacement('last')"
|
(click)="stepPlacement('last')"
|
||||||
@@ -222,7 +225,8 @@
|
|||||||
<div>
|
<div>
|
||||||
<dt>{{ tr().planning.balance }}</dt>
|
<dt>{{ tr().planning.balance }}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{{ balanceLabel(selected) }} <code>{{ selected.placement.balanceId }}</code>
|
{{ balanceLabel(selected) }}
|
||||||
|
<code>{{ selected.placement.balanceId }}</code>
|
||||||
@if (selected.balance) {
|
@if (selected.balance) {
|
||||||
<br />{{ planningDate(selected.balance.availableFrom) }} –
|
<br />{{ planningDate(selected.balance.availableFrom) }} –
|
||||||
{{ planningDate(selected.balance.expiryDate) }}
|
{{ planningDate(selected.balance.expiryDate) }}
|
||||||
@@ -252,7 +256,10 @@
|
|||||||
{{ selected.remainingQuantity }}
|
{{ selected.remainingQuantity }}
|
||||||
@if (selected.remainingBalance) {
|
@if (selected.remainingBalance) {
|
||||||
<br /><small>
|
<br /><small>
|
||||||
{{ formatIdentifier(selected.remainingBalance.classification) }} ·
|
{{
|
||||||
|
formatIdentifier(selected.remainingBalance.classification)
|
||||||
|
}}
|
||||||
|
·
|
||||||
{{ selected.remainingBalance.reason.message }}
|
{{ selected.remainingBalance.reason.message }}
|
||||||
</small>
|
</small>
|
||||||
}
|
}
|
||||||
@@ -379,7 +386,10 @@
|
|||||||
} @else if (selectedCounterfactualResult(); as result) {
|
} @else if (selectedCounterfactualResult(); as result) {
|
||||||
<div class="counterfactual-result">
|
<div class="counterfactual-result">
|
||||||
<h5>{{ result.date }}</h5>
|
<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'
|
result.status === 'eligible'
|
||||||
? tr().planning.eligible
|
? tr().planning.eligible
|
||||||
@@ -453,6 +463,7 @@
|
|||||||
}
|
}
|
||||||
</section>
|
</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>
|
||||||
|
@if (planDetailsOpen() || traceReplay()) {
|
||||||
<li>
|
<li>
|
||||||
<span class="legend-symbol" aria-hidden="true">◎</span
|
<span class="legend-symbol" aria-hidden="true">◎</span
|
||||||
>{{ tr().planning.currentPlacement }}
|
>{{ tr().planning.currentPlacement }}
|
||||||
</li>
|
</li>
|
||||||
<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>
|
||||||
<li>
|
<li>
|
||||||
<span class="legend-symbol" aria-hidden="true">×</span
|
<span class="legend-symbol" aria-hidden="true">×</span
|
||||||
>{{ tr().planning.ineligible }}
|
>{{ tr().planning.ineligible }}
|
||||||
</li>
|
</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,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user