fix(planning): complete explanation and trace behavior (#46)
This commit is contained in:
1 parent
57a39c81ce
commit
c9dbe82b79
10 files changed
+925
-140
No files matched your search
@@ -6,6 +6,7 @@ import { CalendarMonth, CalendarDayStatus } from '../calendar-month/calendar-mon
|
||||
import { CalendarSettings } from '../calendar-settings/calendar-settings';
|
||||
import {
|
||||
CalendarService,
|
||||
DayType,
|
||||
SelectedDates,
|
||||
SelectableDayType,
|
||||
VacDay,
|
||||
@@ -61,7 +62,7 @@ export class CalendarYear {
|
||||
protected readonly traceLoading = signal(false);
|
||||
protected readonly traceResolved = signal(false);
|
||||
protected readonly selectedPlacementIndex = signal(0);
|
||||
protected readonly planDetailsOpen = signal(false);
|
||||
protected readonly planDetailsOpen = signal(true);
|
||||
protected readonly counterfactual = signal<CounterfactualAnalysis | null>(null);
|
||||
protected readonly selectedCounterfactualDate = signal<string | null>(null);
|
||||
protected readonly counterfactualLoading = signal(false);
|
||||
@@ -71,6 +72,7 @@ export class CalendarYear {
|
||||
protected readonly traceCandidatePage = signal(0);
|
||||
protected readonly traceMoment = signal<TraceMoment>('before');
|
||||
protected readonly traceReplay = signal(false);
|
||||
protected readonly selectedTraceDate = signal<string | null>(null);
|
||||
protected readonly liveMessage = signal('');
|
||||
protected readonly planningHorizon = signal<{ start: string; end: string } | null>(null);
|
||||
protected readonly selectedDates = signal(new SelectedDates());
|
||||
@@ -146,14 +148,23 @@ export class CalendarYear {
|
||||
protected readonly currentPlacementDate = computed(() => {
|
||||
if (this.counterfactual())
|
||||
return this.selectedCounterfactualDate() ?? this.counterfactual()!.placement.date;
|
||||
if (this.traceReplay()) return this.selectedTraceDecision()?.dates[0] ?? null;
|
||||
if (this.traceReplay())
|
||||
return this.selectedTraceDate() ?? this.selectedTraceDecision()?.dates[0] ?? null;
|
||||
return this.planDetailsOpen() ? (this.selectedExplanation()?.placement.date ?? null) : null;
|
||||
});
|
||||
|
||||
protected readonly dateStatuses = computed<Readonly<Record<string, CalendarDayStatus>>>(() => {
|
||||
const statuses: Record<string, CalendarDayStatus> = {};
|
||||
const trace = this.planningTrace();
|
||||
if (!this.counterfactual() && this.traceReplay() && trace) {
|
||||
const closedDayDates = new Set<string>();
|
||||
for (const selected of this.selectedDates().datesSelected) {
|
||||
if (selected.type !== DayType.CLOSED_DAY || !selected.range.start) continue;
|
||||
const day = selected.range.start;
|
||||
closedDayDates.add(
|
||||
`${day.getFullYear()}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')}`,
|
||||
);
|
||||
}
|
||||
if (this.traceReplay() && trace) {
|
||||
const priority = new Map([
|
||||
['ineligible', 0],
|
||||
['not-examined', 1],
|
||||
@@ -173,16 +184,13 @@ export class CalendarYear {
|
||||
label: this.traceStatusLabel('not-examined'),
|
||||
};
|
||||
}
|
||||
for (const candidate of trace.phases.flatMap((phase) =>
|
||||
phase.decisions.flatMap((decision) => decision.candidates),
|
||||
)) {
|
||||
if (candidate.status !== 'ineligible' || candidate.reason?.code !== 'non-working')
|
||||
continue;
|
||||
for (const date of candidate.dates ?? (candidate.date ? [candidate.date] : []))
|
||||
for (const date of Object.keys(statuses)) {
|
||||
const reason = this.nonWorkingDateReason(date, closedDayDates);
|
||||
if (reason)
|
||||
statuses[date] = {
|
||||
kind: candidate.status,
|
||||
label: this.traceStatusLabel(candidate.status),
|
||||
reason: candidate.reason.message,
|
||||
kind: 'ineligible',
|
||||
label: this.traceStatusLabel('ineligible'),
|
||||
reason,
|
||||
};
|
||||
}
|
||||
const explicitPriorities = new Map<string, number>();
|
||||
@@ -234,20 +242,39 @@ export class CalendarYear {
|
||||
if (!changedDates.has(placement.date)) setChange(placement.date, 'unchanged');
|
||||
}
|
||||
}
|
||||
for (const result of this.counterfactual()?.results ?? []) {
|
||||
const status = statuses[result.date];
|
||||
const historicalStatus =
|
||||
result.historicalStatus ??
|
||||
(status && status.kind !== 'eligible' ? status.kind : 'not-examined');
|
||||
statuses[result.date] = {
|
||||
...status,
|
||||
kind: historicalStatus,
|
||||
label: this.traceStatusLabel(historicalStatus),
|
||||
hypotheticalLabel:
|
||||
result.status === 'eligible'
|
||||
? this.tr().planning.eligible
|
||||
: this.tr().planning.ineligible,
|
||||
hypotheticalReason: result.reason?.message,
|
||||
};
|
||||
}
|
||||
return statuses;
|
||||
}
|
||||
for (const result of this.counterfactual()?.results ?? [])
|
||||
for (const result of this.counterfactual()?.results ?? []) {
|
||||
const nonWorkingReason = this.nonWorkingDateReason(result.date, closedDayDates);
|
||||
const historicalStatus =
|
||||
result.historicalStatus ?? (nonWorkingReason ? 'ineligible' : 'not-examined');
|
||||
statuses[result.date] = {
|
||||
kind: result.status,
|
||||
label:
|
||||
kind: historicalStatus,
|
||||
label: this.traceStatusLabel(historicalStatus),
|
||||
reason: historicalStatus === 'ineligible' ? nonWorkingReason : undefined,
|
||||
hypotheticalLabel:
|
||||
result.status === 'eligible'
|
||||
? this.tr().planning.eligible
|
||||
: this.tr().planning.ineligible,
|
||||
reason: result.reason?.message,
|
||||
historicalLabel: result.historicalStatus
|
||||
? this.traceStatusLabel(result.historicalStatus)
|
||||
: undefined,
|
||||
hypotheticalReason: result.reason?.message,
|
||||
};
|
||||
}
|
||||
return statuses;
|
||||
});
|
||||
|
||||
@@ -357,13 +384,14 @@ export class CalendarYear {
|
||||
this.traceLoading.set(true);
|
||||
this.traceResolved.set(false);
|
||||
this.selectedPlacementIndex.set(0);
|
||||
this.planDetailsOpen.set(false);
|
||||
this.planDetailsOpen.set(true);
|
||||
this.clearCounterfactual();
|
||||
this.tracePhaseIndex.set(0);
|
||||
this.traceDecisionIndex.set(0);
|
||||
this.traceCandidatePage.set(0);
|
||||
this.traceMoment.set('before');
|
||||
this.traceReplay.set(false);
|
||||
this.selectedTraceDate.set(null);
|
||||
this.selectedDates.set(this.calendarService.getSelectedDatesForYear());
|
||||
this.userInput.remainingVacationDaysSignal.set([]);
|
||||
}
|
||||
@@ -372,6 +400,8 @@ export class CalendarYear {
|
||||
void calculation.explanation.then((explanation) => {
|
||||
if (version !== this.calculationVersion) return;
|
||||
this.planningExplanation.set(explanation ?? null);
|
||||
if (explanation?.kind === 'PlanExplanation' && this.planDetailsOpen())
|
||||
this.selectPlacement(0);
|
||||
});
|
||||
void calculation.explanationError.then((reason) => {
|
||||
if (version === this.calculationVersion) this.explanationError.set(reason ?? null);
|
||||
@@ -435,6 +465,7 @@ export class CalendarYear {
|
||||
|
||||
protected togglePlanDetails(event: Event): void {
|
||||
const open = (event.currentTarget as HTMLDetailsElement).open;
|
||||
if (open === this.planDetailsOpen()) return;
|
||||
this.planDetailsOpen.set(open);
|
||||
if (open) this.selectPlacement(this.selectedPlacementIndex());
|
||||
else this.clearCounterfactual();
|
||||
@@ -464,12 +495,36 @@ export class CalendarYear {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.traceReplay()) {
|
||||
this.selectedTraceDate.set(date);
|
||||
if (this.counterfactual()?.results.some((result) => result.date === date))
|
||||
this.selectedCounterfactualDate.set(date);
|
||||
this.liveMessage.set(this.dateStatusAnnouncement(date, this.dateStatuses()[date]));
|
||||
return;
|
||||
}
|
||||
if (this.counterfactual()?.results.some((result) => result.date === date)) {
|
||||
this.selectedCounterfactualDate.set(date);
|
||||
this.liveMessage.set(`${date}: ${this.dateStatuses()[date]?.label ?? ''}`);
|
||||
this.liveMessage.set(this.dateStatusAnnouncement(date, this.dateStatuses()[date]));
|
||||
}
|
||||
}
|
||||
|
||||
private dateStatusAnnouncement(date: string, status?: CalendarDayStatus): string {
|
||||
return [
|
||||
date,
|
||||
status?.label,
|
||||
status?.reason,
|
||||
status?.hypotheticalLabel
|
||||
? `${this.tr().planning.counterfactual}: ${status.hypotheticalLabel}`
|
||||
: undefined,
|
||||
status?.hypotheticalReason
|
||||
? `${this.tr().planning.reason}: ${status.hypotheticalReason}`
|
||||
: undefined,
|
||||
status?.changeLabel,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('. ');
|
||||
}
|
||||
|
||||
protected selectTracePhase(index: number): void {
|
||||
const last = (this.planningTrace()?.phases.length ?? 1) - 1;
|
||||
index = Math.max(0, Math.min(index, last));
|
||||
@@ -478,6 +533,7 @@ export class CalendarYear {
|
||||
this.traceCandidatePage.set(0);
|
||||
this.traceMoment.set('before');
|
||||
this.traceReplay.set(false);
|
||||
this.selectedTraceDate.set(null);
|
||||
this.clearCounterfactual();
|
||||
this.liveMessage.set(
|
||||
`${this.tr().planning.phases} ${index + 1} ${this.tr().planning.of} ${this.planningTrace()?.phases.length ?? 0}`,
|
||||
@@ -505,6 +561,7 @@ export class CalendarYear {
|
||||
this.traceCandidatePage.set(0);
|
||||
this.traceMoment.set('before');
|
||||
this.traceReplay.set(true);
|
||||
this.selectedTraceDate.set(this.selectedTraceDecision()?.dates[0] ?? null);
|
||||
this.clearCounterfactual();
|
||||
this.liveMessage.set(
|
||||
`${this.tr().planning.decision} ${index + 1} ${this.tr().planning.of} ${this.selectedTracePhase()?.decisions.length ?? 0}`,
|
||||
@@ -535,11 +592,15 @@ export class CalendarYear {
|
||||
this.clearCounterfactual();
|
||||
this.traceMoment.set(moment);
|
||||
this.traceReplay.set(true);
|
||||
this.liveMessage.set(
|
||||
`${this.tr().planning[moment]} ${this.tr().planning.decision} ${this.traceDecisionIndex() + 1} ${this.tr().planning.of} ${this.selectedTracePhase()?.decisions.length ?? 0}`,
|
||||
);
|
||||
}
|
||||
|
||||
protected showFinalPlan(): void {
|
||||
this.traceReplay.set(false);
|
||||
this.clearCounterfactual();
|
||||
this.liveMessage.set(this.tr().planning.finalPlan);
|
||||
}
|
||||
|
||||
protected analyzeTraceCandidate(candidate: PlanningTraceCandidate): void {
|
||||
@@ -547,9 +608,20 @@ export class CalendarYear {
|
||||
if (!candidate.placement || !decision) return;
|
||||
this.traceMoment.set('before');
|
||||
this.traceReplay.set(true);
|
||||
this.selectedTraceDate.set(candidate.placement.date);
|
||||
void this.analyzeSelection({ decisionId: decision.id, placement: candidate.placement });
|
||||
}
|
||||
|
||||
private nonWorkingDateReason(
|
||||
date: string,
|
||||
closedDayDates: ReadonlySet<string>,
|
||||
): string | undefined {
|
||||
const day = new Date(`${date}T00:00:00`);
|
||||
if (day.getDay() === 0 || day.getDay() === 6) return this.tr().dayTypes.weekend;
|
||||
if (closedDayDates.has(date)) return this.tr().dayTypes.closedDay;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async analyzeSelection(selection: CounterfactualSelection): Promise<void> {
|
||||
const calculation = this.activeCalculation;
|
||||
if (!calculation) return;
|
||||
|
||||
Reference in new issue
Block a user