feat(calendar): add explained period feature and update translations

This commit is contained in:
nessar committed 2026-10-03 17:40:42 +02:00
1 parent 01c87d1e44
commit dd16db8c87
12 files changed
+134 -123

No files matched your search

@@ -38,6 +38,7 @@
[class.end]="selectedDates.isEnd(day)" [class.end]="selectedDates.isEnd(day)"
[class.selected]="selectedDates.isSelected(day)" [class.selected]="selectedDates.isSelected(day)"
[class.current]="dateKey === currentDate" [class.current]="dateKey === currentDate"
[class.explained-period]="isInExplainedPeriod(day)"
[class.context-only]="!isInHorizon(day)" [class.context-only]="!isInHorizon(day)"
[class.eligible]="status?.kind === 'eligible'" [class.eligible]="status?.kind === 'eligible'"
[class.ineligible]="status?.kind === 'ineligible'" [class.ineligible]="status?.kind === 'ineligible'"
@@ -111,8 +111,12 @@
} }
.current { .current {
outline: 3px double var(--today-border-color, #1976d2); outline: 2px solid var(--today-border-color);
outline-offset: 1px; outline-offset: -2px;
}
.explained-period {
box-shadow: inset 0 -3px var(--today-border-color);
} }
.eligible, .eligible,
@@ -68,6 +68,9 @@ export class CalendarMonth implements OnInit {
@Input() @Input()
currentDate: string | null = null; currentDate: string | null = null;
@Input()
explainedPeriod?: { start: string; end: string };
@Input() @Input()
dateStatuses: Readonly<Record<string, CalendarDayStatus>> = {}; dateStatuses: Readonly<Record<string, CalendarDayStatus>> = {};
@@ -94,6 +97,15 @@ export class CalendarMonth implements OnInit {
return this.dateStatuses[this.getDateKey(day)]; return this.dateStatuses[this.getDateKey(day)];
} }
isInExplainedPeriod(day: Date): boolean {
const date = this.getDateKey(day);
return (
!!this.explainedPeriod &&
date >= this.explainedPeriod.start &&
date <= this.explainedPeriod.end
);
}
getStatusMarker(day: Date): string { getStatusMarker(day: Date): string {
switch (this.getStatus(day)?.change) { switch (this.getStatus(day)?.change) {
case 'added': case 'added':
@@ -145,6 +157,10 @@ export class CalendarMonth implements OnInit {
} }
const parts = [this.dayFormatter.format(day)]; const parts = [this.dayFormatter.format(day)];
if (tooltip) parts.push(tooltip); if (tooltip) parts.push(tooltip);
if (this.getDateKey(day) === this.currentDate)
parts.push(this.translationService.tr().planning.currentPlacement);
if (this.isInExplainedPeriod(day))
parts.push(this.translationService.tr().planning.explainedPeriod);
if (!this.isInHorizon(day)) if (!this.isInHorizon(day))
parts.push(this.translationService.tr().planning.outsideHorizon); parts.push(this.translationService.tr().planning.outsideHorizon);
return parts.join('. '); return parts.join('. ');
@@ -498,6 +498,9 @@ describe('CalendarYear planning outcome boundary', () => {
)!; )!;
expect(contextDate.classList).toContain('context-only'); expect(contextDate.classList).toContain('context-only');
expect(contextDate.disabled).toBe(true); expect(contextDate.disabled).toBe(true);
expect(contextDate.classList).toContain('explained-period');
expect(contextDate.getAttribute('aria-label')).toContain('Explained time-off period');
expect(root.querySelectorAll('button.explained-period')).toHaveLength(4);
expect(contextDate.getAttribute('aria-label')).toContain( expect(contextDate.getAttribute('aria-label')).toContain(
'Context date outside the planning horizon', 'Context date outside the planning horizon',
); );
@@ -614,6 +617,12 @@ describe('CalendarYear planning outcome boundary', () => {
); );
expect(root.querySelectorAll('button[data-date="2024-05-03"].selected')).toHaveLength(1); expect(root.querySelectorAll('button[data-date="2024-05-03"].selected')).toHaveLength(1);
expect(root.querySelectorAll('button[data-date="2024-05-06"].selected')).toHaveLength(1); expect(root.querySelectorAll('button[data-date="2024-05-06"].selected')).toHaveLength(1);
expect(
Array.from(
root.querySelectorAll<HTMLButtonElement>('button.explained-period'),
(button) => button.dataset['date'],
),
).toEqual(['2024-05-03', '2024-05-04', '2024-05-05', '2024-05-06']);
expect(root.querySelector('.technical-details')?.textContent).toContain( expect(root.querySelector('.technical-details')?.textContent).toContain(
'The planner’s priorities', 'The planner’s priorities',
); );
@@ -635,8 +644,9 @@ describe('CalendarYear planning outcome boundary', () => {
await fixture.whenStable(); await fixture.whenStable();
fixture.detectChanges(); fixture.detectChanges();
expect(document.activeElement).toBe(next); expect(document.activeElement).toBe(next);
expect(calculation.analyzeCounterfactual).toHaveBeenCalledWith(plan.placements[1]); expect(calculation.analyzeCounterfactual).not.toHaveBeenCalled();
expect(root.querySelector('[role="dialog"]')).toBeNull(); expect(root.querySelector('[role="dialog"]')).toBeNull();
expect(root.querySelectorAll('button.explained-period')).toHaveLength(0);
const firstButton = root.querySelector<HTMLButtonElement>('button[data-step="first"]')!; const firstButton = root.querySelector<HTMLButtonElement>('button[data-step="first"]')!;
activateWithKeyboard(firstButton); activateWithKeyboard(firstButton);
@@ -702,33 +712,8 @@ describe('CalendarYear planning outcome boundary', () => {
expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(2); expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(2);
}); });
it('shows the initial placement and starts alternatives on selection without opening the dialog', async () => { it('keeps explanation selection quiet and analyzes alternatives only from Compare dates', async () => {
const { calendar, calculation } = calendarFor(plan, explanation); const { calendar, calculation } = calendarFor(plan, explanation);
calendar.selectedDatesForPlan.mockImplementation((value) =>
selectedDatesForPlan(value, true),
);
const counterfactualWithoutHistoricalStatus: CounterfactualAnalysis = {
kind: 'Counterfactual',
calculationId: 'test',
placement: plan.placements[0],
hypothetical: true,
baseline: objectiveVector,
results: [
{ date: '2024-05-03', status: 'eligible' },
{
date: '2024-05-04',
status: 'ineligible',
},
{
date: '2024-05-08',
status: 'ineligible',
},
],
};
vi.spyOn(calculation, 'analyzeCounterfactual').mockImplementation(async (selection) => ({
...counterfactualWithoutHistoricalStatus,
placement: placementForSelection(selection),
}));
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CalendarYear], imports: [CalendarYear],
providers: [ providers: [
@@ -742,77 +727,65 @@ describe('CalendarYear planning outcome boundary', () => {
await new Promise<void>((resolve) => setTimeout(resolve, 0)); await new Promise<void>((resolve) => setTimeout(resolve, 0));
fixture.detectChanges(); fixture.detectChanges();
const details = fixture.nativeElement.querySelector('.plan-details') as HTMLDetailsElement; const root = fixture.nativeElement as HTMLElement;
const details = root.querySelector<HTMLDetailsElement>('.plan-details')!;
expect(details.open).toBe(true); expect(details.open).toBe(true);
expect(calculation.analyzeCounterfactual).not.toHaveBeenCalled(); expect(root.querySelector<HTMLButtonElement>('button.current')?.dataset['date']).toBe(
expect(fixture.nativeElement.querySelector('button.current')?.dataset.date).toBe(
'2024-05-03', '2024-05-03',
); );
const placement = fixture.nativeElement.querySelector(
'button[data-date="2024-05-03"]',
) as HTMLButtonElement;
placement.click();
fixture.detectChanges();
expect(details.open).toBe(true);
expect(calculation.analyzeCounterfactual).toHaveBeenCalledWith(plan.placements[0]);
expect(fixture.nativeElement.querySelector('[role="dialog"]')).toBeNull();
expect(fixture.nativeElement.querySelector('.technical-details').open).toBe(false);
const compare = fixture.nativeElement.querySelector(
'.compare-dates-button',
) as HTMLButtonElement;
compare.click();
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(3);
expect(fixture.nativeElement.querySelector('button.current')?.dataset.date).toBe(
'2024-05-03',
);
expect(calculation.analyzeCounterfactual).toHaveBeenCalledWith(plan.placements[0]);
expect(fixture.nativeElement.textContent).toContain('Paid leave');
expect(fixture.componentInstance['dateStatuses']()['2024-05-04']).toMatchObject({
kind: 'ineligible',
label: 'Ineligible',
reason: 'Weekend',
});
expect(fixture.componentInstance['dateStatuses']()['2024-05-08']).toMatchObject({
kind: 'ineligible',
label: 'Ineligible',
reason: 'Public Holiday',
});
const weekend = fixture.nativeElement.querySelector(
'button[data-date="2024-05-04"]',
) as HTMLButtonElement;
expect(weekend.getAttribute('aria-label')).toContain('Ineligible');
expect(weekend.getAttribute('aria-label')).toContain('Weekend');
expect(weekend.querySelector('.status-marker')?.textContent).toBe('×');
expect(weekend.querySelector('.hypothetical-marker')?.textContent).toBe('~×');
const holidayButton = fixture.nativeElement.querySelector(
'button[data-date="2024-05-08"]',
) as HTMLButtonElement;
expect(holidayButton.getAttribute('aria-label')).toContain('Ineligible');
expect(holidayButton.getAttribute('aria-label')).toContain('Public Holiday');
expect(holidayButton.querySelector('.status-marker')?.textContent).toBe('×');
expect(holidayButton.querySelector('.hypothetical-marker')?.textContent).toBe('~×');
const next = fixture.nativeElement.querySelector(
'button[data-step="next"]',
) as HTMLButtonElement;
next.click();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('button.current')?.dataset.date).toBe(
'2024-05-06',
);
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('button.current')?.dataset.date).toBe(
'2024-05-06',
);
details.open = false; details.open = false;
details.dispatchEvent(new Event('toggle')); details.dispatchEvent(new Event('toggle'));
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.nativeElement.querySelector('button.current')?.dataset.date).toBe( expect(root.querySelector('button.current')).toBeNull();
details.open = true;
details.dispatchEvent(new Event('toggle'));
fixture.detectChanges();
root.querySelector<HTMLButtonElement>('button[data-date="2024-05-06"]')!.click();
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
expect(calculation.analyzeCounterfactual).not.toHaveBeenCalled();
expect(root.querySelector<HTMLButtonElement>('button.current')?.dataset['date']).toBe(
'2024-05-06', '2024-05-06',
); );
expect(root.querySelectorAll('.status-marker, .hypothetical-marker')).toHaveLength(0);
root.querySelector<HTMLButtonElement>('.compare-dates-button')!.click();
await fixture.whenStable();
fixture.detectChanges();
expect(calculation.analyzeCounterfactual).toHaveBeenCalledExactlyOnceWith(
plan.placements[1],
);
expect(document.querySelector('[role="dialog"]')).not.toBeNull();
expect(root.querySelectorAll('button.selected')).toHaveLength(2);
expect(root.querySelectorAll('.status-marker, .hypothetical-marker')).toHaveLength(0);
expect(root.querySelector('.calendar-legend')?.textContent).not.toContain('Eligible');
expect(root.querySelector<HTMLButtonElement>('button.current')?.dataset['date']).toBe(
'2024-05-06',
);
const input = document.querySelector<HTMLInputElement>('.compare-date-field input')!;
input.value = '04/05/2024';
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(document.querySelector('.unavailable-date')?.textContent).toContain(
'No leave is needed.',
);
expect(root.querySelector('button[data-date="2024-05-04"]')?.classList).not.toContain(
'ineligible',
);
expect(root.querySelector<HTMLButtonElement>('button.current')?.dataset['date']).toBe(
'2024-05-06',
);
document.querySelector<HTMLButtonElement>('.comparison-close')!.click();
await fixture.whenStable();
await vi.waitFor(() => expect(document.querySelector('[role="dialog"]')).toBeNull());
root.querySelector<HTMLButtonElement>('button[data-date="2024-05-03"]')!.click();
fixture.detectChanges();
expect(root.querySelector<HTMLButtonElement>('button.current')?.dataset['date']).toBe(
'2024-05-03',
);
expect(calculation.analyzeCounterfactual).toHaveBeenCalledTimes(1);
}); });
it('explains a whole break and compares another date against the current plan', async () => { it('explains a whole break and compares another date against the current plan', async () => {
@@ -1185,7 +1158,7 @@ describe('CalendarYear planning outcome boundary', () => {
expect(root.querySelector('.plan-summary')).not.toBeNull(); expect(root.querySelector('.plan-summary')).not.toBeNull();
}); });
it('announces background counterfactual failure and retries from explicit comparison', async () => { it('announces an explicit comparison failure and retries inside the dialog', async () => {
const { calendar, calculation } = calendarFor(plan, explanation); const { calendar, calculation } = calendarFor(plan, explanation);
const firstAttempt = deferred<CounterfactualAnalysis>(); const firstAttempt = deferred<CounterfactualAnalysis>();
const retryAttempt = deferred<CounterfactualAnalysis>(); const retryAttempt = deferred<CounterfactualAnalysis>();
@@ -1212,26 +1185,30 @@ describe('CalendarYear planning outcome boundary', () => {
)!; )!;
activateWithKeyboard(plannedDate); activateWithKeyboard(plannedDate);
fixture.detectChanges(); fixture.detectChanges();
expect(root.querySelector('.counterfactual-progress')?.textContent).toContain( expect(calculation.analyzeCounterfactual).not.toHaveBeenCalled();
expect(document.activeElement).toBe(plannedDate);
root.querySelector<HTMLButtonElement>('.compare-dates-button')!.click();
fixture.detectChanges();
expect(document.querySelector('.comparison-loading')?.textContent).toContain(
'Analyzing every date', 'Analyzing every date',
); );
expect(root.querySelector('.calendar-year > p[aria-live]')?.textContent).toContain( expect(root.querySelector('.calendar-year > p[aria-live]')?.textContent).toContain(
'Analyzing every date', 'Analyzing every date',
); );
expect(document.activeElement).toBe(plannedDate);
firstAttempt.reject(new Error('worker unavailable')); firstAttempt.reject(new Error('worker unavailable'));
await fixture.whenStable(); await fixture.whenStable();
fixture.detectChanges(); fixture.detectChanges();
expect( expect(
root.querySelector('.counterfactual-inline-error[role="alert"]')?.textContent, document.querySelector(
'.counterfactual-panel .counterfactual-inline-error[role="alert"]',
)?.textContent,
).toContain('worker unavailable'); ).toContain('worker unavailable');
expect(root.querySelector('.calendar-year > p[aria-live]')?.textContent).toContain( expect(root.querySelector('.calendar-year > p[aria-live]')?.textContent).toContain(
'Date alternatives could not be calculated.', 'Date alternatives could not be calculated.',
); );
expect(document.activeElement).toBe(plannedDate); expect(root.querySelector('.counterfactual-inline-error')).toBeNull();
root.querySelector<HTMLButtonElement>('.compare-dates-button')!.click(); document.querySelector<HTMLButtonElement>('.comparison-done')!.click();
fixture.detectChanges(); fixture.detectChanges();
expect(document.querySelector('.comparison-loading')).not.toBeNull(); expect(document.querySelector('.comparison-loading')).not.toBeNull();
const focusedBeforeResult = document.activeElement; const focusedBeforeResult = document.activeElement;
@@ -888,16 +888,6 @@
</button> </button>
</div> </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"> <p class="calendar-help">
<mat-icon aria-hidden="true">touch_app</mat-icon>{{ tr().planning.calendarHelp }} <mat-icon aria-hidden="true">touch_app</mat-icon>{{ tr().planning.calendarHelp }}
</p> </p>
@@ -926,11 +916,17 @@
</li> </li>
@if (planDetailsOpen() || traceReplay()) { @if (planDetailsOpen() || traceReplay()) {
<li> <li>
<span class="legend-symbol" aria-hidden="true">◎</span <span class="legend-swatch current" aria-hidden="true"></span
>{{ tr().planning.currentPlacement }} >{{ tr().planning.currentPlacement }}
</li> </li>
} }
@if (counterfactual()) { @if (explainedPeriod()) {
<li>
<span class="legend-swatch explained-period" aria-hidden="true"></span
>{{ tr().planning.explainedPeriod }}
</li>
}
@if (traceReplay() && counterfactual()) {
<li> <li>
<span class="legend-symbol hypothetical-symbol" aria-hidden="true">~✓</span <span class="legend-symbol hypothetical-symbol" aria-hidden="true">~✓</span
>{{ tr().planning.eligible }} >{{ tr().planning.eligible }}
@@ -1000,7 +996,8 @@
[horizonStart]="planningHorizon()?.start" [horizonStart]="planningHorizon()?.start"
[horizonEnd]="planningHorizon()?.end" [horizonEnd]="planningHorizon()?.end"
[currentDate]="currentPlacementDate()" [currentDate]="currentPlacementDate()"
[dateStatuses]="dateStatuses()" [explainedPeriod]="explainedPeriod()"
[dateStatuses]="traceReplay() ? dateStatuses() : {}"
(dateSelected)="selectCalendarDate($event)" (dateSelected)="selectCalendarDate($event)"
></app-calendar-month> ></app-calendar-month>
</mat-card-content> </mat-card-content>
@@ -369,10 +369,6 @@ output {
justify-items: start; justify-items: start;
gap: 0.75rem; gap: 0.75rem;
} }
.counterfactual-progress {
color: var(--ui-text-secondary);
font-size: 0.875rem;
}
.decisive-summary { .decisive-summary {
padding: 0.75rem; padding: 0.75rem;
border-inline-start: 3px solid var(--mat-sys-primary); border-inline-start: 3px solid var(--mat-sys-primary);
@@ -442,6 +438,13 @@ output {
&.today { &.today {
border: 2px dashed var(--today-border-color); border: 2px dashed var(--today-border-color);
} }
&.current {
outline: 2px solid var(--today-border-color);
outline-offset: -2px;
}
&.explained-period {
box-shadow: inset 0 -3px var(--today-border-color);
}
} }
.legend-symbol, .legend-symbol,
.status-symbol { .status-symbol {
@@ -260,13 +260,22 @@ export class CalendarYear {
}); });
protected readonly currentPlacementDate = computed(() => { protected readonly currentPlacementDate = computed(() => {
if (this.counterfactual())
return this.selectedCounterfactualDate() ?? this.counterfactual()!.placement.date;
if (this.traceReplay()) if (this.traceReplay())
return this.selectedTraceDate() ?? this.selectedTraceDecision()?.dates[0] ?? null; return (
return this.selectedExplanation()?.placement.date ?? null; this.selectedCounterfactualDate() ??
this.selectedTraceDate() ??
this.selectedTraceDecision()?.dates[0] ??
null
);
return this.planDetailsOpen() ? (this.selectedExplanation()?.placement.date ?? null) : null;
}); });
protected readonly explainedPeriod = computed(() =>
this.planDetailsOpen() && !this.traceReplay()
? this.selectedExplanation()?.timeOffPeriod
: undefined,
);
protected readonly dateStatuses = computed<Readonly<Record<string, CalendarDayStatus>>>(() => { protected readonly dateStatuses = computed<Readonly<Record<string, CalendarDayStatus>>>(() => {
const statuses: Record<string, CalendarDayStatus> = {}; const statuses: Record<string, CalendarDayStatus> = {};
const trace = this.planningTrace(); const trace = this.planningTrace();
@@ -543,7 +552,7 @@ export class CalendarYear {
this.planningExplanation.set(explanation ?? null); this.planningExplanation.set(explanation ?? null);
if (explanation?.kind === 'PlanExplanation') { if (explanation?.kind === 'PlanExplanation') {
this.planDetailsOpen.set(true); this.planDetailsOpen.set(true);
this.selectPlacement(0, false, false); this.selectPlacement(0, false);
this.liveMessage.set( this.liveMessage.set(
`${this.planningOutcome() ? this.outcomeLabel(this.planningOutcome()!) : ''} ${this.placementAnnouncement()}`.trim(), `${this.planningOutcome() ? this.outcomeLabel(this.planningOutcome()!) : ''} ${this.placementAnnouncement()}`.trim(),
); );
@@ -621,7 +630,7 @@ export class CalendarYear {
this.userInput.remainingVacationDaysSignal.set([]); this.userInput.remainingVacationDaysSignal.set([]);
} }
protected selectPlacement(index: number, analyze = true, announce = true): void { protected selectPlacement(index: number, announce = true): void {
const explanation = this.planExplanation(); const explanation = this.planExplanation();
if (!explanation?.placements.length) return; if (!explanation?.placements.length) return;
const selected = Math.max(0, Math.min(index, explanation.placements.length - 1)); const selected = Math.max(0, Math.min(index, explanation.placements.length - 1));
@@ -630,7 +639,6 @@ export class CalendarYear {
this.clearCounterfactual(); this.clearCounterfactual();
this.selectedCounterfactualDate.set(explanation.placements[selected].placement.date); this.selectedCounterfactualDate.set(explanation.placements[selected].placement.date);
if (announce) this.liveMessage.set(this.placementAnnouncement()); if (announce) this.liveMessage.set(this.placementAnnouncement());
if (analyze) void this.analyzeSelection(explanation.placements[selected].placement);
} }
protected compareDates(): void { protected compareDates(): void {
+1
View File
@@ -221,6 +221,7 @@ export interface AppTranslations {
uncompletedStages: string; uncompletedStages: string;
none: string; none: string;
currentPlacement: string; currentPlacement: string;
explainedPeriod: string;
outsideHorizon: string; outsideHorizon: string;
traceTitle: string; traceTitle: string;
traceLoading: string; traceLoading: string;
+1
View File
@@ -143,6 +143,7 @@
"uncompletedStages": "Unterbrochene oder nicht gestartete Schritte", "uncompletedStages": "Unterbrochene oder nicht gestartete Schritte",
"none": "Keine", "none": "Keine",
"currentPlacement": "Aktuell erklärte Platzierung", "currentPlacement": "Aktuell erklärte Platzierung",
"explainedPeriod": "Erklärter Freizeitzeitraum",
"outsideHorizon": "Kontextdatum außerhalb des Planungshorizonts", "outsideHorizon": "Kontextdatum außerhalb des Planungshorizonts",
"traceTitle": "Erweitert: ursprüngliche Entscheidungsspur", "traceTitle": "Erweitert: ursprüngliche Entscheidungsspur",
"traceLoading": "Der Plan ist fertig; seine Entscheidungsspur wird noch geladen…", "traceLoading": "Der Plan ist fertig; seine Entscheidungsspur wird noch geladen…",
+1
View File
@@ -143,6 +143,7 @@
"uncompletedStages": "Interrupted or unstarted stages", "uncompletedStages": "Interrupted or unstarted stages",
"none": "None", "none": "None",
"currentPlacement": "Current explained placement", "currentPlacement": "Current explained placement",
"explainedPeriod": "Explained time-off period",
"outsideHorizon": "Context date outside the planning horizon", "outsideHorizon": "Context date outside the planning horizon",
"traceTitle": "Advanced: original decision trace", "traceTitle": "Advanced: original decision trace",
"traceLoading": "The plan is ready; its decision trace is still loading…", "traceLoading": "The plan is ready; its decision trace is still loading…",
+1
View File
@@ -143,6 +143,7 @@
"uncompletedStages": "Etapas interrumpidas o no iniciadas", "uncompletedStages": "Etapas interrumpidas o no iniciadas",
"none": "Ninguno", "none": "Ninguno",
"currentPlacement": "Asignación explicada actualmente", "currentPlacement": "Asignación explicada actualmente",
"explainedPeriod": "Período de descanso explicado",
"outsideHorizon": "Fecha de contexto fuera del horizonte de planificación", "outsideHorizon": "Fecha de contexto fuera del horizonte de planificación",
"traceTitle": "Avanzado: traza de decisiones original", "traceTitle": "Avanzado: traza de decisiones original",
"traceLoading": "El plan está listo; su traza de decisiones aún se está cargando…", "traceLoading": "El plan está listo; su traza de decisiones aún se está cargando…",
+1
View File
@@ -143,6 +143,7 @@
"uncompletedStages": "Étapes interrompues ou non démarrées", "uncompletedStages": "Étapes interrompues ou non démarrées",
"none": "Aucun", "none": "Aucun",
"currentPlacement": "Placement expliqué actuellement", "currentPlacement": "Placement expliqué actuellement",
"explainedPeriod": "Période de repos expliquée",
"outsideHorizon": "Date de contexte hors de l’horizon de planification", "outsideHorizon": "Date de contexte hors de l’horizon de planification",
"traceTitle": "Avancé : trace des décisions originales", "traceTitle": "Avancé : trace des décisions originales",
"traceLoading": "Le plan est prêt ; sa trace de décision est encore en cours de chargement…", "traceLoading": "Le plan est prêt ; sa trace de décision est encore en cours de chargement…",