import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, inject, } from '@angular/core'; import { CalendarService, Month, SelectedDates } from '../../services/calendar-service'; import { MatTooltip } from '@angular/material/tooltip'; import { CalendarSettingsService } from '../../services/calendar-settings-service'; import { DateCacheService } from '../../services/date-cache.service'; import { TranslationService } from '../../services/translation-service'; import { isWorkingPlanningDate } from '../../services/declared-non-working-days'; import type { LeaveType } from '../../services/leave-planner.service'; export interface DeclarationPreview { dates: ReadonlySet; type: LeaveType | null; } @Component({ selector: 'app-calendar-month', imports: [MatTooltip], templateUrl: './calendar-month.html', styleUrl: './calendar-month.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class CalendarMonth implements OnInit { protected calendarSettingsService = inject(CalendarSettingsService); private dateCache = inject(DateCacheService); protected translationService = inject(TranslationService); @Input({ required: true }) monthIndex!: number; @Input({ required: true }) year!: number; get tr() { return this.translationService.tr; } // Use DateCacheService instead of creating Date objects getNow(): Date { return this.dateCache.getNow(); } currentMonth: Date = new Date(); weeksInAMonth: Month = []; private dayFormatterLocale = ''; private dayFormatter?: Intl.DateTimeFormat; @Input({ required: true }) selectedDates!: SelectedDates; @Input() horizonStart?: string; @Input() horizonEnd?: string; @Input() declarationEntry = false; @Input() declaredDays: Readonly> = {}; @Input() declarationPreview: DeclarationPreview | null = null; getDeclaredType(date: string): LeaveType | undefined { const current = this.declaredDays[date]; if (!this.declarationPreview?.dates.has(date)) return current; const selected = this.declarationPreview.type; return current === selected ? undefined : (selected ?? undefined); } getDayClasses(day: Date): string { const classes = this.selectedDates.getClasses(day); return this.declarationPreview?.dates.has(this.getDateKey(day)) ? `${classes.replace(/\b(cp|rtt|other)\b/g, '')} ${this.getDeclaredType(this.getDateKey(day)) ?? ''}` : classes; } isDateDisabled(day: Date): boolean { return ( !this.isInHorizon(day) || (this.declarationEntry && !isWorkingPlanningDate(this.getDateKey(day))) ); } declaredTypeLabel(type: LeaveType): string { return type === 'cp' ? 'CP' : this.translationService.getDayTypeLabel(type); } @Input() currentDate: string | null = null; @Input() explainedPeriod?: { start: string; end: string }; @Output() readonly dateSelected = new EventEmitter(); getDateKey(day: Date): string { return [ String(day.getFullYear()).padStart(4, '0'), String(day.getMonth() + 1).padStart(2, '0'), String(day.getDate()).padStart(2, '0'), ].join('-'); } isInHorizon(day: Date): boolean { const date = this.getDateKey(day); return ( (!this.horizonStart || date >= this.horizonStart) && (!this.horizonEnd || date < this.horizonEnd) ); } isInExplainedPeriod(day: Date): boolean { const date = this.getDateKey(day); return ( !!this.explainedPeriod && date >= this.explainedPeriod.start && date <= this.explainedPeriod.end ); } getDayAriaLabel(day: Date, tooltip: string | null = this.getDayTooltip(day)): string { const locale = this.translationService.locale(); if (!this.dayFormatter || locale !== this.dayFormatterLocale) { this.dayFormatterLocale = locale; this.dayFormatter = new Intl.DateTimeFormat(locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }); } const parts = [this.dayFormatter.format(day)]; 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)) parts.push(this.translationService.tr().planning.outsideHorizon); return parts.join('. '); } getMonthName(): string { const locale = this.translationService.locale(); const tempDate = new Date(this.year, this.monthIndex, 1); const monthName = tempDate.toLocaleString(locale, { month: 'long' }); return monthName.charAt(0).toUpperCase() + monthName.slice(1); } getWeekdayLetters(): string[] { // January 5, 2026 is a Monday const baseDate = new Date(2026, 0, 5); const locale = this.translationService.locale(); const letters: string[] = []; for (let i = 0; i < 7; i++) { const date = new Date(baseDate); date.setDate(baseDate.getDate() + i); const letter = date.toLocaleString(locale, { weekday: 'narrow' }); letters.push(letter.toUpperCase()); } return letters; } getWeekNumber(week: (Date | null)[]): number | null { for (const day of week) { if (day) { return CalendarService.getWeekNumber(day); } } return null; } getDayTooltip(day: Date): string | null { const previewed = this.declarationPreview?.dates.has(this.getDateKey(day)); const tooltips: string[] = this.selectedDates .dayTypes(day) .filter((type) => !previewed || !['cp', 'rtt', 'other'].includes(type)) .map((type) => this.translationService.getDayTypeLabel(type)); const declaredType = this.getDeclaredType(this.getDateKey(day)); if (declaredType) tooltips.push( `${this.tr().bookedDays.booked} (${this.declaredTypeLabel(declaredType)}). ${this.tr().bookedDays.fixed}`, ); return tooltips.length > 0 ? tooltips.join(' + ') : null; } ngOnInit(): void { this.weeksInAMonth = CalendarService.weeksInMonth(this.monthIndex, this.year); } }