PlanningCalculationService now builds the submitted request, owns the pending and accepted calculation state, and delivers explanations for the accepted calculation only. The duration estimator reads the same request. The calendar and leave-balance summary consume the accepted outcome instead of coordinating writable state on the input service. Refs #275 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
184 lines
5.8 KiB
TypeScript
184 lines
5.8 KiB
TypeScript
import { Injectable, signal } from '@angular/core';
|
|
import { SelectableDayType, VacDay } from './calendar-service';
|
|
import dayjs from 'dayjs';
|
|
import {
|
|
DeclaredNonWorkingDay,
|
|
isWorkingPlanningDate,
|
|
parseDeclaredDays,
|
|
} from './declared-non-working-days';
|
|
import type { LeaveType } from './leave-planner.service';
|
|
|
|
@Injectable({
|
|
providedIn: 'root',
|
|
})
|
|
export class UserInputService {
|
|
private readonly declaredDays = signal(this.restoreDeclaredDays());
|
|
readonly declaredDaysSignal = this.declaredDays.asReadonly();
|
|
readonly declarationEntrySignal = signal(false);
|
|
readonly declaredTypeSignal = signal<LeaveType | null>('cp');
|
|
|
|
private restoreDeclaredDays(): DeclaredNonWorkingDay[] {
|
|
try {
|
|
const saved =
|
|
typeof localStorage === 'undefined'
|
|
? null
|
|
: localStorage.getItem('declaredNonWorkingDays');
|
|
return saved === null ? [] : parseDeclaredDays(JSON.parse(saved));
|
|
} catch (error) {
|
|
console.warn('Failed to restore declared non-working days from localStorage.', error);
|
|
try {
|
|
localStorage.removeItem('declaredNonWorkingDays');
|
|
} catch {
|
|
// Browser storage can be unavailable; keep calendar entry usable.
|
|
}
|
|
return [];
|
|
}
|
|
}
|
|
|
|
toggleDeclaredDay(date: string, type: LeaveType): void {
|
|
this.toggleDeclaredDays([date], type);
|
|
}
|
|
|
|
toggleDeclaredDays(dates: readonly string[], type: LeaveType | null): void {
|
|
const workingDates = new Set(dates.filter(isWorkingPlanningDate));
|
|
if (!workingDates.size) return;
|
|
this.declaredDays.update((days) => {
|
|
const existing = new Map(days.map((day) => [day.date, day.type]));
|
|
const others = days.filter((day) => !workingDates.has(day.date));
|
|
return [
|
|
...others,
|
|
...(type
|
|
? [...workingDates]
|
|
.filter((date) => existing.get(date) !== type)
|
|
.map((date) => ({ date, type }))
|
|
: []),
|
|
].sort((a, b) => a.date.localeCompare(b.date));
|
|
});
|
|
try {
|
|
if (typeof localStorage !== 'undefined')
|
|
localStorage.setItem('declaredNonWorkingDays', JSON.stringify(this.declaredDays()));
|
|
} catch (error) {
|
|
console.warn('Failed to save declared non-working days to localStorage.', error);
|
|
}
|
|
}
|
|
|
|
private restoreDate(value: unknown): Date {
|
|
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
const date = new Date(`${value}T00:00:00`);
|
|
if (dayjs(date).format('YYYY-MM-DD') === value) return date;
|
|
}
|
|
throw new Error('Unsupported saved balance date.');
|
|
}
|
|
|
|
private getSavedValues(): VacDay[] | undefined {
|
|
let stored: string | null = null;
|
|
try {
|
|
if (typeof localStorage === 'undefined') return;
|
|
stored = localStorage.getItem('leaveBalances');
|
|
if (stored === null) return;
|
|
const saved: unknown = JSON.parse(stored);
|
|
if (!Array.isArray(saved)) throw new Error('Saved leave balances must be a list.');
|
|
return saved.map((value: unknown) => {
|
|
if (typeof value !== 'object' || value === null)
|
|
throw new Error('Unsupported saved balance record.');
|
|
const { id, type, numberOfDays, availableDate, expiryDate } = value as Record<
|
|
string,
|
|
unknown
|
|
>;
|
|
if (
|
|
typeof id !== 'string' ||
|
|
typeof numberOfDays !== 'number' ||
|
|
!Number.isFinite(numberOfDays) ||
|
|
(type !== SelectableDayType.CP &&
|
|
type !== SelectableDayType.RTT &&
|
|
type !== SelectableDayType.OTHER)
|
|
)
|
|
throw new Error('Unsupported saved balance record.');
|
|
return {
|
|
id,
|
|
type,
|
|
numberOfDays,
|
|
availableDate:
|
|
availableDate === undefined ? undefined : this.restoreDate(availableDate),
|
|
expiryDate: this.restoreDate(expiryDate),
|
|
};
|
|
});
|
|
} catch (error) {
|
|
console.warn('Failed to restore leave balances from localStorage.', error);
|
|
if (stored !== null) {
|
|
try {
|
|
localStorage.removeItem('leaveBalances');
|
|
} catch {
|
|
// Keep the random fallback usable when browser storage is unavailable.
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
private getRandomNumber(): number {
|
|
return Math.floor(Math.random() * 11) + 10;
|
|
}
|
|
|
|
private getInitialValues(): VacDay[] {
|
|
const availableDate = new Date();
|
|
return [
|
|
{
|
|
id: crypto.randomUUID(),
|
|
type: SelectableDayType.CP,
|
|
numberOfDays: this.getRandomNumber(),
|
|
availableDate,
|
|
// today + 365 days
|
|
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
|
},
|
|
{
|
|
id: crypto.randomUUID(),
|
|
type: SelectableDayType.RTT,
|
|
numberOfDays: this.getRandomNumber(),
|
|
availableDate: new Date(availableDate),
|
|
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
|
},
|
|
{
|
|
id: crypto.randomUUID(),
|
|
type: SelectableDayType.OTHER,
|
|
numberOfDays: this.getRandomNumber(),
|
|
availableDate: new Date(availableDate),
|
|
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
|
},
|
|
];
|
|
}
|
|
|
|
public createVacationDay(): VacDay {
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
type: SelectableDayType.CP,
|
|
numberOfDays: 1,
|
|
availableDate: new Date(),
|
|
expiryDate: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
|
|
};
|
|
}
|
|
|
|
readonly vacationNumberSignal = signal<VacDay[]>(
|
|
this.getSavedValues() ?? this.getInitialValues(),
|
|
);
|
|
public updateBalances(updater: (balances: VacDay[]) => VacDay[]): void {
|
|
this.vacationNumberSignal.update(updater);
|
|
try {
|
|
if (typeof localStorage === 'undefined') return;
|
|
// Store calendar dates, so restoring them does not depend on a UTC offset.
|
|
const balances = this.vacationNumberSignal().map((balance) => ({
|
|
id: balance.id,
|
|
type: balance.type,
|
|
numberOfDays: balance.numberOfDays,
|
|
availableDate: balance.availableDate
|
|
? dayjs(balance.availableDate).format('YYYY-MM-DD')
|
|
: undefined,
|
|
expiryDate: dayjs(balance.expiryDate).format('YYYY-MM-DD'),
|
|
}));
|
|
localStorage.setItem('leaveBalances', JSON.stringify(balances));
|
|
} catch (error) {
|
|
console.warn('Failed to save leave balances to localStorage.', error);
|
|
}
|
|
}
|
|
}
|