Files
vac-optimizer/src/app/components/calendar-year/calendar-year.html
T
2026-10-07 00:28:29 +02:00

1394 lines
43 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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
>{{ declarationEntry() ? tr().bookedDays.help : tr().planning.calendarHelp }}
</p>
@if (declarationEntry() && !calendarHorizon()) {
<p role="alert">{{ tr().settings.invalidHorizon }}</p>
}
<div class="calendar-legend" role="group" [attr.aria-label]="tr().planning.legend">
<strong>{{ tr().planning.legend }}</strong>
<ul>
<li>
<span class="legend-swatch declared" aria-hidden="true"></span
>{{ tr().bookedDays.booked }}
</li>
<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
#monthGrid
class="month-grid"
[class.declaration-entry]="declarationEntry()"
(pointerdown)="startDeclarationRange($event)"
(click)="selectDeclaredDate($event)"
>
@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]="calendarHorizon()?.start"
[horizonEnd]="calendarHorizon()?.end"
[declarationEntry]="declarationEntry()"
[declaredDays]="calendarDeclaredDays()"
[declarationPreview]="declarationPreview()"
[currentDate]="declarationEntry() ? null : currentPlacementDate()"
[explainedPeriod]="declarationEntry() ? undefined : explainedPeriod()"
[dateStatuses]="!declarationEntry() && traceReplay() ? dateStatuses() : {}"
(dateSelected)="!declarationEntry() && 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>