6.9 KiB
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
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
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
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
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:
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:
- Simple: Single
ftefield on Resource, changed when contract changes (current approach) - Monthly:
ResourceFTEHistorymodel 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/countriespage- 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.tsxgets Country dropdown (required) and Metro City dropdown (filtered by country)- Auto-suggest metro city based on existing
postalCodewhere possible
Dashboard/Reports: SAH Display
- Resource detail page shows SAH for current period
- Chargeability report uses SAH as the denominator
Migration
- Create Country + MetroCity tables with seed data
- Backfill existing resources: derive country from
federalState(Germany) or leave null - 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