feat: AI assistant (HartBOT), demand filling, budget-per-role, project favorites, and UX improvements

AI Assistant (HartBOT):
- Chat panel with inline layout, session persistence, message history (up-arrow recall)
- OpenAI function calling with 20+ tools (search, navigate, create/cancel allocations, update status)
- RBAC-aware tool filtering, fuzzy search with word-level matching
- Navigation actions (router.push) and data invalidation after mutations
- Country/metro city/org unit/role filtering on resource search

Demand Filling Enhancements:
- Two-phase fill modal: plan multiple resources, then confirm & assign all at once
- Availability preview per resource (available/partial/conflict days, existing bookings)
- Coverage bar showing demand hours distribution across assigned resources
- Fill demand from project detail page (new Assign button per demand)
- Fixed: filled demands no longer shown on timeline, demand bars no longer overlap

Budget per Role:
- DemandRequirement.budgetCents field (schema + API + UI)
- Project wizard step 3: budget input per role with allocation summary bar
- Project detail: allocated vs booked budget per demand
- Fill demand modal: role budget display with cost estimates
- AllocationModal: budget field for demand editing

Project Favorites:
- User.favoriteProjectIds (JSONB) with toggle API
- Star button on projects list and detail page (optimistic updates)
- "My Projects" dashboard widget (favorites + responsible person projects)

Project Management:
- Edit project from detail page (ProjectModal integration)
- Edit demands from detail page (AllocationModal integration)
- Admin-only project deletion (cascades assignments + demands)
- Create user accounts from admin panel

Timeline Fixes:
- Country multi-select filter with backend support
- URL param sync for same-page navigation (AI assistant integration)
- Demand lane stacking (no more overlapping bars)
- Single-day booking resize handles (always visible, min 6px)
- Single-day resize allowed (start === end)
- "All Clients" toggle (select all / deselect all)

Other Fixes:
- crypto.randomUUID fallback for non-secure contexts
- Chat message limit raised (200 max, client sends last 40)
- Status dropdown portal (no longer clipped by table overflow)
- Cents display restored in budget views (2 decimal places)
- Allocations grouped view with project sub-groups (collapsed by default)
- Server-side resource search for project wizard (no 500 limit)

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-16 15:31:48 +01:00
parent f5551e33c7
commit b0e55786c3
44 changed files with 4516 additions and 609 deletions
+73 -236
View File
@@ -1,21 +1,21 @@
# Plan: Calculation Rules Engine
# Plan: Budget per Role / Demand
## Anforderungsanalyse
Planarchy berechnet Kosten und Chargeability aktuell hart verdrahtet: Vacation blockiert Stunden komplett, Sick Days werden nicht modelliert, und die Chargeability-Berechnung kennt keine Regeln fuer die Entkopplung von "Person ist chargeable" vs. "Projekt wird belastet".
Jede Staffing-Demand (Rolle) in einem Projekt soll ein eigenes Budget bekommen. Aktuell gibt es nur ein einziges `budgetCents` auf Projektebene. Ziel:
**Gewuenschtes Verhalten (Beispiele):**
1. **DemandRequirement** bekommt ein `budgetCents` Feld (wie viel Budget ist dieser Rolle zugewiesen)
2. **StaffingRequirement** (JSONB auf Project) bekommt ein optionales `budgetCents` Feld fuer den Wizard
3. **Project Wizard Step 3** zeigt Budget-Input pro Rolle + verbleibendes unverteiltes Projekt-Budget
4. **Project Detail Page** zeigt pro Demand: zugewiesenes Budget vs. gebuchtes Budget (aus Assignments berechnet)
5. **Fill Demand Modal** zeigt verbleibendes Rollen-Budget beim Zuweisen von Ressourcen
| Szenario | Person chargeable? | Projekt belastet? | Heute |
|----------|-------------------|-------------------|-------|
| Krank + gebucht auf Projekt | Ja | Nein | Nicht modelliert |
| Urlaub + gebucht auf Projekt | Ja | Nein | Urlaub blockiert Stunden komplett |
| Urlaub, nicht gebucht | Ja | — | Urlaub mindert SAH |
| Normal gebucht | Ja | Ja | Korrekt |
### Architektur-Entscheidung
**Kernidee:** Ein regelbasiertes System, das pro Tag entscheidet:
1. **costEffect** — Wird der Tag dem Projekt belastet? (`charge` / `zero` / `reduce`)
2. **chargeabilityEffect** — Zaehlt der Tag fuer die Chargeability der Person? (`count` / `skip`)
`budgetCents` als **explizite Spalte** auf `DemandRequirement` (nicht in `metadata` JSONB), weil:
- Typsicher, indizierbar, aggregierbar via SQL
- Konsistent mit dem Muster auf `Project.budgetCents`
- Default `0` = kein Budget zugewiesen (abwaertskompatibel)
---
@@ -23,264 +23,101 @@ Planarchy berechnet Kosten und Chargeability aktuell hart verdrahtet: Vacation b
| Paket | Dateien | Art der Aenderung |
|-------|---------|-----------------|
| shared | `src/types/calculation-rules.ts` | create |
| shared | `src/schemas/calculation-rules.schema.ts` | create |
| shared | `src/types/index.ts` | edit (re-export) |
| db | `prisma/schema.prisma` | edit (neues Model) |
| engine | `src/rules/engine.ts` | create |
| engine | `src/rules/default-rules.ts` | create |
| engine | `src/rules/index.ts` | create |
| engine | `src/allocation/calculator.ts` | edit (Rules-Integration) |
| engine | `src/chargeability/calculator.ts` | edit (Rules-Integration) |
| engine | `src/budget/monitor.ts` | edit (Rules-Integration) |
| engine | `src/__tests__/rules-engine.test.ts` | create |
| engine | `src/__tests__/calculator-rules.test.ts` | create |
| api | `src/router/calculation-rules.ts` | create |
| api | `src/router/index.ts` | edit (Router registrieren) |
| api | `src/router/timeline.ts` | edit (Rules durchreichen) |
| api | `src/router/allocation.ts` | edit (Rules bei Berechnung) |
| web | `src/app/(app)/admin/calculation-rules/page.tsx` | create |
| web | `src/components/admin/CalculationRulesClient.tsx` | create |
| web | `src/components/layout/AppShell.tsx` | edit (Navigation) |
| `packages/db` | `prisma/schema.prisma` | **edit**`budgetCents` auf DemandRequirement |
| `packages/shared` | `src/types/project.ts` | **edit**`budgetCents?` auf StaffingRequirement |
| `packages/shared` | `src/types/allocation.ts` | **edit**`budgetCents` auf DemandRequirementRecord |
| `packages/shared` | `src/schemas/allocation.schema.ts` | **edit**`budgetCents` in CreateDemandRequirementSchema |
| `packages/api` | `src/router/allocation.ts` | **edit** — budgetCents durchreichen in create/update |
| `packages/api` | `src/router/project.ts` | **edit** — bei Demand-Erstellung aus Wizard budgetCents uebernehmen |
| `apps/web` | `src/components/projects/ProjectWizard.tsx` | **edit** — Step 3: Budget-Input pro Rolle + Restbudget-Anzeige |
| `apps/web` | `src/components/projects/ProjectDemandsTable.tsx` | **edit** — Spalten: Allocated Budget, Booked Budget |
| `apps/web` | `src/components/allocations/FillOpenDemandModal.tsx` | **edit** — Rollen-Budget-Anzeige |
---
## Architektur
## Task-Liste
### Datenmodell
### Task 1: Prisma Schema — `budgetCents` auf DemandRequirement
```prisma
model CalculationRule {
id String @id @default(cuid())
name String // "Sick Leave — Chargeable, No Project Cost"
description String?
Datei: `packages/db/prisma/schema.prisma`
// ── Matching ──
triggerType AbsenceTrigger // SICK, VACATION, PUBLIC_HOLIDAY, CUSTOM
// Optional narrowing (null = all):
projectId String?
orderType OrderType? // CHARGEABLE, INTERNAL, INVESTMENT
- [ ] `budgetCents Int @default(0)` auf DemandRequirement hinzufuegen
- [ ] `pnpm db:push` ausfuehren (generiert Prisma Client)
- [ ] Dev-Server neustarten (`.next/` Cache loeschen)
// ── Effects ──
costEffect CostEffect // CHARGE, ZERO, REDUCE
costReductionPercent Int? // nur bei REDUCE (0-100)
chargeabilityEffect ChargeabilityEffect // COUNT, SKIP
### Task 2: Shared Types aktualisieren
// ── Ordering ──
priority Int @default(0) // hoehere Prioritaet gewinnt
isActive Boolean @default(true)
Dateien:
- `packages/shared/src/types/project.ts``budgetCents?: number` auf `StaffingRequirement`
- `packages/shared/src/types/allocation.ts``budgetCents: number` auf `DemandRequirementRecord`
- `packages/shared/src/schemas/allocation.schema.ts``budgetCents: z.number().int().min(0).default(0)` in `CreateDemandRequirementBaseSchema`
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
### Task 3: API — budgetCents durchreichen
project Project? @relation(fields: [projectId], references: [id])
Datei: `packages/api/src/router/allocation.ts`
@@map("calculation_rules")
}
- [ ] `createDemandRequirement``budgetCents` aus Input an Prisma create weitergeben
- [ ] `updateDemandRequirement``budgetCents` updatebar machen
- [ ] `checkResourceAvailability` — optional: gebuchte Kosten vs. Rollen-Budget zurueckgeben
enum AbsenceTrigger {
SICK
VACATION
PUBLIC_HOLIDAY
CUSTOM
}
Datei: `packages/api/src/router/project.ts`
enum CostEffect {
CHARGE // normaler Kostenauftrag ans Projekt
ZERO // keine Kosten ans Projekt
REDUCE // reduzierte Kosten (costReductionPercent)
}
- [ ] Bei Projekt-Erstellung mit StaffingReqs: wenn `staffingReq.budgetCents` vorhanden, an DemandRequirement weitergeben
enum ChargeabilityEffect {
COUNT // Person zaehlt als chargeable
SKIP // Person zaehlt nicht (Tag wird aus SAH-Nenner genommen)
}
```
### Task 4: Project Wizard Step 3 — Budget-Input pro Rolle
### Rule Matching (Engine)
Datei: `apps/web/src/components/projects/ProjectWizard.tsx`
```
findMatchingRule(day, absenceType, projectId?, orderType?):
candidates = rules.filter(r =>
r.isActive &&
r.triggerType === absenceType &&
(r.projectId === null || r.projectId === projectId) &&
(r.orderType === null || r.orderType === orderType)
)
// Spezifischere Regeln gewinnen, dann priority
return candidates.sort(bySpecificityThenPriority)[0] ?? DEFAULT_RULE
```
- [ ] Pro StaffingRequirement-Karte: neues Feld "Role Budget (EUR)" (Input, konvertiert zu Cents)
- [ ] Oben im Step: Anzeige "Project Budget: X EUR | Allocated: Y EUR | Remaining: Z EUR"
- [ ] Farbkodierung: gruen wenn alles verteilt, amber wenn Rest, rot wenn ueberallokiert
- [ ] Budget-Wert wird in `state.staffingReqs[i].budgetCents` gespeichert
**Specificity scoring:**
| Filter combination | Score |
|-------------------|-------|
| projectId + orderType | 3 |
| projectId only | 2 |
| orderType only | 1 |
| global (no filter) | 0 |
### Task 5: Project Detail Page — Budget-Spalten pro Demand
### Default Rules (Seed)
Datei: `apps/web/src/components/projects/ProjectDemandsTable.tsx`
| Name | Trigger | Cost | Chargeability |
|------|---------|------|---------------|
| Urlaub — Person chargeable, Projekt nicht belastet | VACATION | ZERO | COUNT |
| Krankheit — Person chargeable, Projekt nicht belastet | SICK | ZERO | COUNT |
| Feiertag — kein Effekt | PUBLIC_HOLIDAY | ZERO | SKIP |
- [ ] Neue Spalte "Allocated Budget" — zeigt `demand.budgetCents` formatiert als EUR
- [ ] Neue Spalte "Booked Budget" — berechnet: Summe der `dailyCostCents * Arbeitstage` aller Assignments dieses Demands
- Hinweis: Die Demand-Daten vom Server enthalten `assignments[]` — daraus berechnen
- [ ] Neue Spalte "Remaining" — Allocated minus Booked
- [ ] Farbkodierung: gruen wenn unter Budget, rot wenn ueber Budget
### Integration Points
### Task 6: Fill Demand Modal — Rollen-Budget anzeigen
**1. `calculateAllocation()` (engine/allocation/calculator.ts)**
Datei: `apps/web/src/components/allocations/FillOpenDemandModal.tsx`
Aktuell: Vacation-Tag → `effectiveHours = 0`, `costCents = 0`.
Neu: Fuer jeden Tag pruefen ob eine Regel greift. Das DailyBreakdown bekommt zwei neue Felder:
```ts
interface DailyBreakdown {
date: Date;
isWorkday: boolean;
hours: number; // effektive Stunden (wie bisher)
costCents: number; // Kosten fuer das Projekt (regel-gesteuert)
// NEU:
absenceType?: AbsenceTrigger; // was fuer ein Tag ist das?
chargeableHours: number; // Stunden die fuer Chargeability zaehlen
}
```
- Wenn `costEffect === ZERO`: `costCents = 0`, aber `chargeableHours = hoursPerDay`
- Wenn `costEffect === REDUCE`: `costCents = round(normal * (100 - reduction) / 100)`
- Wenn `chargeabilityEffect === COUNT`: `chargeableHours = hoursPerDay` (selbst wenn absent)
- Wenn `chargeabilityEffect === SKIP`: `chargeableHours = 0`
**2. `deriveResourceForecast()` (engine/chargeability/calculator.ts)**
Aktuell: `absence` Ratio ist immer 0 (kein Input).
Neu: Erhaelt `chargeableHours` pro Assignment-Slice statt nur `hoursPerDay * workingDays`.
Die Summe der chargeable Hours wird gegen SAH normiert.
**3. `computeBudgetStatus()` (engine/budget/monitor.ts)**
Aktuell: `dailyCostCents * workingDays` — nimmt an, jeder Tag kostet gleich.
Neu: Bekommt optional ein `adjustedTotalCostCents` pro Allocation, das bereits die Regel-Reduktionen enthaelt. Fallback auf bisherige Berechnung wenn keine Rules aktiv.
**4. Timeline Router (api/router/timeline.ts)**
Laedt Rules einmal, reicht sie an den Calculator durch. Rules werden gecacht (sie aendern sich selten).
---
## Task-Liste (atomare Schritte in Reihenfolge)
### Phase A: Datenmodell & Types
- [x] **A1:** Shared Types erstellen — `AbsenceTrigger`, `CostEffect`, `ChargeabilityEffect`, `CalculationRule` Interface → `packages/shared/src/types/calculation-rules.ts`
- [x] **A2:** Zod Schemas erstellen — `CreateCalculationRuleSchema`, `UpdateCalculationRuleSchema``packages/shared/src/schemas/calculation-rules.schema.ts`
- [x] **A3:** Re-exports in `packages/shared/src/types/index.ts` und `packages/shared/src/schemas/index.ts`
- [x] **A4:** Prisma Schema erweitern — `CalculationRule` Model + Enums → `packages/db/prisma/schema.prisma`
- [x] **A5:** Prisma Client regenerieren (db:push erfordert laufende DB)
### Phase B: Rules Engine (Pure Logic)
- [x] **B1:** Rule Matching Engine — `findMatchingRule()`, Specificity-Scoring → `packages/engine/src/rules/engine.ts`
- [x] **B2:** Default Rules — hartcodierte Fallback-Regeln → `packages/engine/src/rules/default-rules.ts`
- [x] **B3:** Index-Datei → `packages/engine/src/rules/index.ts`
- [x] **B4:** Tests fuer Rule Matching — 20 Tests (Specificity, Priority, Fallback, applyCostEffect) → `packages/engine/src/__tests__/rules-engine.test.ts`
### Phase C: Calculator-Integration
- [x] **C1:** `DailyBreakdown` erweitern — `absenceType`, `chargeableHours``packages/shared/src/types/engine.ts`
- [x] **C2:** `AllocationCalculationInput` erweitern — `calculationRules`, `absenceDays`, `projectId`, `orderType``packages/shared/src/types/engine.ts`
- [x] **C3:** `calculateAllocation()` anpassen — Regel-Lookup pro Tag, neue Felder befuellen → `packages/engine/src/allocation/calculator.ts`
- [x] **C4:** `AllocationCalculationResult` erweitern — `totalChargeableHours`, `totalProjectCostCents` (regelbereinigt)
- [x] **C5:** `deriveResourceForecast()``AssignmentSlice.totalChargeableHours` optional, verwendet statt `hoursPerDay*workingDays``packages/engine/src/chargeability/calculator.ts`
- [x] **C6:** `computeBudgetStatus()` — optional `adjustedTotalCostCents` pro Allocation → `packages/engine/src/budget/monitor.ts`
- [x] **C7:** Tests — 9 Tests (Sick+Booked, Vacation+Booked, Feiertag, Half-Day, REDUCE, Defaults) → `packages/engine/src/__tests__/calculator-rules.test.ts`
- [x] **C8:** Bestehende Calculator-Tests verifiziert — 283/283 bestanden, volle Rueckwaertskompatibilitaet
### Phase D: API Router
- [x] **D1:** `calculation-rules.ts` Router — CRUD (list, getById, getActive, create, update, delete) → `packages/api/src/router/calculation-rules.ts`
- [x] **D2:** Router registrieren → `packages/api/src/router/index.ts`
- [x] **D3:** Timeline Router — `loadCalculationRules()` + `buildAbsenceDays()` Helpers; `updateAllocationInline` + `applyShift` nutzen Rules → `packages/api/src/router/timeline.ts`
- [x] **D4:** Allocation Router — nutzt calculateAllocation nicht direkt (laeuft ueber Timeline); kein Handlungsbedarf
- [x] **D5:** Seed — 3 Default-Regeln einfuegen → `packages/db/src/seed.ts`
### Phase E: Admin UI
- [x] **E1:** `CalculationRulesClient.tsx` — Tabelle mit Rules, Create/Edit Modal → `apps/web/src/components/admin/CalculationRulesClient.tsx`
- [x] **E2:** Page Route → `apps/web/src/app/(app)/admin/calculation-rules/page.tsx`
- [x] **E3:** AppShell Navigation — "Calc. Rules" unter Admin-Bereich → `apps/web/src/components/layout/AppShell.tsx`
### Phase F: Sick Days Pipeline
- [x] **F1:** Timeline Router — `buildAbsenceDays()` laedt SICK/VACATION/PUBLIC_HOLIDAY mit Typ-Tag und reicht an Calculator → `packages/api/src/router/timeline.ts`
- [x] **F2:** Chargeability Report — Vacation-Query um `type`+`isHalfDay` erweitert; per-Monat AbsenceDays gebaut; `calculateAllocation()` mit Rules fuer `totalChargeableHours`; Rules aus DB geladen → `packages/api/src/router/chargeability-report.ts`
- [ ] Im Demand-Summary oben: "Role Budget: X EUR | Booked: Y EUR | Remaining: Z EUR"
- [ ] Beim Hinzufuegen einer Ressource zum Plan: geschaetzte Kosten anzeigen (LCR * verfuegbare Stunden)
- [ ] Warnung wenn geplante Kosten das Rollen-Budget ueberschreiten
---
## Abhaengigkeiten
```
A1 ─── A2 ─── A3 (shared types muessen zuerst stehen)
A4 ─── A5 (Schema vor DB push)
B1 ─── B4 (Engine vor Tests)
B2 ─── B3 (Default Rules vor Index)
A3 ──┐
├── B1 (Types muessen existieren)
A5 ──┘
B4 ──┐
├── C1 → C2 → C3 → C4 (Calculator braucht Types + Engine)
C3 ──── C5 (Chargeability braucht neue DailyBreakdown-Felder)
C3 ──── C6 (Budget braucht adjustierte Kosten)
C3 ──── C7 (Tests nach Implementation)
C4 ──── C8 (Regressionstest)
C4 ──── D1 (Router braucht funktionierende Engine)
D1 ──── D2 (Registrierung nach Router)
D1 ──── D3 (Timeline braucht Router fuer Daten-Zugriff)
D1 ──── D4
D2 ──── E1 → E2 → E3 (UI braucht API)
D3 ──── F1 (Sick Dates Pipeline braucht Rule-aware Timeline)
C5 ──── F2 (Forecast braucht neue chargeableHours)
```
**Parallelisierbar:**
- A1+A2+A3 parallel zu A4 (Types vs. Schema — keine Datei-Ueberschneidung)
- B1+B2 parallel (verschiedene Dateien)
- C5+C6 parallel (verschiedene Calculator-Dateien, nachdem C3 fertig)
- E1+E2+E3 als eigener Stream nachdem D2 fertig
- **Task 1 → Task 2 → Task 3** (sequentiell: Schema → Types → API)
- **Task 4** benoetigt Task 2 (StaffingRequirement-Typ mit budgetCents)
- **Task 5** benoetigt Task 1+3 (budgetCents auf DemandRequirement + API liefert es)
- **Task 6** benoetigt Task 5 (gleiche Berechnung)
- Task 4 und Task 5 koennen **parallel** nach Task 3
---
## Akzeptanzkriterien
- [ ] `pnpm --filter @planarchy/engine exec vitest run` — alle bestehenden Tests + neue Rules-Tests gruen
- [ ] `pnpm --filter @planarchy/api exec vitest run` — alle Tests gruen
- [ ] `pnpm --filter @planarchy/web exec tsc --noEmit`0 neue Errors
- [ ] Ohne konfigurierte Rules: exakt gleiches Verhalten wie heute (Rueckwaertskompatibilitaet)
- [ ] Default-Regeln: Urlaub+Gebucht → Person chargeable, Projekt nicht belastet
- [ ] Default-Regeln: Krank+Gebucht → Person chargeable, Projekt nicht belastet
- [ ] Admin-UI: Rules erstellen, bearbeiten, loeschen, (de-)aktivieren, priorisieren
- [ ] Budget Monitor zeigt regelkonforme Kosten (Sick/Vacation-Tage nicht auf Projekt)
- [ ] Chargeability Report zeigt korrekte Ratios (Sick/Vacation als chargeable)
- [ ] `pnpm db:push` laeuft ohne Fehler
- [ ] `pnpm test:unit` — alle Tests gruen
- [ ] `pnpm --filter @planarchy/web exec tsc --noEmit`keine neuen Errors
- [ ] Project Wizard Step 3: Budget-Input pro Rolle sichtbar, Restbudget wird live berechnet
- [ ] Project Detail `/projects/[id]`: Demands-Tabelle zeigt Allocated / Booked / Remaining Budget
- [ ] Fill Demand Modal: Rollen-Budget und geschaetzte Kosten sichtbar
- [ ] Bestehende Projekte/Demands funktionieren weiterhin (budgetCents default 0)
---
## Risiken & offene Fragen
1. **Rueckwaertskompatibilitaet** — Wenn keine Rules existieren UND kein Seed gelaufen ist, muss der Calculator sich exakt wie heute verhalten. Loesung: `DEFAULT_RULES` als Fallback in der Engine hartcodiert.
2. **Performance** — Rules werden pro Tag pro Allocation evaluiert. Bei 100 Allocations x 250 Tage = 25.000 Evaluierungen. Mitigiert durch: Rules einmal laden + im Memory halten (typisch <10 Regeln), Matching ist O(n) mit n ≈ 5-10.
3. **Sick Days nicht im Allocation-Calculator** — Aktuell kennt `calculateAllocation()` nur `vacationDates`. Es braucht einen neuen Input `sickDates` (oder generischer: `absenceDates` mit Typ-Tag). Die Daten kommen aus der Vacation-Tabelle mit `type = SICK`.
4. **Half-Day Absences** — Das aktuelle System modelliert halbe Tage (`isHalfDay` auf Vacation). Die Rules Engine muss damit umgehen: halber Krankheitstag → halbe Stunden chargeable, halbe Stunden Projektkosten.
5. **Historische Korrektheit** — Aenderungen an Rules wirken sich auf alle zukuenftigen Berechnungen aus. Es gibt kein "Versioning" von Rules. Falls gewuenscht, koennte man `validFrom`/`validTo` Felder ergaenzen — aber erst wenn der Use Case auftritt (YAGNI).
6. **SAH-Integration** — SAH-Calculator hat eigenen Absence-Abzug. Die Rules Engine darf die SAH-Berechnung nicht doppelt reduzieren. Loesung: SAH bleibt wie ist (zaehlt alle Absences ab), die Rules Engine steuert nur die **Zuordnung** der Stunden (chargeable vs. nicht, Projekt vs. nicht).
1. **Abwaertskompatibilitaet:** `@default(0)` stellt sicher, dass bestehende Demands kein Budget haben (0 = nicht gesetzt). UI sollte "Not set" anzeigen wenn 0.
2. **Budget-Berechnung Booked:** `dailyCostCents` ist pro Tag. Gebuchte Kosten = `dailyCostCents * Anzahl Arbeitstage im Zeitraum`. Diese Berechnung existiert bereits im Engine-Paket (`computeBudgetStatus`).
3. **StaffingReqs JSONB:** Die `staffingReqs` auf Project sind JSONB. Aeltere Projekte haben kein `budgetCents` darin — der Wizard muss `budgetCents ?? 0` defaulten.
4. **Budget-Ueberschreitung:** Soll weiterhin erlaubt sein (Warnung, kein Block) — konsistent mit dem bestehenden Ansatz bei Projekt-Budget.