feat: Nearshore-Ratio indicator per project
Engine (packages/engine): - calculateShoringRatio() pure function: onshore/offshore hours, country breakdown, threshold check, weighted by hours not headcount - 12 unit tests: empty, 100% onshore/offshore, mixed ratios, custom threshold, case-insensitive, unknown country, FTE weighting Schema: - Project.shoringThreshold (default 55%) — per-project configurable - Project.onshoreCountryCode (default "DE") — configurable onshore country API (project router): - getShoringRatio query: loads assignments with resource.country, computes ratio, returns full breakdown - update mutation: accepts shoringThreshold + onshoreCountryCode UI: - ShoringIndicator: stacked horizontal bar with country segments, severity badge (green/yellow/red), hover tooltip, dark theme - ShoringBadge: mini colored dot + % for project list column - ProjectModal: "Max Offshore %" number input - Project detail: indicator after budget status card - Project list: "Shoring" column (default hidden, toggleable) AI Assistant: - get_shoring_ratio tool: human-readable breakdown with threshold alert Colors: green (<threshold-10), yellow (threshold-10 to threshold), red (>=threshold) Default: 55% offshore threshold, "DE" as onshore country Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -1,19 +1,17 @@
|
||||
# Admin Set Password — Plan
|
||||
# Nearshore-Ratio Indikator — Plan
|
||||
|
||||
## Anforderungsanalyse
|
||||
|
||||
**Was:** Admins sollen im User-Management das Passwort fuer beliebige User setzen/zuruecksetzen koennen.
|
||||
**Was:** Ein Nearshore/Offshore-Ratio Indikator pro Projekt, der zeigt wieviel % der gebuchten Ressourcen Standort Deutschland vs. andere Standorte haben.
|
||||
|
||||
**Ist-Zustand:**
|
||||
- `user.create` Mutation hat Passwort-Support (Argon2 Hashing via `@node-rs/argon2`)
|
||||
- Kein `setPassword` oder `resetPassword` Mutation fuer bestehende User
|
||||
- UsersClient hat Passwort nur im Create-Formular, nicht im Edit-Bereich
|
||||
**Kernkonzept:**
|
||||
- **Onshore** = Resource hat `countryId` → Country mit Code "DE" (Deutschland)
|
||||
- **Offshore/Nearshore** = Resource hat `countryId` → Country mit Code != "DE" (oder kein Country gesetzt)
|
||||
- **Shoring-Ratio** = (Offshore-Stunden / Gesamt-Stunden) * 100
|
||||
- **Threshold** = Default 55%, pro Projekt aenderbar
|
||||
- **Anzeige** = Farbiger Indikator (gruen < Threshold, rot >= Threshold)
|
||||
|
||||
**Soll-Zustand:**
|
||||
- Neuer `setPassword` adminProcedure im user Router
|
||||
- "Set Password" Button pro User im Admin-UI
|
||||
- Passwort-Modal mit Eingabe + Bestaetigung
|
||||
- Audit-Log Eintrag bei Passwort-Aenderung (ohne Passwort-Wert!)
|
||||
**Datenquelle:** Assignments eines Projekts → Resource.countryId → Country.code
|
||||
|
||||
---
|
||||
|
||||
@@ -21,54 +19,159 @@
|
||||
|
||||
| Paket | Dateien | Art der Aenderung |
|
||||
|-------|---------|------------------|
|
||||
| `packages/api` | `src/router/user.ts` | **edit** — `setPassword` Mutation hinzufuegen |
|
||||
| `apps/web` | `src/components/admin/UsersClient.tsx` | **edit** — "Set Password" Button + Modal |
|
||||
| `packages/db` | `prisma/schema.prisma` | **edit** — `shoringThreshold Int?` auf Project |
|
||||
| `packages/engine` | `src/allocation/shoring-ratio.ts` | **create** — Pure Berechnung |
|
||||
| `packages/engine` | `src/__tests__/shoring-ratio.test.ts` | **create** — Unit Tests |
|
||||
| `packages/api` | `src/router/project.ts` | **edit** — `getShoringRatio` Query + Threshold im Update |
|
||||
| `packages/api` | `src/router/assistant-tools.ts` | **edit** — `get_shoring_ratio` AI Tool |
|
||||
| `apps/web` | `src/components/projects/ShoringIndicator.tsx` | **create** — Visueller Indikator |
|
||||
| `apps/web` | `src/app/(app)/projects/[id]/page.tsx` | **edit** — Indikator auf Detail-Seite |
|
||||
| `apps/web` | `src/app/(app)/projects/ProjectsClient.tsx` | **edit** — Indikator-Spalte in Projektliste |
|
||||
| `apps/web` | `src/components/projects/ProjectModal.tsx` | **edit** — Threshold-Feld im Edit |
|
||||
| `apps/web` | `src/components/dashboard/widgets/ProjectHealthWidget.tsx` | **edit** — Optional: Shoring in Health-Score |
|
||||
|
||||
---
|
||||
|
||||
## Task-Liste
|
||||
|
||||
- [ ] **Task 1:** `setPassword` Mutation → `packages/api/src/router/user.ts`
|
||||
- Input: `{ userId: string, password: string }` (min 8 Zeichen)
|
||||
- adminProcedure (nur Admins duerfen Passwoerter setzen)
|
||||
- Hash mit `@node-rs/argon2` (gleiches Pattern wie `create`)
|
||||
- `db.user.update({ where: { id }, data: { passwordHash } })`
|
||||
- Audit-Log: `createAuditEntry({ entityType: "User", action: "UPDATE", summary: "Password reset by admin" })`
|
||||
- KEIN Passwort-Wert im Audit-Log (Sicherheit!)
|
||||
### Phase 1: Schema + Engine (sequenziell)
|
||||
|
||||
- [ ] **Task 2:** UI — "Set Password" Button + Modal → `UsersClient.tsx`
|
||||
- Pro User-Zeile: "Set Password" Button (Schloss-Icon)
|
||||
- Klick oeffnet AnimatedModal mit:
|
||||
- User-Name als Titel
|
||||
- Neues Passwort Input (min 8 Zeichen)
|
||||
- Passwort bestaetigen Input
|
||||
- Validierung: Passwoerter muessen uebereinstimmen
|
||||
- Submit-Button (disabled wenn <8 Zeichen oder nicht matching)
|
||||
- Success: Toast "Password updated", Modal schliessen
|
||||
- Error: Fehlermeldung anzeigen
|
||||
- [ ] **Task 1:** Schema erweitern → `packages/db/prisma/schema.prisma`
|
||||
- `shoringThreshold Int? @default(55)` auf dem Project Model
|
||||
- Repraesntiert den kritischen Shoring-Prozentsatz (Default 55%)
|
||||
- `prisma db push`
|
||||
|
||||
- [ ] **Task 2:** Pure Shoring-Ratio Berechnung → `packages/engine/src/allocation/shoring-ratio.ts`
|
||||
```typescript
|
||||
interface ShoringInput {
|
||||
resourceId: string;
|
||||
countryCode: string | null; // "DE", "ES", "IN", etc.
|
||||
hoursPerDay: number;
|
||||
workingDays: number; // oder totalHours direkt
|
||||
}
|
||||
|
||||
interface ShoringResult {
|
||||
totalHours: number;
|
||||
onshoreHours: number; // countryCode === "DE"
|
||||
offshoreHours: number; // countryCode !== "DE"
|
||||
offshoreRatio: number; // 0-100 (%)
|
||||
onshoreRatio: number; // 0-100 (%)
|
||||
threshold: number; // Default 55
|
||||
isAboveThreshold: boolean;
|
||||
byCountry: Record<string, { hours: number; pct: number; resourceCount: number }>;
|
||||
}
|
||||
|
||||
export function calculateShoringRatio(
|
||||
assignments: ShoringInput[],
|
||||
threshold?: number, // Default 55
|
||||
onshoreCountryCode?: string, // Default "DE"
|
||||
): ShoringResult
|
||||
```
|
||||
|
||||
- [ ] **Task 3:** Unit Tests → `packages/engine/src/__tests__/shoring-ratio.test.ts`
|
||||
- 100% Deutsche Ressourcen → offshoreRatio = 0, isAboveThreshold = false
|
||||
- 100% Offshore → offshoreRatio = 100, isAboveThreshold = true
|
||||
- 50/50 Mix → offshoreRatio = 50, isAboveThreshold = false (< 55)
|
||||
- 60/40 Mix → offshoreRatio = 60, isAboveThreshold = true (> 55)
|
||||
- Custom Threshold (30%) → 40% Offshore = above threshold
|
||||
- Leere Assignments → offshoreRatio = 0
|
||||
- Resource ohne Country → zaehlt als Offshore
|
||||
|
||||
- [ ] **Task 4:** Export → `packages/engine/src/allocation/index.ts`
|
||||
|
||||
### Phase 2: API + UI (parallel)
|
||||
|
||||
- [ ] **Task 5:** API Query → `packages/api/src/router/project.ts`
|
||||
- `getShoringRatio` Query (protectedProcedure):
|
||||
- Input: `{ projectId: string }`
|
||||
- Laedt Assignments mit Resource.country
|
||||
- Ruft `calculateShoringRatio()` auf
|
||||
- Returns: `ShoringResult`
|
||||
- `update` Mutation erweitern: `shoringThreshold` im Input akzeptieren
|
||||
- `getById` erweitern: `shoringThreshold` im Response zurueckgeben
|
||||
|
||||
- [ ] **Task 6:** Visueller Indikator → `ShoringIndicator.tsx`
|
||||
- Kompakte Anzeige:
|
||||
```
|
||||
[====DE 45%====|==ES 30%==|IN 25%] ⚠ 55% offshore (Limit: 55%)
|
||||
```
|
||||
- Stacked horizontal bar: Laender-Segmente farbig
|
||||
- Gruen/Gelb/Rot Badge je nach Threshold
|
||||
- Gruen: < Threshold - 10pp
|
||||
- Gelb: Threshold - 10pp bis Threshold
|
||||
- Rot: >= Threshold
|
||||
- Tooltip: Aufschluesselung nach Land
|
||||
- Props: `projectId: string` (laedt Daten selbst via tRPC)
|
||||
|
||||
- [ ] **Task 7:** Threshold im ProjectModal → `ProjectModal.tsx`
|
||||
- Neues Feld: "Max Offshore %" (Number Input, 0-100, Default 55)
|
||||
- Unter "Budget" oder als eigene Section "Shoring"
|
||||
- Label: "Maximum Offshore/Nearshore Ratio (%)"
|
||||
- Hinweis: "Alert when offshore staffing exceeds this percentage"
|
||||
|
||||
- [ ] **Task 8:** Indikator auf Projekt-Detail-Seite → `projects/[id]/page.tsx`
|
||||
- `<ShoringIndicator projectId={id} />` neben dem Budget-Status
|
||||
- Oder als eigene Karte unter den Budget-Karten
|
||||
|
||||
- [ ] **Task 9:** Indikator-Spalte in Projektliste → `ProjectsClient.tsx`
|
||||
- Neue Spalte "Shoring" mit Mini-Indikator (nur Badge gruen/gelb/rot + %)
|
||||
- InfoTooltip: "Offshore staffing ratio vs project threshold"
|
||||
|
||||
### Phase 3: AI + Dashboard (parallel)
|
||||
|
||||
- [ ] **Task 10:** AI Assistant Tool → `assistant-tools.ts`
|
||||
- `get_shoring_ratio` Tool:
|
||||
- Input: `{ projectId: string }`
|
||||
- Returns: menschenlesbare Aufschluesselung
|
||||
- "Project X: 45% onshore (DE), 55% offshore (ES 30%, IN 25%). ⚠ Above 55% threshold."
|
||||
|
||||
- [ ] **Task 11:** Optional: Dashboard Widget oder ProjectHealth Integration
|
||||
- Shoring-Warnung in ProjectHealthWidget einbauen
|
||||
- Oder neues Mini-Widget "Shoring Alerts" (Projekte ueber Threshold)
|
||||
|
||||
---
|
||||
|
||||
## Abhaengigkeiten
|
||||
|
||||
- Task 1 muss vor Task 2 (API benoetigt fuer UI)
|
||||
- Beide Tasks koennen in einer Sequenz implementiert werden (gleicher Agent)
|
||||
```
|
||||
Task 1 (Schema) → Task 2 (Engine) → Task 3 (Tests) → Task 4 (Export)
|
||||
Task 4 → Task 5 (API)
|
||||
Task 5 → Task 6 + Task 7 + Task 8 + Task 9 (alle parallel)
|
||||
Task 5 → Task 10 (AI Tool)
|
||||
Task 5 → Task 11 (Dashboard)
|
||||
```
|
||||
|
||||
- Tasks 6-9 **vollstaendig parallel** (verschiedene Dateien)
|
||||
- Tasks 10-11 parallel zu Tasks 6-9
|
||||
|
||||
---
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] `pnpm test:unit` laeuft gruen (inkl. neue shoring-ratio Tests)
|
||||
- [ ] `pnpm --filter @planarchy/web exec tsc --noEmit` — keine neuen Errors
|
||||
- [ ] Admin kann Passwort fuer beliebigen User setzen
|
||||
- [ ] Passwort wird mit Argon2 gehasht (nicht plaintext gespeichert)
|
||||
- [ ] Audit-Log Eintrag wird erstellt (ohne Passwort-Wert)
|
||||
- [ ] Min 8 Zeichen Validierung im UI und API
|
||||
- [ ] Passwort-Bestaetigung muss uebereinstimmen
|
||||
- [ ] **Projekt-Detail:** Shoring-Indikator sichtbar mit Laender-Aufschluesselung
|
||||
- [ ] **Projektliste:** Shoring-Badge-Spalte (gruen/gelb/rot)
|
||||
- [ ] **Projekt-Edit:** Threshold aenderbar (Default 55%)
|
||||
- [ ] **AI Assistant:** "Wie ist der Shoring-Mix bei Projekt X?" liefert Antwort
|
||||
- [ ] **Farben:** Gruen < 45%, Gelb 45-55%, Rot >= 55% (bei Default-Threshold)
|
||||
- [ ] **Resource ohne Country:** Zaehlt als Offshore
|
||||
- [ ] **Projekt ohne Assignments:** Zeigt "No data" statt Fehler
|
||||
|
||||
---
|
||||
|
||||
## Risiken & offene Fragen
|
||||
|
||||
- **Sicherheit:** Nur ADMIN-Rolle darf Passwoerter setzen (adminProcedure)
|
||||
- **Audit:** Passwort-Wert DARF NICHT im Audit-Log erscheinen
|
||||
- **UX:** Soll der User benachrichtigt werden? → Vorschlag: Nein, Admin setzt manuell und teilt dem User das Passwort separat mit
|
||||
### Risiken
|
||||
- **Country-Daten unvollstaendig:** Viele Resources haben moeglicherweise kein `countryId` gesetzt
|
||||
→ Mitigation: Zaehlt als Offshore + Warning "X resources have no country assigned"
|
||||
- **Stunden vs Headcount:** Soll nach Stunden oder Koepfen gemessen werden?
|
||||
→ Vorschlag: Stunden (gewichtet), da ein 50%-FTE weniger zaehlt als ein 100%-FTE
|
||||
|
||||
### Offene Fragen
|
||||
1. **Nur "DE" als Onshore?** Oder soll der Onshore-Laendercode konfigurierbar sein?
|
||||
→ Vorschlag: Default "DE", aber konfigurierbar pro Projekt
|
||||
2. **Welche Assignment-Status zaehlen?** Nur CONFIRMED? Auch PROPOSED?
|
||||
→ Vorschlag: CONFIRMED + PROPOSED (beide relevant fuer Planung)
|
||||
3. **Soll der Indikator auch in der Timeline sichtbar sein?**
|
||||
→ Vorschlag: Erst mal nur Projektliste + Detail-Seite
|
||||
|
||||
Reference in New Issue
Block a user