# Plan: Country-Driven Standard Available Hours (SAH) and FTE **Date:** 2026-03-13 **Status:** Draft **Depends on:** OrgUnit hierarchy, Resource model extensions ## Problem CapaKraken currently uses a flat `hoursPerDay` on allocations. The chargeability reporting model requires: - Country-specific daily working hours (8h Germany, 9h India, variable Spain) - Public holidays per country AND metro city - FTE as a multiplier that reduces available hours (FTE 0.5 = 4h/day in an 8h country) - SAH (Standard Available Hours) = net hours after deducting holidays and absences Without SAH, FTE-based chargeability calculations cannot be accurate. ## Concepts ### Standard Available Hours (SAH) > "Die Standard Available Hrs (SAH) bezeichnen die Netto-Arbeitszeit, also die Zeit, die nach Abzug aller Fehlzeiten tatsaechlich fuer die produktive Arbeit zur Verfuegung steht." **Formula per resource per period:** ``` grossHoursPerDay = country.dailyWorkingHours (with Spain schedule rules) effectiveHoursPerDay = grossHoursPerDay * resource.fte workingDaysInPeriod = calendarDays - weekends - publicHolidays(country, metroCity) absenceDays = vacation + illness + other absence SAH = effectiveHoursPerDay * (workingDaysInPeriod - absenceDays) ``` ### Daily Working Hours by Country | Country | Daily Hours | Special Rules | |---|---|---| | Costa Rica | 8h | - | | Germany | 8h | - | | Hungary | 8h | - | | India | 9h | - | | Italy | 8h | - | | Portugal | 8h | - | | Spain | variable | Fridays always 6.5h; Mon-Thu 6.5h during 1 Jul - 15 Sep, otherwise 9h | | United Kingdom | 8h | - | ### FTE Impact - FTE is stored with at least 2 decimal places (e.g. 0.50, 0.80) - FTE can change per month (contract changes, joiners/leavers) - Part-time 50% in Germany: 8h * 0.50 = 4h effective daily hours - Part-time 80% in India: 9h * 0.80 = 7.2h effective daily hours ## Schema Changes ### New: `Country` model ```prisma model Country { id String @id @default(cuid()) code String @unique // ISO 3166-1 alpha-2 (DE, IN, ES, ...) name String // Display name dailyWorkingHours Float @default(8.0) // Base daily hours scheduleRules Json? @db.JsonB // For Spain-style variable schedules createdAt DateTime @default(now()) updatedAt DateTime @updatedAt metroCities MetroCity[] resources Resource[] } ``` ### New: `MetroCity` model ```prisma model MetroCity { id String @id @default(cuid()) name String // e.g. "Munich", "Stuttgart" countryId String country Country @relation(fields: [countryId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt resources Resource[] @@unique([countryId, name]) } ``` ### Resource model extensions ```prisma model Resource { // ... existing fields ... fte Float @default(1.0) // Already exists, ensure 2+ decimals countryId String? country Country? @relation(fields: [countryId], references: [id]) metroCityId String? metroCity MetroCity? @relation(fields: [metroCityId], references: [id]) } ``` Note: Resource already has `postalCode` and `federalState` from Phase 9 (Vacation Pro). The new `countryId` + `metroCityId` are complementary — `federalState` stays for German public holiday derivation, `countryId` drives daily hours and SAH. ## Engine: SAH Calculator Location: `packages/engine/src/sah/calculator.ts` ```typescript interface SAHInput { country: { dailyWorkingHours: number; scheduleRules?: SpainScheduleRule }; fte: number; periodStart: Date; periodEnd: Date; publicHolidays: Date[]; // from country + metro city absenceDays: Date[]; // vacation, illness, other } interface SAHResult { grossWorkingDays: number; publicHolidayDays: number; absenceDays: number; netWorkingDays: number; effectiveHoursPerDay: number; standardAvailableHours: number; } function calculateSAH(input: SAHInput): SAHResult; ``` Spain schedule handling: ```typescript interface SpainScheduleRule { type: 'spain'; fridayHours: number; // 6.5 summerPeriod: { from: string; to: string }; // "07-01" to "09-15" summerHours: number; // 6.5 regularHours: number; // 9.0 } function getSpainDailyHours(date: Date, rule: SpainScheduleRule): number; ``` ## Public Holidays Phase 9 already has `computeEaster()` and `getPublicHolidays(year, state?)` for German states (Bavaria). This needs extension: - Move to a country-aware public holiday system - Support metro city specific holidays (e.g. Munich Assumption Day) - Seed known holidays per country from a reference table - Admin UI to manage/override holidays per country+city Existing Phase 9 German holiday logic becomes the "Germany" implementation within the broader system. ## FTE Monthly Tracking The chargeability report shows FTE can vary by month per resource. Options: 1. **Simple:** Single `fte` field on Resource, changed when contract changes (current approach) 2. **Monthly:** `ResourceFTEHistory` model tracking FTE per month with effective date Recommendation: Start with option 1 (single FTE field). Add history tracking as a follow-up if month-over-month FTE change reporting is needed. The current Resource model already has `fte`. ## UI Changes ### Admin: Country + Metro City Management - `/admin/countries` page - CRUD for countries with daily working hours and schedule rules - Nested metro city management per country - Seed data for the 8 known countries + their metro cities ### Resource: Country + Metro City Assignment - `ResourceModal.tsx` gets Country dropdown (required) and Metro City dropdown (filtered by country) - Auto-suggest metro city based on existing `postalCode` where possible ### Dashboard/Reports: SAH Display - Resource detail page shows SAH for current period - Chargeability report uses SAH as the denominator ## Migration 1. Create Country + MetroCity tables with seed data 2. Backfill existing resources: derive country from `federalState` (Germany) or leave null 3. Admin assigns countries to remaining resources ## Dependencies - Public holiday system must be extended to all countries (not just German states) - Vacation/absence system (Phase 9) feeds into SAH absence deduction - Chargeability report (separate plan) consumes SAH as primary metric ## Acceptance Criteria - [ ] Country model with daily working hours and optional schedule rules - [ ] Metro City model linked to Country - [ ] Resource linked to Country + Metro City - [ ] SAH calculator in engine: `calculateSAH()` pure function - [ ] Spain variable schedule correctly handled - [ ] FTE reduces effective daily hours: `effectiveHours = dailyHours * fte` - [ ] Public holidays extended beyond Germany to all 8 countries - [ ] Admin UI for Country + Metro City CRUD - [ ] Resource modal with Country + Metro City selection - [ ] Seed data for all 8 countries and their metro cities