- Refactor leave-planner.worker.ts to support cancellation of ongoing calculations. - Introduce `calculatePlanningAsyncInternal` for asynchronous planning calculations. - Add cancellation handling in the worker message event. - Update planning-date.ts with new utility functions for date manipulation. - Extend translation-service.ts and translation files (en.json, de.json, es.json, fr.json) to include new strings related to planning horizon and cancellation. - Create documentation for the cancellation feature in planner-cancellation.md. - Add unit tests for cancellation functionality in leave-planner-cancellation.spec.ts and leave-planner.worker.spec.ts. #57 #58
1377 lines
42 KiB
HTML
1377 lines
42 KiB
HTML
<div class="calendar-year">
|
||
<p class="sr-only" role="status" aria-live="polite" aria-atomic="true">
|
||
{{ liveMessage() }}
|
||
</p>
|
||
|
||
@if (isLoading()) {
|
||
<div class="loading-overlay">
|
||
<div class="loading-spinner">
|
||
<div class="spinner-container">
|
||
<mat-spinner
|
||
[diameter]="72"
|
||
[mode]="'determinate'"
|
||
[value]="loadingProgress()"
|
||
></mat-spinner>
|
||
<div class="spinner-text">{{ loadingProgress() }}%</div>
|
||
</div>
|
||
<p role="status">{{ isCancelling() ? tr().planning.cancelling : tr().loading }}</p>
|
||
<button
|
||
mat-stroked-button
|
||
type="button"
|
||
class="cancel-calculation"
|
||
[disabled]="isCancelling()"
|
||
(click)="cancelCalculation()"
|
||
>
|
||
{{ tr().planning.cancel }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<header class="results-header">
|
||
<div class="results-heading">
|
||
<h2 #resultsTitle id="planning-results-title" tabindex="-1">
|
||
{{ tr().planning.title }}
|
||
</h2>
|
||
@if (planningOutcome()?.kind === 'InvalidInput') {
|
||
<p class="planning-status error" role="alert">
|
||
<mat-icon aria-hidden="true">error</mat-icon>
|
||
{{ tr().planning.invalidInput }}
|
||
</p>
|
||
} @else if (planningOutcome()?.kind === 'PlanningConflict') {
|
||
<p class="planning-status error" role="alert">
|
||
<mat-icon aria-hidden="true">error</mat-icon>
|
||
{{ tr().planning.conflict }}
|
||
</p>
|
||
} @else if (planningOutcome()?.kind === 'PlanningIncomplete') {
|
||
<p
|
||
class="planning-status"
|
||
[class.error]="hasPlannerError()"
|
||
[class.warning]="!hasPlannerError()"
|
||
[attr.role]="hasPlannerError() ? 'alert' : 'status'"
|
||
>
|
||
<mat-icon aria-hidden="true">{{
|
||
hasPlannerError() ? 'error' : 'warning'
|
||
}}</mat-icon>
|
||
{{ outcomeLabel(planningOutcome()!) }}
|
||
</p>
|
||
} @else if (planningOutcome()?.kind === 'Plan') {
|
||
<p class="planning-status success" role="status">
|
||
<mat-icon aria-hidden="true">check_circle</mat-icon>
|
||
{{ tr().planning.complete }}
|
||
</p>
|
||
}
|
||
</div>
|
||
</header>
|
||
|
||
@if (planningOutcome(); as outcome) {
|
||
@if (outcome.kind === 'InvalidInput') {
|
||
<ul class="reason-list" role="alert">
|
||
@for (reason of outcome.reasons; track reason.code + reason.message) {
|
||
<li>
|
||
<strong>{{ formatIdentifier(reason.code) }}:</strong> {{ reason.message }}
|
||
</li>
|
||
}
|
||
</ul>
|
||
}
|
||
}
|
||
|
||
@if (planningExplanation(); as outcomeExplanation) {
|
||
@if (outcomeExplanation.kind === 'ConflictExplanation') {
|
||
<section
|
||
class="outcome-explanation conflict-explanation"
|
||
aria-labelledby="conflict-title"
|
||
>
|
||
<h3 id="conflict-title">{{ tr().planning.conflictDetails }}</h3>
|
||
<p>
|
||
<strong>{{ tr().planning.conflictingRules }}:</strong>
|
||
</p>
|
||
<ul>
|
||
@for (rule of outcomeExplanation.conflictingRules; track rule.id) {
|
||
<li>
|
||
{{ rule.label }} <code>{{ rule.id }}</code>
|
||
</li>
|
||
}
|
||
</ul>
|
||
<p>
|
||
<strong>{{ tr().planning.bottleneck }}:</strong>
|
||
{{ outcomeExplanation.bottleneck.message }}
|
||
</p>
|
||
</section>
|
||
} @else if (outcomeExplanation.kind === 'IncompleteExplanation') {
|
||
<section
|
||
class="outcome-explanation incomplete-explanation"
|
||
aria-labelledby="incomplete-title"
|
||
>
|
||
<h3 id="incomplete-title">{{ tr().planning.incompleteDetails }}</h3>
|
||
<p>{{ outcomeExplanation.reason.message }}</p>
|
||
<dl class="compact-facts">
|
||
<div>
|
||
<dt>{{ tr().planning.completedStages }}</dt>
|
||
<dd>
|
||
{{
|
||
outcomeExplanation.completedStages.length
|
||
? outcomeExplanation.completedStages.join(', ')
|
||
: tr().planning.none
|
||
}}
|
||
</dd>
|
||
</div>
|
||
<div>
|
||
<dt>{{ tr().planning.uncompletedStages }}</dt>
|
||
<dd>
|
||
{{
|
||
outcomeExplanation.uncompletedStages.length
|
||
? outcomeExplanation.uncompletedStages.join(', ')
|
||
: tr().planning.none
|
||
}}
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
</section>
|
||
}
|
||
}
|
||
|
||
@if (displayedPlan(); as plan) {
|
||
<dl class="plan-summary">
|
||
<div>
|
||
<dt>{{ tr().planning.daysPlaced }}</dt>
|
||
<dd>{{ plan.placements.length }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>{{ tr().planning.daysGained }}</dt>
|
||
<dd>{{ plan.metrics.nonWorkingDayGain }}</dd>
|
||
</div>
|
||
<div>
|
||
<dt>{{ tr().planning.longestWorkSequence }}</dt>
|
||
<dd>{{ plan.metrics.longestWorkSequence }}</dd>
|
||
</div>
|
||
</dl>
|
||
}
|
||
|
||
<div class="plan-insights">
|
||
@if (selectedExplanation(); as selected) {
|
||
<details
|
||
#planDetails
|
||
class="plan-details"
|
||
[open]="planDetailsOpen()"
|
||
(toggle)="togglePlanDetails($event)"
|
||
>
|
||
<summary>
|
||
<mat-icon aria-hidden="true">lightbulb</mat-icon>
|
||
<span
|
||
>{{ tr().planning.whyPlacement
|
||
}}<small>{{ tr().planning.explanationHelp }}</small></span
|
||
>
|
||
<mat-icon class="disclosure-chevron" aria-hidden="true">expand_more</mat-icon>
|
||
</summary>
|
||
<section
|
||
class="placement-explanation"
|
||
aria-labelledby="placement-explanation-title"
|
||
>
|
||
<header class="explanation-heading">
|
||
<div>
|
||
<h3 id="placement-explanation-title">
|
||
{{ displayDate(selected.placement.date) }} ·
|
||
{{ balanceLabel(selected) }}
|
||
</h3>
|
||
</div>
|
||
<nav
|
||
class="placement-stepper"
|
||
[attr.aria-label]="tr().planning.placementNavigation"
|
||
>
|
||
<button
|
||
type="button"
|
||
data-step="first"
|
||
[attr.aria-disabled]="selectedPlacementIndex() === 0"
|
||
[attr.aria-label]="tr().planning.first"
|
||
(click)="stepPlacement('first')"
|
||
>
|
||
<mat-icon aria-hidden="true">first_page</mat-icon>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
data-step="previous"
|
||
[attr.aria-disabled]="selectedPlacementIndex() === 0"
|
||
[attr.aria-label]="tr().planning.previous"
|
||
(click)="stepPlacement('previous')"
|
||
>
|
||
<mat-icon aria-hidden="true">chevron_left</mat-icon>
|
||
</button>
|
||
<output aria-current="step" [attr.aria-label]="placementAnnouncement()">
|
||
{{ tr().planning.placement }} {{ selectedPlacementIndex() + 1 }}
|
||
{{ tr().planning.of }} {{ planExplanation()!.placements.length }}
|
||
</output>
|
||
<button
|
||
type="button"
|
||
data-step="next"
|
||
[attr.aria-disabled]="
|
||
selectedPlacementIndex() ===
|
||
planExplanation()!.placements.length - 1
|
||
"
|
||
[attr.aria-label]="tr().planning.next"
|
||
(click)="stepPlacement('next')"
|
||
>
|
||
<mat-icon aria-hidden="true">chevron_right</mat-icon>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
data-step="last"
|
||
[attr.aria-disabled]="
|
||
selectedPlacementIndex() ===
|
||
planExplanation()!.placements.length - 1
|
||
"
|
||
[attr.aria-label]="tr().planning.last"
|
||
(click)="stepPlacement('last')"
|
||
>
|
||
<mat-icon aria-hidden="true">last_page</mat-icon>
|
||
</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>
|
||
}
|
||
|
||
@if (selected.timeOffPeriod; as period) {
|
||
<section class="break-story" aria-labelledby="break-title">
|
||
<h4 id="break-title">
|
||
{{ tr().planning.explanation.breakTitle }} ·
|
||
{{ displayDate(period.start) }} – {{ displayDate(period.end) }}
|
||
</h4>
|
||
<div class="break-equation">
|
||
<span
|
||
><strong>{{ period.placementDates.length }}</strong
|
||
>{{ tr().planning.explanation.leaveDays }}</span
|
||
>
|
||
<span>+</span>
|
||
<span
|
||
><strong>{{ period.nonWorkingDates.length }}</strong
|
||
>{{ tr().planning.explanation.freeDays }}</span
|
||
>
|
||
<span>=</span>
|
||
<span
|
||
><strong>{{ period.duration }}</strong
|
||
>{{ tr().planning.explanation.daysOff }}</span
|
||
>
|
||
</div>
|
||
<p>{{ tr().planning.explanation.wholeBreak }}</p>
|
||
</section>
|
||
}
|
||
<div class="leave-story">
|
||
<p>
|
||
{{ leaveUsed(selected) }}
|
||
@if (selected.balance) {
|
||
{{ tr().planning.explanation.expires }}
|
||
<strong>{{
|
||
displayDate(planningDate(selected.balance.expiryDate))
|
||
}}</strong
|
||
>.
|
||
}
|
||
</p>
|
||
@if (selected.ruleIds.length) {
|
||
<p>
|
||
{{ tr().planning.explanation.linkedRules }}
|
||
@for (ruleId of selected.ruleIds; track ruleId; let last = $last) {
|
||
<strong>{{ ruleLabel(ruleId) }}</strong
|
||
>{{ last ? '.' : ', ' }}
|
||
}
|
||
</p>
|
||
}
|
||
@if (hasAtRiskContribution(selected)) {
|
||
<p>{{ tr().planning.explanation.atRiskContribution }}</p>
|
||
}
|
||
@if (selected.supporting) {
|
||
<p>{{ tr().planning.details.supporting }}</p>
|
||
}
|
||
<p>
|
||
<strong>{{ tr().planning.remaining }}:</strong>
|
||
{{ selected.remainingQuantity }} {{ tr().userInput.daysSuffix }}
|
||
@if (selected.remainingBalance; as remaining) {
|
||
·
|
||
{{
|
||
tr().planning.details.remainingReasons[remaining.classification]
|
||
}}
|
||
}
|
||
</p>
|
||
</div>
|
||
@let decisiveObjective =
|
||
selected.firstDecisiveObjective ??
|
||
selected.comparison.firstDecisiveObjective;
|
||
@if (decisiveObjective) {
|
||
<p class="decisive-summary">
|
||
<strong>{{ tr().planning.details.firstDifference }}:</strong>
|
||
{{ objectiveCopy(decisiveObjective).label }}
|
||
</p>
|
||
}
|
||
<section class="day-impact" aria-labelledby="impact-title">
|
||
<h4 id="impact-title">{{ tr().planning.explanation.impact }}</h4>
|
||
<p>{{ tr().planning.explanation.impactHelp }}</p>
|
||
@if (placementChanges().length) {
|
||
<table class="impact-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">
|
||
{{ tr().planning.explanation.criterion }}
|
||
</th>
|
||
<th scope="col">
|
||
{{ tr().planning.explanation.withoutDay }}
|
||
</th>
|
||
<th scope="col">{{ tr().planning.explanation.withDay }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (change of placementChanges(); track change.id) {
|
||
<tr [class.decisive-row]="change.id === decisiveObjective">
|
||
<th scope="row">
|
||
{{ change.label }}<small>{{ change.help }}</small>
|
||
@if (change.id === decisiveObjective) {
|
||
<span class="decisive-label">{{
|
||
tr().planning.details.firstDifference
|
||
}}</span>
|
||
}
|
||
</th>
|
||
<td>{{ change.before }}</td>
|
||
<td>{{ change.after }}</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
} @else {
|
||
<p>{{ tr().planning.explanation.noChange }}</p>
|
||
}
|
||
</section>
|
||
|
||
<details class="technical-details">
|
||
<summary>{{ tr().planning.technicalDetails }}</summary>
|
||
<div class="calculation-content">
|
||
<p class="calculation-intro">{{ tr().planning.details.intro }}</p>
|
||
<section class="balance-detail" aria-labelledby="detail-balance-title">
|
||
<h4 id="detail-balance-title">
|
||
{{ tr().planning.details.balance }} ·
|
||
{{ balanceLabel(selected) }}
|
||
</h4>
|
||
<dl class="balance-ledger">
|
||
@if (selected.balance?.availableFrom; as available) {
|
||
<div>
|
||
<dt>{{ tr().planning.details.available }}</dt>
|
||
<dd>{{ displayDate(planningDate(available)) }}</dd>
|
||
</div>
|
||
}
|
||
@if (selected.balance; as balance) {
|
||
<div>
|
||
<dt>{{ tr().planning.details.expires }}</dt>
|
||
<dd>
|
||
{{ displayDate(planningDate(balance.expiryDate)) }}
|
||
</dd>
|
||
</div>
|
||
}
|
||
<div>
|
||
<dt>{{ tr().planning.remaining }}</dt>
|
||
<dd>
|
||
{{ selected.remainingQuantity }}
|
||
{{ tr().userInput.daysSuffix }}
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
@if (selected.remainingBalance; as remaining) {
|
||
<p>
|
||
{{
|
||
tr().planning.details.remainingReasons[
|
||
remaining.classification
|
||
]
|
||
}}
|
||
</p>
|
||
}
|
||
</section>
|
||
<ng-container
|
||
[ngTemplateOutlet]="calculationTable"
|
||
[ngTemplateOutletContext]="{
|
||
comparison: selected.comparison,
|
||
beforeLabel: tr().planning.explanation.withoutDay,
|
||
afterLabel: tr().planning.explanation.withDay,
|
||
audit: selected,
|
||
}"
|
||
/>
|
||
</div>
|
||
</details>
|
||
|
||
<button type="button" class="compare-dates-button" (click)="compareDates()">
|
||
<mat-icon aria-hidden="true">compare_arrows</mat-icon
|
||
>{{
|
||
counterfactualError()
|
||
? tr().planning.retryCounterfactual
|
||
: tr().planning.compareDates
|
||
}}
|
||
</button>
|
||
</section>
|
||
</details>
|
||
} @else if (explanationError()) {
|
||
<p class="inline-error" role="alert">
|
||
{{ tr().planning.explanationError }} {{ explanationError()!.message }}
|
||
</p>
|
||
}
|
||
|
||
@if (planningOutcome() && planningOutcome()?.kind !== 'InvalidInput') {
|
||
<details class="trace-explorer" [open]="traceOpen()" (toggle)="toggleTrace($event)">
|
||
<summary>
|
||
<mat-icon aria-hidden="true">account_tree</mat-icon>
|
||
<span
|
||
>{{ tr().planning.traceTitle
|
||
}}<small>{{ tr().planning.traceHelp }}</small></span
|
||
>
|
||
<mat-icon class="disclosure-chevron" aria-hidden="true">expand_more</mat-icon>
|
||
</summary>
|
||
@if (traceLoading()) {
|
||
<p role="status">{{ tr().planning.traceLoading }}</p>
|
||
} @else if (planningTrace(); as trace) {
|
||
@if (trace.error) {
|
||
<p class="inline-error" role="alert">{{ trace.error.message }}</p>
|
||
}
|
||
<div class="trace-layout">
|
||
<nav class="trace-phases" [attr.aria-label]="tr().planning.phases">
|
||
<div class="trace-phase-stepper">
|
||
<button
|
||
type="button"
|
||
data-phase-step="first"
|
||
[attr.aria-disabled]="tracePhaseIndex() === 0"
|
||
[attr.aria-label]="tr().planning.firstPhase"
|
||
(click)="stepTracePhase('first')"
|
||
>
|
||
<mat-icon aria-hidden="true">first_page</mat-icon>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
data-phase-step="previous"
|
||
[attr.aria-disabled]="tracePhaseIndex() === 0"
|
||
[attr.aria-label]="tr().planning.previousPhase"
|
||
(click)="stepTracePhase('previous')"
|
||
>
|
||
<mat-icon aria-hidden="true">chevron_left</mat-icon>
|
||
</button>
|
||
<output
|
||
aria-current="step"
|
||
[attr.aria-label]="tracePhaseAnnouncement()"
|
||
>{{ tracePhaseIndex() + 1 }} {{ tr().planning.of }}
|
||
{{ trace.phases.length }}</output
|
||
>
|
||
<button
|
||
type="button"
|
||
data-phase-step="next"
|
||
[attr.aria-disabled]="
|
||
tracePhaseIndex() === trace.phases.length - 1
|
||
"
|
||
[attr.aria-label]="tr().planning.nextPhase"
|
||
(click)="stepTracePhase('next')"
|
||
>
|
||
<mat-icon aria-hidden="true">chevron_right</mat-icon>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
data-phase-step="last"
|
||
[attr.aria-disabled]="
|
||
tracePhaseIndex() === trace.phases.length - 1
|
||
"
|
||
[attr.aria-label]="tr().planning.lastPhase"
|
||
(click)="stepTracePhase('last')"
|
||
>
|
||
<mat-icon aria-hidden="true">last_page</mat-icon>
|
||
</button>
|
||
</div>
|
||
@for (phase of trace.phases; track phase.id; let index = $index) {
|
||
<button
|
||
type="button"
|
||
[class.active]="index === tracePhaseIndex()"
|
||
[attr.aria-pressed]="index === tracePhaseIndex()"
|
||
(click)="selectTracePhase(index)"
|
||
>
|
||
<span>{{ phaseLabel(phase.name) }}</span>
|
||
<small>{{ tracePhaseStatusLabel(phase.status) }}</small>
|
||
</button>
|
||
}
|
||
</nav>
|
||
|
||
@if (selectedTracePhase(); as phase) {
|
||
<section class="trace-decisions" aria-labelledby="trace-phase-title">
|
||
<h3 id="trace-phase-title">{{ phaseLabel(phase.name) }}</h3>
|
||
@if (phase.decisions.length) {
|
||
<nav [attr.aria-label]="tr().planning.decisions">
|
||
<button
|
||
type="button"
|
||
[attr.aria-disabled]="traceDecisionIndex() === 0"
|
||
[attr.aria-label]="tr().planning.firstDecision"
|
||
(click)="stepTraceDecision('first')"
|
||
>
|
||
<mat-icon aria-hidden="true">first_page</mat-icon>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
[attr.aria-disabled]="traceDecisionIndex() === 0"
|
||
[attr.aria-label]="tr().planning.previousDecision"
|
||
(click)="stepTraceDecision('previous')"
|
||
>
|
||
<mat-icon aria-hidden="true">chevron_left</mat-icon>
|
||
</button>
|
||
<output
|
||
aria-current="step"
|
||
[attr.aria-label]="traceDecisionAnnouncement()"
|
||
>{{ tr().planning.decision }}
|
||
{{ traceDecisionIndex() + 1 }} {{ tr().planning.of }}
|
||
{{ phase.decisions.length }}</output
|
||
>
|
||
<button
|
||
type="button"
|
||
[attr.aria-disabled]="
|
||
traceDecisionIndex() === phase.decisions.length - 1
|
||
"
|
||
[attr.aria-label]="tr().planning.nextDecision"
|
||
(click)="stepTraceDecision('next')"
|
||
>
|
||
<mat-icon aria-hidden="true">chevron_right</mat-icon>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
[attr.aria-disabled]="
|
||
traceDecisionIndex() === phase.decisions.length - 1
|
||
"
|
||
[attr.aria-label]="tr().planning.lastDecision"
|
||
(click)="stepTraceDecision('last')"
|
||
>
|
||
<mat-icon aria-hidden="true">last_page</mat-icon>
|
||
</button>
|
||
</nav>
|
||
|
||
@if (selectedTraceDecision(); as decision) {
|
||
<div
|
||
class="trace-toolbar"
|
||
role="group"
|
||
[attr.aria-label]="tr().planning.replay"
|
||
>
|
||
<button
|
||
type="button"
|
||
[attr.aria-pressed]="
|
||
traceReplay() && traceMoment() === 'before'
|
||
"
|
||
(click)="showTraceMoment('before')"
|
||
>
|
||
{{ tr().planning.before }}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
[attr.aria-pressed]="
|
||
traceReplay() && traceMoment() === 'after'
|
||
"
|
||
(click)="showTraceMoment('after')"
|
||
>
|
||
{{ tr().planning.after }}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
[attr.aria-pressed]="!traceReplay()"
|
||
(click)="showFinalPlan()"
|
||
>
|
||
{{ tr().planning.finalPlan }}
|
||
</button>
|
||
</div>
|
||
@if (historicalEligibilityLoading()) {
|
||
<p role="status">
|
||
{{ tr().planning.historicalEligibilityLoading }}
|
||
</p>
|
||
}
|
||
@if (historicalEligibilityError(); as error) {
|
||
<p role="alert">
|
||
{{ tr().planning.historicalEligibilityError }}
|
||
{{ error }}
|
||
<button
|
||
type="button"
|
||
(click)="retryHistoricalEligibility()"
|
||
>
|
||
{{ tr().planning.retryHistoricalEligibility }}
|
||
</button>
|
||
</p>
|
||
}
|
||
<p>
|
||
<strong>{{ tr().planning.dates }}:</strong>
|
||
{{
|
||
decision.dates.length
|
||
? decision.dates.join(', ')
|
||
: tr().planning.none
|
||
}}
|
||
</p>
|
||
@if (decision.reason) {
|
||
<p>
|
||
<strong>{{ tr().planning.reason }}:</strong>
|
||
{{ decision.reason.message }}
|
||
</p>
|
||
}
|
||
|
||
<h4>{{ tr().planning.candidates }}</h4>
|
||
@if (decision.candidates.length > 50) {
|
||
<nav
|
||
class="candidate-pagination"
|
||
[attr.aria-label]="tr().planning.candidates"
|
||
>
|
||
<button
|
||
type="button"
|
||
[attr.aria-disabled]="
|
||
traceCandidatePage() === 0
|
||
"
|
||
[attr.aria-label]="
|
||
tr().planning.previousCandidates
|
||
"
|
||
(click)="stepTraceCandidatePage(-1)"
|
||
>
|
||
<mat-icon aria-hidden="true"
|
||
>chevron_left</mat-icon
|
||
>
|
||
</button>
|
||
<output
|
||
>{{ tr().planning.page }}
|
||
{{ traceCandidatePage() + 1 }}
|
||
{{ tr().planning.of }}
|
||
{{ traceCandidatePages() }}</output
|
||
>
|
||
<button
|
||
type="button"
|
||
[attr.aria-disabled]="
|
||
traceCandidatePage() ===
|
||
traceCandidatePages() - 1
|
||
"
|
||
[attr.aria-label]="tr().planning.nextCandidates"
|
||
(click)="stepTraceCandidatePage(1)"
|
||
>
|
||
<mat-icon aria-hidden="true"
|
||
>chevron_right</mat-icon
|
||
>
|
||
</button>
|
||
</nav>
|
||
}
|
||
<div class="candidate-list">
|
||
@for (
|
||
candidate of visibleTraceCandidates();
|
||
track $index
|
||
) {
|
||
<details>
|
||
<summary>
|
||
<span
|
||
class="status-symbol"
|
||
aria-hidden="true"
|
||
>{{
|
||
candidate.status === 'chosen'
|
||
? '✓'
|
||
: candidate.status ===
|
||
'ineligible'
|
||
? '×'
|
||
: '•'
|
||
}}</span
|
||
>
|
||
{{ traceStatusLabel(candidate.status) }} ·
|
||
@if (
|
||
candidate.sourceDate &&
|
||
candidate.sourceDate !== candidate.date
|
||
) {
|
||
{{ candidate.sourceDate }} →
|
||
}
|
||
{{
|
||
candidate.dates?.join(', ') ||
|
||
candidate.date ||
|
||
tr().planning.none
|
||
}}
|
||
</summary>
|
||
@if (candidate.reason) {
|
||
<p>{{ candidate.reason.message }}</p>
|
||
}
|
||
@if (candidate.technicalComparison?.length) {
|
||
<section class="technical-comparison">
|
||
<h5>
|
||
{{
|
||
tr().planning.details
|
||
.traceStageTitle
|
||
}}
|
||
</h5>
|
||
<p class="calculation-intro">
|
||
{{
|
||
tr().planning.details
|
||
.traceStageHelp
|
||
}}
|
||
</p>
|
||
<table class="impact-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">
|
||
{{
|
||
tr().planning
|
||
.explanation
|
||
.criterion
|
||
}}
|
||
</th>
|
||
<th scope="col">
|
||
{{
|
||
tr().planning.before
|
||
}}
|
||
</th>
|
||
<th scope="col">
|
||
{{
|
||
tr().planning.after
|
||
}}
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (
|
||
comparator of candidate.technicalComparison;
|
||
track $index
|
||
) {
|
||
<tr
|
||
[class.decisive-row]="
|
||
comparator.name ===
|
||
candidate.firstDecisiveComparator
|
||
"
|
||
>
|
||
<th scope="row">
|
||
<code>{{
|
||
comparator.name
|
||
}}</code>
|
||
<small>{{
|
||
comparator.direction ===
|
||
'maximize'
|
||
? tr()
|
||
.planning
|
||
.details
|
||
.higher
|
||
: tr()
|
||
.planning
|
||
.details
|
||
.lower
|
||
}}</small>
|
||
@if (
|
||
comparator.name ===
|
||
candidate.firstDecisiveComparator
|
||
) {
|
||
<span
|
||
class="decisive-label"
|
||
>{{
|
||
tr()
|
||
.planning
|
||
.details
|
||
.firstDifference
|
||
}}</span
|
||
>
|
||
}
|
||
</th>
|
||
<td>
|
||
{{
|
||
comparator.before
|
||
}}
|
||
</td>
|
||
<td>
|
||
{{
|
||
comparator.after
|
||
}}
|
||
</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
}
|
||
@if (candidate.comparison; as comparison) {
|
||
<div class="calculation-content">
|
||
<ng-container
|
||
[ngTemplateOutlet]="
|
||
calculationTable
|
||
"
|
||
[ngTemplateOutletContext]="{
|
||
comparison,
|
||
beforeLabel:
|
||
tr().planning.before,
|
||
afterLabel: tr().planning.after,
|
||
audit: comparison,
|
||
traceEffect:
|
||
!!candidate
|
||
.technicalComparison
|
||
?.length,
|
||
}"
|
||
/>
|
||
</div>
|
||
}
|
||
@if (candidate.placement) {
|
||
<button
|
||
type="button"
|
||
(click)="
|
||
analyzeTraceCandidate(candidate)
|
||
"
|
||
>
|
||
{{ tr().planning.compareAlternatives }}
|
||
</button>
|
||
}
|
||
</details>
|
||
}
|
||
</div>
|
||
|
||
<h4>{{ tr().planning.deltas }}</h4>
|
||
<ul>
|
||
@for (delta of decision.deltas; track $index) {
|
||
<li>
|
||
<strong>{{
|
||
formatIdentifier(delta.kind)
|
||
}}</strong>
|
||
@if (delta.fromDate || delta.date) {
|
||
· {{ delta.fromDate || delta.date }}
|
||
}
|
||
@if (delta.toDate) {
|
||
→ {{ delta.toDate }}
|
||
}
|
||
@if (delta.balanceId) {
|
||
· {{ delta.balanceId }}
|
||
}
|
||
@if (delta.remainingAfter !== undefined) {
|
||
· {{ delta.remainingBefore }} →
|
||
{{ delta.remainingAfter }}
|
||
}
|
||
</li>
|
||
}
|
||
</ul>
|
||
@if (traceState(); as state) {
|
||
<p class="trace-state-summary" role="status">
|
||
{{
|
||
traceMoment() === 'before'
|
||
? tr().planning.before
|
||
: tr().planning.after
|
||
}}: {{ state.placements.length }}
|
||
{{ tr().planning.placements }},
|
||
{{ state.earnedActivations.length }}
|
||
{{ tr().planning.activations }}
|
||
@for (balance of state.balances; track balance.id) {
|
||
<span class="trace-state-balance"
|
||
>{{ balance.id }}: {{ balance.remaining }}
|
||
{{ tr().planning.balanceRemaining }}
|
||
<small
|
||
>{{ tr().planning.details.available }}
|
||
{{ displayDate(balance.availableFrom) }}
|
||
·
|
||
{{ tr().planning.details.expires }}
|
||
{{
|
||
displayDate(balance.expiryDate)
|
||
}}</small
|
||
></span
|
||
>
|
||
}
|
||
</p>
|
||
}
|
||
}
|
||
} @else {
|
||
<p>{{ tr().planning.noDecisions }}</p>
|
||
}
|
||
</section>
|
||
}
|
||
</div>
|
||
} @else if (traceResolved()) {
|
||
<p role="status">{{ tr().planning.traceUnavailable }}</p>
|
||
}
|
||
</details>
|
||
}
|
||
</div>
|
||
|
||
@if (traceReplay()) {
|
||
<div class="replay-notice" role="status">
|
||
<span>{{ tr().planning.replayNotice }}</span>
|
||
<button type="button" class="quiet-button" (click)="showFinalPlan()">
|
||
{{ tr().planning.finalPlan }}
|
||
</button>
|
||
</div>
|
||
}
|
||
<p class="calendar-help">
|
||
<mat-icon aria-hidden="true">touch_app</mat-icon>{{ tr().planning.calendarHelp }}
|
||
</p>
|
||
<div class="calendar-legend" role="group" [attr.aria-label]="tr().planning.legend">
|
||
<strong>{{ tr().planning.legend }}</strong>
|
||
<ul>
|
||
<li>
|
||
<span class="legend-swatch weekend" aria-hidden="true"></span
|
||
>{{ tr().dayTypes.weekend }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-swatch closed-day" aria-hidden="true"></span
|
||
>{{ tr().dayTypes.closedDay }}
|
||
</li>
|
||
<li><span class="legend-swatch cp" aria-hidden="true"></span>{{ tr().dayTypes.cp }}</li>
|
||
<li>
|
||
<span class="legend-swatch rtt" aria-hidden="true"></span>{{ tr().dayTypes.rtt }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-swatch other" aria-hidden="true"></span
|
||
>{{ tr().dayTypes.other }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-swatch today" aria-hidden="true"></span
|
||
>{{ tr().dayTypes.today }}
|
||
</li>
|
||
@if (planDetailsOpen() || traceReplay()) {
|
||
<li>
|
||
<span class="legend-swatch current" aria-hidden="true"></span
|
||
>{{ tr().planning.currentPlacement }}
|
||
</li>
|
||
}
|
||
@if (explainedPeriod()) {
|
||
<li>
|
||
<span class="legend-swatch explained-period" aria-hidden="true"></span
|
||
>{{ tr().planning.explainedPeriod }}
|
||
</li>
|
||
}
|
||
@if (traceReplay() && counterfactual()) {
|
||
<li>
|
||
<span class="legend-symbol hypothetical-symbol" aria-hidden="true">~✓</span
|
||
>{{ tr().planning.eligible }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-symbol hypothetical-symbol" aria-hidden="true">~×</span
|
||
>{{ tr().planning.ineligible }}
|
||
</li>
|
||
}
|
||
@if (traceReplay()) {
|
||
<li>
|
||
<span class="legend-symbol" aria-hidden="true">✓</span
|
||
>{{ traceStatusLabel('chosen') }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-symbol" aria-hidden="true">−</span
|
||
>{{ traceStatusLabel('evaluated-rejected') }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-symbol" aria-hidden="true">⌁</span
|
||
>{{ traceStatusLabel('pruned') }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-symbol" aria-hidden="true">?</span
|
||
>{{ traceStatusLabel('not-examined') }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-symbol" aria-hidden="true">×</span
|
||
>{{ traceStatusLabel('ineligible') }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-symbol" aria-hidden="true">+</span
|
||
>{{ traceChangeLabel('added') }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-symbol" aria-hidden="true">←</span
|
||
>{{ traceChangeLabel('moved-from') }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-symbol" aria-hidden="true">→</span
|
||
>{{ traceChangeLabel('moved-to') }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-symbol" aria-hidden="true">−</span
|
||
>{{ traceChangeLabel('removed') }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-symbol" aria-hidden="true">≈</span
|
||
>{{ traceChangeLabel('shared') }}
|
||
</li>
|
||
<li>
|
||
<span class="legend-symbol" aria-hidden="true">=</span
|
||
>{{ traceChangeLabel('unchanged') }}
|
||
</li>
|
||
}
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="month-grid">
|
||
@for (month of months(); track month.year + '-' + month.monthIndex) {
|
||
<mat-card class="month-card">
|
||
<mat-card-content>
|
||
<app-calendar-month
|
||
[monthIndex]="month.monthIndex"
|
||
[year]="month.year"
|
||
[selectedDates]="calendarSelectedDates()"
|
||
[horizonStart]="planningHorizon()?.start"
|
||
[horizonEnd]="planningHorizon()?.end"
|
||
[currentDate]="currentPlacementDate()"
|
||
[explainedPeriod]="explainedPeriod()"
|
||
[dateStatuses]="traceReplay() ? dateStatuses() : {}"
|
||
(dateSelected)="selectCalendarDate($event)"
|
||
></app-calendar-month>
|
||
</mat-card-content>
|
||
</mat-card>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<ng-template
|
||
#calculationTable
|
||
let-comparison="comparison"
|
||
let-beforeLabel="beforeLabel"
|
||
let-afterLabel="afterLabel"
|
||
let-audit="audit"
|
||
let-traceEffect="traceEffect"
|
||
>
|
||
<section class="criteria-detail">
|
||
<h4>
|
||
{{
|
||
traceEffect
|
||
? tr().planning.details.traceImpactTitle
|
||
: tr().planning.details.criteria
|
||
}}
|
||
</h4>
|
||
<p class="calculation-intro">
|
||
{{
|
||
traceEffect
|
||
? tr().planning.details.traceImpactHelp
|
||
: tr().planning.details.criteriaHelp
|
||
}}
|
||
</p>
|
||
<table class="impact-table criteria-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">{{ tr().planning.explanation.criterion }}</th>
|
||
<th scope="col">{{ beforeLabel }}</th>
|
||
<th scope="col">{{ afterLabel }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (
|
||
objective of objectiveRows(comparison.before, comparison.after);
|
||
track objective.id;
|
||
let index = $index
|
||
) {
|
||
@if (objective.numeric) {
|
||
<tr
|
||
[class.decisive-row]="
|
||
!traceEffect && objective.id === comparison.firstDecisiveObjective
|
||
"
|
||
>
|
||
<th scope="row">
|
||
<details class="criterion-help">
|
||
<summary>{{ index + 1 }}. {{ objective.label }}</summary>
|
||
<p>{{ objective.help }}</p>
|
||
</details>
|
||
<small>{{
|
||
objective.direction === 'maximize'
|
||
? tr().planning.details.higher
|
||
: tr().planning.details.lower
|
||
}}</small>
|
||
@if (
|
||
!traceEffect &&
|
||
objective.id === comparison.firstDecisiveObjective
|
||
) {
|
||
<span class="decisive-label">{{
|
||
tr().planning.details.firstDifference
|
||
}}</span>
|
||
}
|
||
</th>
|
||
<td>{{ objective.before ?? tr().planning.none }}</td>
|
||
<td>{{ objective.value }}</td>
|
||
</tr>
|
||
}
|
||
}
|
||
</tbody>
|
||
</table>
|
||
@if (hasTieBreaks(comparison.after)) {
|
||
<div class="tie-breaks">
|
||
<h5>{{ tr().planning.details.tieBreaks }}</h5>
|
||
<ol>
|
||
@for (
|
||
objective of objectiveRows(comparison.before, comparison.after);
|
||
track objective.id
|
||
) {
|
||
@if (!objective.numeric) {
|
||
<li>
|
||
{{ objective.label }}
|
||
@if (
|
||
!traceEffect &&
|
||
objective.id === comparison.firstDecisiveObjective
|
||
) {
|
||
<span class="decisive-label">{{
|
||
tr().planning.details.firstDifference
|
||
}}</span>
|
||
}
|
||
</li>
|
||
}
|
||
}
|
||
</ol>
|
||
</div>
|
||
}
|
||
</section>
|
||
<ng-container [ngTemplateOutlet]="auditData" [ngTemplateOutletContext]="{ $implicit: audit }" />
|
||
</ng-template>
|
||
|
||
<ng-template #auditData let-data>
|
||
<details class="audit-data">
|
||
<summary>{{ tr().planning.details.audit }}</summary>
|
||
<p class="calculation-intro">{{ tr().planning.details.auditHelp }}</p>
|
||
<pre tabindex="0" [attr.aria-label]="tr().planning.details.audit">{{ data | json }}</pre>
|
||
</details>
|
||
</ng-template>
|
||
|
||
<ng-template #dateComparisonDialog>
|
||
<section class="counterfactual-panel">
|
||
<header class="comparison-heading">
|
||
<div>
|
||
<h2 id="counterfactual-title" tabindex="-1">
|
||
{{ tr().planning.comparison.title }}
|
||
</h2>
|
||
<p id="comparison-intro">
|
||
{{
|
||
traceReplay()
|
||
? tr().planning.explanation.traceHelp
|
||
: tr().planning.explanation.moveHelp
|
||
}}
|
||
</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="comparison-close"
|
||
[attr.aria-label]="tr().planning.stopComparing"
|
||
(click)="clearCounterfactual()"
|
||
>
|
||
<mat-icon aria-hidden="true">close</mat-icon>
|
||
</button>
|
||
</header>
|
||
<mat-dialog-content class="comparison-content">
|
||
@if (counterfactualLoading()) {
|
||
<div class="comparison-loading" role="status">
|
||
<mat-spinner [diameter]="28" />
|
||
<p>{{ tr().planning.counterfactualLoading }}</p>
|
||
</div>
|
||
} @else if (counterfactualError()) {
|
||
<div class="counterfactual-inline-error" role="alert">
|
||
<p>{{ tr().planning.counterfactualError }} {{ counterfactualError() }}</p>
|
||
</div>
|
||
} @else if (counterfactual(); as analysis) {
|
||
<div class="comparison-date-pair">
|
||
<div class="original-date">
|
||
<span>{{
|
||
traceReplay()
|
||
? tr().planning.comparison.candidate
|
||
: tr().planning.explanation.plannedDate
|
||
}}</span>
|
||
<strong>{{ displayDate(analysis.placement.date) }}</strong>
|
||
</div>
|
||
<mat-icon aria-hidden="true">arrow_forward</mat-icon>
|
||
<mat-form-field
|
||
class="compare-date-field"
|
||
appearance="outline"
|
||
subscriptSizing="dynamic"
|
||
>
|
||
<mat-label>{{ tr().planning.explanation.tryDate }}</mat-label>
|
||
<input
|
||
id="comparison-date"
|
||
matInput
|
||
[matDatepicker]="comparisonPicker"
|
||
placeholder="DD/MM/YYYY"
|
||
[value]="comparisonDateInput()"
|
||
[min]="planningHorizon()?.start + 'T00:00:00'"
|
||
[max]="analysis.results.at(-1)?.date + 'T00:00:00'"
|
||
[attr.aria-invalid]="invalidComparisonDate()"
|
||
[aria-describedby]="
|
||
invalidComparisonDate()
|
||
? 'comparison-range comparison-date-error'
|
||
: 'comparison-range'
|
||
"
|
||
(dateInput)="changeComparisonDate($event)"
|
||
/>
|
||
<mat-datepicker-toggle matIconSuffix [for]="comparisonPicker" />
|
||
<mat-datepicker #comparisonPicker />
|
||
</mat-form-field>
|
||
</div>
|
||
@if (analysis.results[0]; as firstDate) {
|
||
<p id="comparison-range" class="comparison-range">
|
||
{{ tr().planning.comparison.dateRange }} {{ displayDate(firstDate.date) }} –
|
||
{{ displayDate(analysis.results.at(-1)!.date) }}
|
||
</p>
|
||
}
|
||
@if (invalidComparisonDate()) {
|
||
<p id="comparison-date-error" class="inline-error" role="alert">
|
||
{{ tr().planning.comparison.invalidDate }}
|
||
</p>
|
||
}
|
||
@if (nearbyComparisonDates().length) {
|
||
<div
|
||
class="nearby-dates"
|
||
role="group"
|
||
[attr.aria-label]="tr().planning.comparison.nearby"
|
||
>
|
||
<span>{{ tr().planning.comparison.nearby }}</span>
|
||
@for (alternative of nearbyComparisonDates(); track alternative.date) {
|
||
<button
|
||
type="button"
|
||
[attr.aria-pressed]="
|
||
selectedCounterfactualDate() === alternative.date
|
||
"
|
||
(click)="selectCalendarDate(alternative.date)"
|
||
>
|
||
{{ displayDate(alternative.date) }}
|
||
</button>
|
||
}
|
||
</div>
|
||
} @else {
|
||
<p class="comparison-range">{{ tr().planning.comparison.noAlternatives }}</p>
|
||
}
|
||
@if (selectedCounterfactualResult(); as result) {
|
||
<div class="counterfactual-result">
|
||
<p class="sr-only" role="status">
|
||
{{ displayDate(result.date) }} ·
|
||
{{
|
||
result.status === 'eligible'
|
||
? tr().planning.comparison.available
|
||
: tr().planning.comparison.unavailable
|
||
}}
|
||
</p>
|
||
@if (result.status === 'ineligible') {
|
||
<div class="unavailable-date" role="status">
|
||
<mat-icon aria-hidden="true">event_busy</mat-icon>
|
||
<div>
|
||
<h3>{{ tr().planning.comparison.unavailable }}</h3>
|
||
@if (result.reason) {
|
||
<p>
|
||
{{
|
||
tr().planning.explanation.unavailable[
|
||
result.reason.code
|
||
] ?? result.reason.message
|
||
}}
|
||
</p>
|
||
}
|
||
</div>
|
||
</div>
|
||
} @else if (!traceReplay() && result.date === analysis.placement.date) {
|
||
<p class="comparison-empty">{{ tr().planning.explanation.sameDate }}</p>
|
||
} @else {
|
||
<div class="comparison-result-heading">
|
||
<h3>{{ tr().planning.comparison.changes }}</h3>
|
||
<span class="available-date"
|
||
><mat-icon aria-hidden="true">check_circle</mat-icon
|
||
>{{ tr().planning.comparison.available }}</span
|
||
>
|
||
</div>
|
||
@if (dateComparison()) {
|
||
@if (dateChanges().length) {
|
||
<table class="impact-table">
|
||
<thead>
|
||
<tr>
|
||
<th scope="col">
|
||
{{ tr().planning.explanation.criterion }}
|
||
</th>
|
||
<th scope="col">
|
||
{{
|
||
traceReplay()
|
||
? tr().planning.explanation
|
||
.beforeDecision
|
||
: tr().planning.explanation.currentPlan
|
||
}}
|
||
</th>
|
||
<th scope="col">
|
||
{{
|
||
traceReplay()
|
||
? tr().planning.explanation
|
||
.withCandidate
|
||
: tr().planning.explanation.alternative
|
||
}}
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (change of dateChanges(); track change.id) {
|
||
<tr>
|
||
<th scope="row">{{ change.label }}</th>
|
||
<td>{{ change.before }}</td>
|
||
<td>{{ change.after }}</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
} @else {
|
||
<p class="comparison-empty">
|
||
{{ tr().planning.explanation.noChange }}
|
||
</p>
|
||
}
|
||
} @else {
|
||
<p class="comparison-empty">
|
||
{{ tr().planning.explanation.comparisonUnavailable }}
|
||
</p>
|
||
}
|
||
}
|
||
<details class="technical-details date-calculation-details">
|
||
<summary>{{ tr().planning.technicalDetails }}</summary>
|
||
<div class="calculation-content">
|
||
<p>
|
||
<strong>{{ tr().planning.historical }}:</strong>
|
||
{{
|
||
dateStatuses()[result.date]?.label ??
|
||
traceStatusLabel(
|
||
result.historicalStatus ?? 'not-examined'
|
||
)
|
||
}}
|
||
</p>
|
||
@if (dateStatuses()[result.date]?.reason; as historicalReason) {
|
||
<p>
|
||
<strong>{{ tr().planning.reason }}:</strong>
|
||
{{ historicalReason }}
|
||
</p>
|
||
}
|
||
@if (dateComparison(); as comparison) {
|
||
<ng-container
|
||
[ngTemplateOutlet]="calculationTable"
|
||
[ngTemplateOutletContext]="{
|
||
comparison,
|
||
beforeLabel: traceReplay()
|
||
? tr().planning.explanation.beforeDecision
|
||
: tr().planning.explanation.currentPlan,
|
||
afterLabel: traceReplay()
|
||
? tr().planning.explanation.withCandidate
|
||
: tr().planning.explanation.alternative,
|
||
audit: result,
|
||
traceEffect: traceReplay(),
|
||
}"
|
||
/>
|
||
} @else {
|
||
<ng-container
|
||
[ngTemplateOutlet]="auditData"
|
||
[ngTemplateOutletContext]="{ $implicit: result }"
|
||
/>
|
||
}
|
||
</div>
|
||
</details>
|
||
</div>
|
||
} @else if (!invalidComparisonDate()) {
|
||
<p class="comparison-empty">{{ tr().planning.comparison.chooseDate }}</p>
|
||
}
|
||
}
|
||
</mat-dialog-content>
|
||
<footer class="comparison-footer">
|
||
<span>{{ tr().planning.comparison.preview }}</span>
|
||
<button
|
||
type="button"
|
||
class="comparison-done"
|
||
(click)="counterfactualError() ? retryCounterfactual() : clearCounterfactual()"
|
||
>
|
||
{{
|
||
counterfactualError()
|
||
? tr().planning.retryCounterfactual
|
||
: traceReplay()
|
||
? tr().planning.comparison.backToTrace
|
||
: tr().planning.comparison.backToPlan
|
||
}}
|
||
</button>
|
||
</footer>
|
||
</section>
|
||
</ng-template>
|