Files
vac-optimizer/src/app/components/calendar-year/calendar-year.html
T
nessar d9d9befa12
CI / policy (push) Successful in 7s
CI / validate (push) Successful in 1m15s
CI / test (push) Successful in 3m18s
CI / benchmark (push) Failing after 5m53s
CI / build (push) Successful in 1m23s
CI / security (push) Successful in 7m6s
CI / policy (pull_request) Successful in 7s
CI / validate (pull_request) Successful in 1m9s
CI / build (pull_request) Successful in 43s
CI / test (pull_request) Successful in 3m19s
CI / images (push) Successful in 3m7s
CI / required (push) Successful in 2s
CI / benchmark (pull_request) Failing after 5m28s
CI / security (pull_request) Successful in 6m37s
CI / images (pull_request) Successful in 3m18s
CI / required (pull_request) Successful in 1s
refactor(planning): remove the original decision trace
2026-10-15 20:42:33 +02:00

1014 lines
31 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>
@if (displayedPlan()) {
<button
type="button"
class="statistics-button"
aria-haspopup="dialog"
(click)="openStatistics()"
>
<mat-icon aria-hidden="true">bar_chart</mat-icon>
{{ tr().planning.statistics.open }}
</button>
}
</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>
@for (contribution of plan.timeWindowContributions ?? []; track contribution.ruleId) {
<section class="outcome-explanation declared-rule">
<h3>{{ contribution.label }}</h3>
<p>
{{ contribution.declaredDays.length }} {{ tr().bookedDays.booked }} +
{{ contribution.placementDates.length }}
{{ tr().planning.explanation.leaveDays }} =
<strong
>{{
contribution.declaredDays.length + contribution.placementDates.length
}}
/ {{ contribution.requested }}</strong
>
</p>
<p>
@for (day of contribution.declaredDays; track day.date) {
<span>{{ displayDate(day.date) }} · {{ tr().dayTypes[day.type] }} </span>
}
</p>
<p>{{ tr().bookedDays.fixed }}</p>
</section>
}
@if (plan.calculationDate) {
<p>{{ tr().planning.calculationDate }} : {{ displayDate(plan.calculationDate) }}</p>
}
@for (
contribution of plan.ifThenContributions ?? [];
track contribution.ruleId + contribution.occurrence
) {
<section class="outcome-explanation declared-rule">
<h3>{{ contribution.label }} · {{ displayDate(contribution.occurrence) }}</h3>
<p>
{{ contribution.declaredDays.length }} {{ tr().bookedDays.booked }} +
{{ contribution.placementDates.length }}
{{ tr().planning.explanation.leaveDays }} =
<strong>{{ contribution.requested }} / {{ contribution.requested }}</strong>
</p>
@for (day of contribution.declaredDays; track day.date) {
<span>{{ displayDate(day.date) }} · {{ tr().dayTypes[day.type] }} </span>
}
@if (contribution.declaredDays.length) {
<p>{{ tr().bookedDays.fixed }}</p>
}
@if (contribution.earnedEntitlement; as earned) {
<p>
{{
earned.added
? tr().planning.futureEntitlement
: tr().planning.includedEntitlement
}}
: {{ earned.quantity }} {{ tr().dayTypes[earned.type] }}
</p>
<p>
{{ tr().planning.details.available }}
{{ displayDate(earned.availableFrom) }} ·
{{ tr().planning.details.expires }} {{ displayDate(earned.expiryDate) }}
</p>
}
</section>
}
@for (period of plan.timeOffPeriods; track period.start) {
@if (period.declaredDates?.length) {
<section class="outcome-explanation declared-period">
<h3>
{{ tr().planning.explanation.breakTitle }} ·
{{ displayDate(period.start) }} – {{ displayDate(period.end) }}
</h3>
<p>
{{ period.declaredDates!.length }} {{ tr().bookedDays.booked }} +
{{ period.placementDates.length }}
{{ tr().planning.explanation.leaveDays }} +
{{ period.nonWorkingDates.length }}
{{ tr().planning.explanation.freeDays }} =
<strong
>{{ period.duration }} {{ tr().planning.explanation.daysOff }}</strong
>
</p>
<p>{{ tr().bookedDays.fixed }}</p>
</section>
}
}
}
<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">
@if (period.declaredDates?.length) {
<span
><strong>{{ period.declaredDates!.length }}</strong
>{{ tr().bookedDays.booked }}</span
>
<span>+</span>
}
<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) {
·
{{
translationService.getRemainingBalanceReason(
remaining,
displayedPlan(),
planningOutcome()?.kind === 'PlanningIncomplete'
)
}}
}
</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>
{{
translationService.getRemainingBalanceReason(
remaining,
displayedPlan(),
planningOutcome()?.kind === 'PlanningIncomplete'
)
}}
</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>
}
</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()) {
<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>
}
</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()"
(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"
>
<section class="criteria-detail">
<h4>
{{ tr().planning.details.criteria }}
</h4>
<p class="calculation-intro">
{{ 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]="
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 (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 (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 #planStatisticsDialog>
<h2 mat-dialog-title id="plan-statistics-title">{{ tr().planning.statistics.title }}</h2>
<mat-dialog-content class="statistics-content">
@if (displayedPlan(); as plan) {
@if (planningOutcome()?.kind === 'PlanningIncomplete') {
<p class="incomplete-incumbent">{{ tr().planning.statistics.partialPlan }}</p>
}
<p id="plan-statistics-help">{{ tr().planning.statistics.help }}</p>
<dl class="plan-summary statistics-summary">
<div>
<dt>{{ tr().planning.statistics.leaveDays }}</dt>
<dd>{{ plan.placements.length }}</dd>
</div>
<div>
<dt>
{{ tr().planning.daysGained }} ({{ tr().planning.statistics.calendarDays }})
</dt>
<dd>{{ formatStatistic(plan.metrics.nonWorkingDayGain) }}</dd>
</div>
<div>
<dt>
{{ tr().planning.statistics.averageWorkSequence }} ({{
tr().planning.statistics.workingDays
}})
</dt>
<dd>{{ formatStatistic(plan.metrics.averageWorkSequence) }}</dd>
</div>
<div>
<dt>
{{ tr().planning.longestWorkSequence }} ({{
tr().planning.statistics.workingDays
}})
</dt>
<dd>{{ formatStatistic(plan.metrics.longestWorkSequence) }}</dd>
</div>
<div>
<dt>
{{ tr().planning.statistics.averageTimeOffPeriod }} ({{
tr().planning.statistics.calendarDays
}})
</dt>
<dd>{{ formatStatistic(plan.metrics.averageTimeOffPeriod) }}</dd>
</div>
<div>
<dt>
{{ tr().planning.statistics.longestTimeOffPeriod }} ({{
tr().planning.statistics.calendarDays
}})
</dt>
<dd>{{ formatStatistic(plan.metrics.longestTimeOffPeriod) }}</dd>
</div>
<div>
<dt>
{{ tr().planning.statistics.weekendConnectedHolidays }} ({{
tr().planning.statistics.calendarDays
}})
</dt>
<dd>{{ formatStatistic(weekendConnectedHolidays()) }}</dd>
</div>
<div>
<dt>
{{ tr().planning.statistics.earnedLeave }} ({{
tr().planning.statistics.leaveDaysUnit
}})
</dt>
<dd>{{ formatStatistic(earnedLeave().total) }}</dd>
</div>
</dl>
@if (earnedLeave().contributions.length) {
<section aria-labelledby="earned-leave-title">
<h3 id="earned-leave-title">
{{ tr().planning.statistics.earnedLeaveByRule }} ({{
tr().planning.statistics.leaveDaysUnit
}})
</h3>
<dl class="earned-leave-breakdown">
@for (
contribution of earnedLeave().contributions;
track contribution.ruleId
) {
<div>
<dt>{{ contribution.label }}</dt>
<dd>{{ formatStatistic(contribution.quantity) }}</dd>
</div>
}
</dl>
</section>
}
}
</mat-dialog-content>
<mat-dialog-actions align="end">
<button type="button" mat-dialog-close>{{ tr().settings.close }}</button>
</mat-dialog-actions>
</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">
{{ 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>{{ 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 (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">
{{ tr().planning.explanation.currentPlan }}
</th>
<th scope="col">
{{ 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">
@if (dateComparison(); as comparison) {
<ng-container
[ngTemplateOutlet]="calculationTable"
[ngTemplateOutletContext]="{
comparison,
beforeLabel: tr().planning.explanation.currentPlan,
afterLabel: tr().planning.explanation.alternative,
audit: result,
}"
/>
} @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
: tr().planning.comparison.backToPlan
}}
</button>
</footer>
</section>
</ng-template>