feat(planning): complete verified explanations and trace delivery (#46)
Implement plan explanations, original decision traces, calendar replay, and worker counterfactual analysis. Preserve incomplete checkpoints and conflict reasons, with accessible navigation and localized comparison details. Keep planning outcomes independent of tracing and bound worker delivery with receiver acknowledgements. Retain complete candidate history while releasing temporary buffers and benchmark references. Validated with 201 tests, app/spec type checks, lint, production build, baseline outcome comparisons, and repeated loaded browser measurements. Document performance and remaining limits.
This commit is contained in:
1 parent
0ce33cc9d6
commit
01c87d1e44
20 files changed
+7625
-1001
No files matched your search
@@ -1,5 +1,7 @@
|
||||
<div class="calendar-year">
|
||||
<p class="sr-only" aria-live="polite" aria-atomic="true">{{ liveMessage() }}</p>
|
||||
<p class="sr-only" role="status" aria-live="polite" aria-atomic="true">
|
||||
{{ liveMessage() }}
|
||||
</p>
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="loading-overlay">
|
||||
@@ -12,7 +14,7 @@
|
||||
></mat-spinner>
|
||||
<div class="spinner-text">{{ loadingProgress() }}%</div>
|
||||
</div>
|
||||
<p>{{ tr().loading }}</p>
|
||||
<p role="status">{{ tr().loading }}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -170,7 +172,7 @@
|
||||
<button
|
||||
type="button"
|
||||
data-step="first"
|
||||
[disabled]="selectedPlacementIndex() === 0"
|
||||
[attr.aria-disabled]="selectedPlacementIndex() === 0"
|
||||
[attr.aria-label]="tr().planning.first"
|
||||
(click)="stepPlacement('first')"
|
||||
>
|
||||
@@ -179,20 +181,20 @@
|
||||
<button
|
||||
type="button"
|
||||
data-step="previous"
|
||||
[disabled]="selectedPlacementIndex() === 0"
|
||||
[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>
|
||||
<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"
|
||||
[disabled]="
|
||||
[attr.aria-disabled]="
|
||||
selectedPlacementIndex() ===
|
||||
planExplanation()!.placements.length - 1
|
||||
"
|
||||
@@ -204,7 +206,7 @@
|
||||
<button
|
||||
type="button"
|
||||
data-step="last"
|
||||
[disabled]="
|
||||
[attr.aria-disabled]="
|
||||
selectedPlacementIndex() ===
|
||||
planExplanation()!.placements.length - 1
|
||||
"
|
||||
@@ -283,7 +285,32 @@
|
||||
}
|
||||
</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>
|
||||
@@ -302,9 +329,14 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (change of placementChanges(); track change.id) {
|
||||
<tr>
|
||||
<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>
|
||||
@@ -358,9 +390,6 @@
|
||||
}}
|
||||
</p>
|
||||
}
|
||||
@if (selected.supporting) {
|
||||
<p>{{ tr().planning.details.supporting }}</p>
|
||||
}
|
||||
</section>
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="calculationTable"
|
||||
@@ -376,7 +405,11 @@
|
||||
|
||||
<button type="button" class="compare-dates-button" (click)="compareDates()">
|
||||
<mat-icon aria-hidden="true">compare_arrows</mat-icon
|
||||
>{{ tr().planning.compareDates }}
|
||||
>{{
|
||||
counterfactualError()
|
||||
? tr().planning.retryCounterfactual
|
||||
: tr().planning.compareDates
|
||||
}}
|
||||
</button>
|
||||
</section>
|
||||
</details>
|
||||
@@ -408,7 +441,7 @@
|
||||
<button
|
||||
type="button"
|
||||
data-phase-step="first"
|
||||
[disabled]="tracePhaseIndex() === 0"
|
||||
[attr.aria-disabled]="tracePhaseIndex() === 0"
|
||||
[attr.aria-label]="tr().planning.firstPhase"
|
||||
(click)="stepTracePhase('first')"
|
||||
>
|
||||
@@ -417,20 +450,24 @@
|
||||
<button
|
||||
type="button"
|
||||
data-phase-step="previous"
|
||||
[disabled]="tracePhaseIndex() === 0"
|
||||
[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"
|
||||
[disabled]="tracePhaseIndex() === trace.phases.length - 1"
|
||||
[attr.aria-disabled]="
|
||||
tracePhaseIndex() === trace.phases.length - 1
|
||||
"
|
||||
[attr.aria-label]="tr().planning.nextPhase"
|
||||
(click)="stepTracePhase('next')"
|
||||
>
|
||||
@@ -439,7 +476,9 @@
|
||||
<button
|
||||
type="button"
|
||||
data-phase-step="last"
|
||||
[disabled]="tracePhaseIndex() === trace.phases.length - 1"
|
||||
[attr.aria-disabled]="
|
||||
tracePhaseIndex() === trace.phases.length - 1
|
||||
"
|
||||
[attr.aria-label]="tr().planning.lastPhase"
|
||||
(click)="stepTracePhase('last')"
|
||||
>
|
||||
@@ -466,7 +505,7 @@
|
||||
<nav [attr.aria-label]="tr().planning.decisions">
|
||||
<button
|
||||
type="button"
|
||||
[disabled]="traceDecisionIndex() === 0"
|
||||
[attr.aria-disabled]="traceDecisionIndex() === 0"
|
||||
[attr.aria-label]="tr().planning.firstDecision"
|
||||
(click)="stepTraceDecision('first')"
|
||||
>
|
||||
@@ -474,20 +513,22 @@
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
[disabled]="traceDecisionIndex() === 0"
|
||||
[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"
|
||||
[disabled]="
|
||||
[attr.aria-disabled]="
|
||||
traceDecisionIndex() === phase.decisions.length - 1
|
||||
"
|
||||
[attr.aria-label]="tr().planning.nextDecision"
|
||||
@@ -497,7 +538,7 @@
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
[disabled]="
|
||||
[attr.aria-disabled]="
|
||||
traceDecisionIndex() === phase.decisions.length - 1
|
||||
"
|
||||
[attr.aria-label]="tr().planning.lastDecision"
|
||||
@@ -539,6 +580,23 @@
|
||||
{{ 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>
|
||||
{{
|
||||
@@ -562,13 +620,17 @@
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
[disabled]="traceCandidatePage() === 0"
|
||||
[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 }}
|
||||
@@ -578,14 +640,16 @@
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
[disabled]="
|
||||
[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>
|
||||
}
|
||||
@@ -624,6 +688,100 @@
|
||||
@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
|
||||
@@ -636,6 +794,10 @@
|
||||
tr().planning.before,
|
||||
afterLabel: tr().planning.after,
|
||||
audit: comparison,
|
||||
traceEffect:
|
||||
!!candidate
|
||||
.technicalComparison
|
||||
?.length,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
@@ -687,6 +849,21 @@
|
||||
{{ 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>
|
||||
}
|
||||
}
|
||||
@@ -697,7 +874,7 @@
|
||||
}
|
||||
</div>
|
||||
} @else if (traceResolved()) {
|
||||
<p>{{ tr().planning.traceUnavailable }}</p>
|
||||
<p role="status">{{ tr().planning.traceUnavailable }}</p>
|
||||
}
|
||||
</details>
|
||||
}
|
||||
@@ -711,6 +888,16 @@
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (counterfactualLoading()) {
|
||||
<p class="counterfactual-progress" aria-hidden="true">
|
||||
{{ tr().planning.counterfactualLoading }}
|
||||
</p>
|
||||
}
|
||||
@if (counterfactualError()) {
|
||||
<div class="counterfactual-inline-error" role="alert">
|
||||
<p>{{ tr().planning.counterfactualError }} {{ counterfactualError() }}</p>
|
||||
</div>
|
||||
}
|
||||
<p class="calendar-help">
|
||||
<mat-icon aria-hidden="true">touch_app</mat-icon>{{ tr().planning.calendarHelp }}
|
||||
</p>
|
||||
@@ -828,10 +1015,23 @@
|
||||
let-beforeLabel="beforeLabel"
|
||||
let-afterLabel="afterLabel"
|
||||
let-audit="audit"
|
||||
let-traceEffect="traceEffect"
|
||||
>
|
||||
<section class="criteria-detail">
|
||||
<h4>{{ tr().planning.details.criteria }}</h4>
|
||||
<p class="calculation-intro">{{ tr().planning.details.criteriaHelp }}</p>
|
||||
<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>
|
||||
@@ -849,7 +1049,7 @@
|
||||
@if (objective.numeric) {
|
||||
<tr
|
||||
[class.decisive-row]="
|
||||
objective.id === comparison.firstDecisiveObjective
|
||||
!traceEffect && objective.id === comparison.firstDecisiveObjective
|
||||
"
|
||||
>
|
||||
<th scope="row">
|
||||
@@ -862,7 +1062,10 @@
|
||||
? tr().planning.details.higher
|
||||
: tr().planning.details.lower
|
||||
}}</small>
|
||||
@if (objective.id === comparison.firstDecisiveObjective) {
|
||||
@if (
|
||||
!traceEffect &&
|
||||
objective.id === comparison.firstDecisiveObjective
|
||||
) {
|
||||
<span class="decisive-label">{{
|
||||
tr().planning.details.firstDifference
|
||||
}}</span>
|
||||
@@ -886,7 +1089,10 @@
|
||||
@if (!objective.numeric) {
|
||||
<li>
|
||||
{{ objective.label }}
|
||||
@if (objective.id === comparison.firstDecisiveObjective) {
|
||||
@if (
|
||||
!traceEffect &&
|
||||
objective.id === comparison.firstDecisiveObjective
|
||||
) {
|
||||
<span class="decisive-label">{{
|
||||
tr().planning.details.firstDifference
|
||||
}}</span>
|
||||
@@ -940,7 +1146,9 @@
|
||||
<p>{{ tr().planning.counterfactualLoading }}</p>
|
||||
</div>
|
||||
} @else if (counterfactualError()) {
|
||||
<p class="inline-error" role="alert">{{ tr().planning.counterfactualError }}</p>
|
||||
<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">
|
||||
@@ -1106,6 +1314,12 @@
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
@if (dateStatuses()[result.date]?.reason; as historicalReason) {
|
||||
<p>
|
||||
<strong>{{ tr().planning.reason }}:</strong>
|
||||
{{ historicalReason }}
|
||||
</p>
|
||||
}
|
||||
@if (dateComparison(); as comparison) {
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="calculationTable"
|
||||
@@ -1118,6 +1332,7 @@
|
||||
? tr().planning.explanation.withCandidate
|
||||
: tr().planning.explanation.alternative,
|
||||
audit: result,
|
||||
traceEffect: traceReplay(),
|
||||
}"
|
||||
/>
|
||||
} @else {
|
||||
@@ -1136,11 +1351,17 @@
|
||||
</mat-dialog-content>
|
||||
<footer class="comparison-footer">
|
||||
<span>{{ tr().planning.comparison.preview }}</span>
|
||||
<button type="button" class="comparison-done" (click)="clearCounterfactual()">
|
||||
<button
|
||||
type="button"
|
||||
class="comparison-done"
|
||||
(click)="counterfactualError() ? retryCounterfactual() : clearCounterfactual()"
|
||||
>
|
||||
{{
|
||||
traceReplay()
|
||||
? tr().planning.comparison.backToTrace
|
||||
: tr().planning.comparison.backToPlan
|
||||
counterfactualError()
|
||||
? tr().planning.retryCounterfactual
|
||||
: traceReplay()
|
||||
? tr().planning.comparison.backToTrace
|
||||
: tr().planning.comparison.backToPlan
|
||||
}}
|
||||
</button>
|
||||
</footer>
|
||||
|
||||
Reference in new issue
Block a user