# Holiday Calendar Implementation Plan ## Ziel Planarchy soll standortabhaengige Feiertage fachlich korrekt berechnen koennen, sodass zwei Personen im selben Land, aber in unterschiedlichen Regionen oder Staedten, unterschiedliche `SAH` und damit unterschiedliche Chargeability erhalten koennen. Die Feiertagsaufloesung soll kuenftig diese Prioritaet haben: 1. `metroCity` 2. `federalState` / Region 3. `country` Manuelle, ressourcenspezifische `PUBLIC_HOLIDAY`-Eintraege bleiben weiterhin moeglich und ueberschreiben bzw. ergaenzen den Kalender. ## Ist-Zustand Aktuell existieren drei getrennte Mechanismen: 1. Statisch codierte Feiertage in `packages/shared/src/constants/publicHolidays.ts` 2. Batch-/Auto-Import von `PUBLIC_HOLIDAY`-Vacations 3. Laufzeitberechnung von `SAH` bzw. Chargeability aus Land/Bundesland Die zentralen Luecken: - Es gibt kein Holiday-Stammdatenmodell in der Datenbank. - Es gibt keinen Editor fuer Feiertagskalender. - `metroCity` wird fuer Feiertage nicht ausgewertet. - Die aktuelle Logik ist faktisch auf Deutschland plus `federalState` zugeschnitten. - Feiertagswissen ist doppelt vorhanden: statische Kalenderlogik plus importierte `Vacation`-Datensaetze. ## Zielarchitektur ### 1. Holiday Calendar als Stammdatenmodell Neue Stammdatenobjekte: - `HolidayCalendar` - `HolidayCalendarEntry` `HolidayCalendar` beschreibt den Gueltigkeitsbereich eines Kalenders: - `scopeType`: `COUNTRY | STATE | CITY` - `countryId` - optional `stateCode` - optional `metroCityId` - `name` - `isActive` - optional `priority` `HolidayCalendarEntry` beschreibt den einzelnen Feiertag: - `holidayCalendarId` - `date` - `name` - optional `isRecurringAnnual` - optional `source` Fachregel: - Pro Scope soll es genau einen aktiven Kalender geben. - Die effektiven Feiertage eines Mitarbeiters ergeben sich aus Merge mit Prioritaet `country < state < city`. - Gleiche Daten auf engerem Scope ueberschreiben denselben Tag vom breiteren Scope. ### 2. Laufzeit-Resolver statt statischer Sonderlogik Neue gemeinsame Backend-Komponente: - `resolveResourceHolidayCalendar(...)` Aufgaben: - liest Kalenderdaten fuer `countryId`, `federalState`, `metroCityId` - ermittelt die effektiven Feiertage fuer einen Zeitraum - merged diese mit expliziten `Vacation`-Eintraegen vom Typ `PUBLIC_HOLIDAY` - liefert: - `publicHolidayStrings` - `absenceDays` - optional Debug-Metadaten zur Herkunft eines Feiertags Diese Komponente wird die einzige Quelle fuer Feiertagslogik in: - Chargeability Report - Chargeability Alerts - Computation Graph - ggf. weitere SAH-/Allocation-Pfade ### 3. Import und Editor werden auf Stammdaten umgestellt Der heutige Batch-/Auto-Import darf nicht die Primarlogik fuer Feiertage bleiben. Zielbild: - Stammdatenkalender sind die Quelle der Wahrheit. - Optionaler Import in `Vacation` bleibt nur als Kompatibilitaets- oder Exportfunktion. - Bestehende `PUBLIC_HOLIDAY`-Vacations werden fuer Uebergangszeit weiter beruecksichtigt. ## Datenmodell ### Prisma-Erweiterungen Neue Modelle: ```prisma model HolidayCalendar { id String @id @default(cuid()) name String scopeType HolidayCalendarScope countryId String stateCode String? metroCityId String? isActive Boolean @default(true) priority Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt country Country @relation(fields: [countryId], references: [id]) metroCity MetroCity? @relation(fields: [metroCityId], references: [id]) entries HolidayCalendarEntry[] @@index([countryId, scopeType]) @@index([metroCityId]) } model HolidayCalendarEntry { id String @id @default(cuid()) holidayCalendarId String date DateTime @db.Date name String isRecurringAnnual Boolean @default(false) source String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt holidayCalendar HolidayCalendar @relation(fields: [holidayCalendarId], references: [id], onDelete: Cascade) @@unique([holidayCalendarId, date, name]) @@index([date]) } ``` Neues Enum: ```prisma enum HolidayCalendarScope { COUNTRY STATE CITY } ``` ### Integritaetsregeln - `STATE` verlangt `stateCode`. - `CITY` verlangt `metroCityId`. - `CITY` und `STATE` muessen zum selben `countryId` passen. - Ein `CITY`-Kalender darf nur fuer eine `MetroCity` des angegebenen Landes existieren. Diese Regeln werden teils im Schema, teils in Router-Validierung erzwungen. ## API- und Backend-Pakete ### Paket A: Schema und Datenzugriff Dateien: - `packages/db/prisma/schema.prisma` - neue Migration - ggf. `packages/shared/src/types/*` - ggf. `packages/shared/src/schemas/*` Ergebnis: - Holiday-Calendar-Datenmodell ist vorhanden - Zod-/Shared-Typen fuer CRUD sind definiert ### Paket B: Holiday Calendar Router Neue oder erweiterte API: - `packages/api/src/router/holiday-calendar.ts` - Router in `packages/api/src/index.ts` Operationen: - `listCalendars` - `getCalendarById` - `createCalendar` - `updateCalendar` - `deleteCalendar` - `createEntry` - `updateEntry` - `deleteEntry` - optional `previewResolvedHolidays` ### Paket C: Gemeinsamer Resolver Dateien: - `packages/api/src/lib/holiday-resolver.ts` - bestehende Hilfen in `packages/api/src/lib/holiday-availability.ts` refactoren oder ersetzen Ergebnis: - einheitliche Feiertagsaufloesung fuer alle Backend-Pfade - keine neue statische Sonderlogik in Routern ### Paket D: Integration in Berechnungen Betroffene Stellen: - `packages/api/src/router/chargeability-report.ts` - `packages/api/src/lib/chargeability-alerts.ts` - `packages/api/src/router/computation-graph.ts` - weitere `calculateSAH`-Aufrufer mit Feiertagsbezug Abnahme: - dieselbe Ressource liefert je nach `metroCity` / `federalState` unterschiedliche `SAH` - gleiche Eingaben erzeugen in allen Reports denselben Feiertagseffekt ### Paket E: UI / Admin Betroffene Stellen: - neue Admin-Seite oder Erweiterung im Country-Admin - Wiederverwendung moeglicher Muster aus: - `apps/web/src/components/admin/CountriesClient.tsx` - `apps/web/src/components/vacations/PublicHolidayBatch.tsx` - vorhandene Modal-/Table-Komponenten Ziel: - Kalender pro Land / Bundesland / Stadt anlegen und bearbeiten - Eintraege pro Jahr pflegen - Aufloesung fuer eine Beispiel-Ressource optional vorschauen ### Paket F: Kompatibilitaet / Migration Uebergangsstrategie: 1. Bestehende `PUBLIC_HOLIDAY`-Vacations bleiben gueltig. 2. Neuer Resolver nutzt zuerst Stammdatenkalender plus manuelle Overrides. 3. Batch-/Auto-Import wird als Legacy-Funktion markiert. 4. Spaeter kann entschieden werden, ob Import nur noch Materialisierung fuer Sonderfaelle ist. ## Fachliche Aufloesungsregeln ### Prioritaet 1. Manuelle ressourcenspezifische `PUBLIC_HOLIDAY`-Vacation 2. `CITY`-Kalender 3. `STATE`-Kalender 4. `COUNTRY`-Kalender ### Merge-Regeln - Gleiches Datum mehrfach: - engster Scope gewinnt fuer Anzeige/Quelle - fuer `SAH` zaehlt der Tag genau einmal - Feiertag auf Wochenende: - erscheint im Kalender - reduziert `SAH` nur, wenn der Tag laut Verfuegbarkeit ein Arbeitstag ist - Halbtag-Feiertage: - aktuell nicht erforderlich - nur aufnehmen, wenn fachlich explizit benoetigt ## Umsetzung in parallelen Workern ### Worker 1: Schema + Shared Contracts Verantwortung: - Prisma-Modelle - Migration - Shared Types / Zod Schemas Write Scope: - `packages/db/prisma/schema.prisma` - `packages/shared/src/types/*` - `packages/shared/src/schemas/*` ### Worker 2: Backend Router + Validation Verantwortung: - CRUD-API fuer Holiday Calendars - Validierung von Scope-Regeln - Audit-Logging Write Scope: - `packages/api/src/router/holiday-calendar.ts` - `packages/api/src/index.ts` - eng verbundene Tests ### Worker 3: Resolver + Berechnungsintegration Verantwortung: - gemeinsamer Holiday Resolver - Integration in Report, Alerts, Computation Graph - Entfernung duplizierter Feiertagslogik Write Scope: - `packages/api/src/lib/holiday-resolver.ts` - `packages/api/src/lib/holiday-availability.ts` - `packages/api/src/router/chargeability-report.ts` - `packages/api/src/lib/chargeability-alerts.ts` - `packages/api/src/router/computation-graph.ts` - eng verbundene Tests ### Worker 4: Admin UI Verantwortung: - neue Holiday-Calendar-Admin-Oberflaeche - Calendar-Entry-Editing - optional Preview fuer aufgeloeste Feiertage Write Scope: - `apps/web/src/components/admin/*` - relevante App-Routen - eng verbundene UI-Tests falls vorhanden ### Worker 5: Migration / Legacy Behavior / Verify Verantwortung: - Legacy Import klar einhaengen oder abgrenzen - Verify-/Smoke-Pfade - End-to-End-Pruefung der fachlichen Szenarien Write Scope: - `packages/api/src/lib/holiday-auto-import.ts` - `packages/api/src/router/vacation.ts` - Verify-Skripte und Tests ## Teststrategie ### Unit - Resolver merged `country + state + city` korrekt - `CITY` ueberschreibt `STATE`, `STATE` ergaenzt `COUNTRY` - manuelle `PUBLIC_HOLIDAY`-Vacation wird beruecksichtigt - identisches Datum wird nur einmal auf `SAH` angerechnet ### Integration - Chargeability Report: zwei Ressourcen, gleiches Land, unterschiedliche Stadt, unterschiedliche `SAH` - Chargeability Alerts: derselbe Feiertagseffekt wie im Report - Computation Graph: dieselbe Feiertagsanzahl wie Resolver ### UI - Kalender anlegen fuer `COUNTRY`, `STATE`, `CITY` - Eintrag anlegen/aendern/loeschen - Scope-Validierung verhindert ungueltige Kombinationen ### Datenmigration / Regression - bestehende `PUBLIC_HOLIDAY`-Vacations bleiben wirksam - alte Batch-Funktion erzeugt keine Konflikte - Repo-weit: - `pnpm test` - `pnpm typecheck` - relevanter E2E-Smoke fuer Admin-Pfad, falls vorhanden ## Abnahme-Kriterien - Feiertage sind nicht mehr hart an Deutschland/Bundesland im Laufzeitpfad gekoppelt. - `metroCity` kann `SAH` fachlich beeinflussen. - Es gibt eine Admin-faehige Pflege fuer Feiertagskalender. - Report, Alerts und Computation Graph verwenden denselben Resolver. - Bestehende manuelle Feiertagsabwesenheiten bleiben kompatibel. ## Empfohlene Reihenfolge 1. Schema + Shared Contracts 2. Backend Router 3. Resolver + Integration 4. UI 5. Migration/Legacy und Gesamttests ## Offene Produktentscheidungen - Sollen Feiertage kuenftig nur manuell gepflegt werden oder auch per externem Provider importierbar sein? - Brauchen wir Halbtag-Feiertage? - Reicht `metroCity` als lokaler Scope oder brauchen wir spaeter feinere Geo-Einheiten? - Soll Legacy-Batch-Import langfristig entfernt oder als Materialisierung behalten werden?