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
184 lines
6.1 KiB
TypeScript
184 lines
6.1 KiB
TypeScript
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();
|
||
}
|
||
}
|