chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
# Plan: Country-Driven Standard Available Hours (SAH) and FTE
|
||||
|
||||
**Date:** 2026-03-13
|
||||
**Status:** Draft
|
||||
**Depends on:** OrgUnit hierarchy, Resource model extensions
|
||||
|
||||
## Problem
|
||||
|
||||
Planarchy 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
|
||||
Reference in New Issue
Block a user