Files
vac-optimizer/src/app/components/user-input/user-input.ts
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

184 lines
6.1 KiB
TypeScript
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.
import { Component, computed, effect, inject, ChangeDetectionStrategy } from '@angular/core';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { DateAdapter } from '@angular/material/core';
import { FormsModule } from '@angular/forms';
import dayjs from 'dayjs';
import { UserInputService } from '../../services/user-input-service';
import { RulesPanel } from '../rules-panel/rules-panel';
import { MatCard, MatCardContent } from '@angular/material/card';
import { MatIcon } from '@angular/material/icon';
import { DayInput } from '../userInput/day-input/day-input';
import { MatButtonModule } from '@angular/material/button';
import { VacDay } from '../../services/calendar-service';
import type { LeaveType } from '../../services/leave-planner.service';
import { TranslationService } from '../../services/translation-service';
import { CalendarSettingsService } from '../../services/calendar-settings-service';
import { PlanningDurationService } from '../../services/planning-duration.service';
import { addPlanningDays, isPlanningDate } from '../../services/planning-date';
import { DAY_FIRST_DATE_PROVIDERS } from '../../shared/day-first-date-adapter';
@Component({
selector: 'app-user-input',
imports: [
MatInputModule,
MatFormFieldModule,
MatDatepickerModule,
FormsModule,
RulesPanel,
MatCard,
MatCardContent,
MatIcon,
MatButtonModule,
DayInput,
],
templateUrl: './user-input.html',
styleUrl: './user-input.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: DAY_FIRST_DATE_PROVIDERS,
})
export class UserInput {
private userInputService: UserInputService = inject(UserInputService);
protected readonly declarationEntry = this.userInputService.declarationEntrySignal;
protected readonly declaredType = this.userInputService.declaredTypeSignal;
protected readonly declaredTypes: readonly LeaveType[] = ['cp', 'rtt', 'other'];
protected readonly calendarSettings = inject(CalendarSettingsService);
protected readonly duration = inject(PlanningDurationService);
protected readonly translationService = inject(TranslationService);
private readonly dateAdapter = inject<DateAdapter<Date>>(DateAdapter);
protected readonly planningDates = computed(() => {
const { start, lastPlanningDate } = this.calendarSettings.planningDates();
return {
start: isPlanningDate(start) ? new Date(`${start}T00:00:00`) : null,
lastPlanningDate: isPlanningDate(lastPlanningDate)
? new Date(`${lastPlanningDate}T00:00:00`)
: null,
};
});
protected readonly durationLabel = computed(() => {
const estimate = this.duration.estimate();
if (!estimate) return '';
return this.tr()
.userInput.durationEstimate.replace(
'{range}',
`${estimate.minimum}–${estimate.maximum}`,
)
.replace('{unit}', this.tr().userInput.durationUnits[estimate.unit]);
});
get tr() {
return this.translationService.tr;
}
protected readonly remainingDays = computed(() =>
(this.userInputService.displayedPlanSignal()?.remainingBalances ?? []).reduce(
(total, balance) => total + balance.quantity,
0,
),
);
protected readonly hasAtRiskDays = computed(() =>
this.userInputService
.displayedPlanSignal()
?.remainingBalances.some((balance) => balance.classification !== 'deferred'),
);
protected remainingDaysDetails = computed(() => {
const plan = this.userInputService.displayedPlanSignal();
if (!plan) return [];
const incomplete =
this.userInputService.planningOutcomeSignal()?.kind === 'PlanningIncomplete';
const copy = this.tr().userInput;
return plan.remainingBalances.map((balance) => {
const code =
incomplete &&
['preserved', 'active-priorities', 'not-established'].includes(balance.reason.code)
? 'calculation-incomplete'
: balance.reason.code;
return {
id: balance.id,
title: `${balance.quantity} ${balance.quantity > 1 ? copy.daysSuffix : copy.daySuffix} ${this.translationService.getDayTypeLabel(balance.type)}`,
reason: this.translationService.getRemainingBalanceReason(
balance,
plan,
incomplete,
),
validity: copy.remainingValidity
.replace(
'{available}',
this.translationService.formatPlanningDate(balance.availableFrom),
)
.replace(
'{expiry}',
this.translationService.formatPlanningDate(balance.expiryDate),
),
horizon: copy.remainingHorizon
.replace(
'{start}',
this.translationService.formatPlanningDate(plan.horizon.start),
)
.replace(
'{end}',
this.translationService.formatPlanningDate(
addPlanningDays(plan.horizon.end, -1),
),
),
earnedFrom: balance.sourceRuleId
? copy.remainingEarnedFrom.replace(
'{rule}',
this.ruleName(balance.sourceRuleId),
)
: '',
suggestion: copy.remainingSuggestions[code],
};
});
});
constructor() {
effect(() => this.dateAdapter.setLocale(this.translationService.locale()));
}
protected setPlanningDate(field: 'start' | 'lastPlanningDate', value: Date | null): void {
const dates = {
...this.calendarSettings.planningDates(),
[field]: value ? dayjs(value).format('YYYY-MM-DD') : '',
};
this.calendarSettings.setPlanningDates(dates.start, dates.lastPlanningDate);
}
private ruleName(id: string): string {
return (
this.userInputService.planningSnapshotSignal()?.rules.find((rule) => rule.id === id)
?.name ?? id
);
}
protected hasRemainingDays = computed(() => this.remainingDays() > 0);
protected get daysOff() {
return this.userInputService.vacationNumberSignal();
}
public addDay() {
this.userInputService.updateBalances((days) => [
...days,
this.userInputService.createVacationDay(),
]);
}
public removeFromList(index: number) {
this.userInputService.updateBalances((days) => days.filter((_, i) => i !== index));
}
public updateDay(index: number, updatedDay: VacDay) {
this.userInputService.updateBalances((days) =>
days.map((day, i) => (i === index ? updatedDay : day)),
);
}
public triggerComputation() {
if (this.calendarSettings.planningHorizonError()) return;
this.userInputService.requestComputation();
}
}