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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user