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
1014 lines
31 KiB
HTML
1014 lines
31 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>
|
||
@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>
|