feat: admin set password for users + fix dashboard cache error

Admin Set Password:
- New setPassword adminProcedure in user router (Argon2 hashing)
- Audit log: "Password reset by admin" (no password value logged)
- UI: per-user "Password" button with key icon in User Management
- Modal: new password + confirm, min 8 chars, mismatch validation
- Success toast + auto-close on completion

Dashboard fix:
- Corrupted .next cache causing "Cannot find module worker.js"
- Fixed by clearing .next cache and restarting dev server

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-23 09:32:38 +01:00
parent 208f866d68
commit bc6afefeae
3 changed files with 226 additions and 150 deletions
+40 -142
View File
@@ -1,61 +1,19 @@
# Dashboard Widget Filter System — Plan
# Admin Set Password — Plan
## Anforderungsanalyse
**Was:** Einheitliches Filter-System fuer alle Dashboard-Widgets. Filter-Logik soll geteilt werden statt pro Widget dupliziert.
**Was:** Admins sollen im User-Management das Passwort fuer beliebige User setzen/zuruecksetzen koennen.
**Anforderungen pro Widget:**
| Widget | Filter benoetigt |
|--------|-----------------|
| **Project Health** | Projektname (Suche), Client |
| **Budget Forecast** | Projektname (Suche), Client |
| **Chargeability Overview** | Country, Role/Chapter |
| **Skill Gap** | (optional: Skill-Suche) |
| **Resource Table** | Hat bereits: Chapter-Filter |
| **Project Table** | Hat bereits: Suche + Status-Filter |
| **Peak Times** | Hat bereits: Granularity + GroupBy |
**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
**Design-Prinzip:** Ein shared `<WidgetFilterBar>` Komponente die verschiedene Filter-Typen als deklarative Config akzeptiert. Filter-State wird via `onConfigChange` im Widget-Config persistiert (bereits vorhanden).
---
## Architektur
### Shared Filter Component
```tsx
// Deklarative Filter-Konfiguration pro Widget
const filters: WidgetFilter[] = [
{ type: "search", key: "search", placeholder: "Filter projects..." },
{ type: "select", key: "clientId", label: "Client", options: clients },
{ type: "select", key: "countryId", label: "Country", options: countries },
{ type: "select", key: "roleId", label: "Role", options: roles },
];
<WidgetFilterBar
filters={filters}
values={config} // Aktueller Filter-State aus Widget-Config
onChange={onConfigChange} // Persistiert in localStorage via Dashboard-Layout
/>
```
### Filter-Typen
| Typ | UI-Element | Use Cases |
|-----|-----------|-----------|
| `search` | Text-Input mit Lupe | Projektname, Ressourcenname |
| `select` | Dropdown | Client, Country, Role, Status |
| `toggle` | Checkbox | Include Proposed, Show Inactive |
### Daten-Flow
```
Widget Config (localStorage)
↓ values
WidgetFilterBar → onChange → onConfigChange → persistiert
↓ values
Widget Query (tRPC) → gefilterte Daten
```
**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!)
---
@@ -63,114 +21,54 @@ Widget Query (tRPC) → gefilterte Daten
| Paket | Dateien | Art der Aenderung |
|-------|---------|------------------|
| `apps/web` | `src/components/dashboard/WidgetFilterBar.tsx` | **create**Shared Filter-Komponente |
| `apps/web` | `src/hooks/useWidgetFilterOptions.ts` | **create**Hook fuer gemeinsame Filter-Optionen (clients, countries, roles) |
| `apps/web` | `src/components/dashboard/widgets/ProjectHealthWidget.tsx` | **edit** — Filter integrieren |
| `apps/web` | `src/components/dashboard/widgets/BudgetForecastWidget.tsx` | **edit** — Filter integrieren |
| `apps/web` | `src/components/dashboard/widgets/ChargeabilityWidget.tsx` | **edit** — Filter integrieren |
| `apps/web` | `src/components/dashboard/widgets/SkillGapWidget.tsx` | **edit** — Optional: Skill-Suche |
| `apps/web` | `src/components/dashboard/widgets/DemandWidget.tsx` | **edit** — Optional: Client/Chapter Filter |
| `apps/web` | `src/components/dashboard/widgets/TopValueWidget.tsx` | **edit** — Optional: Chapter Filter |
| `packages/api` | `src/router/user.ts` | **edit**`setPassword` Mutation hinzufuegen |
| `apps/web` | `src/components/admin/UsersClient.tsx` | **edit**"Set Password" Button + Modal |
---
## Task-Liste
### Phase 1: Shared Infrastructure
- [ ] **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!)
- [ ] **Task 1:** `useWidgetFilterOptions` Hook erstellen → `src/hooks/useWidgetFilterOptions.ts`
- Cached Queries fuer Clients, Countries, Roles (alle mit `staleTime: 300_000`)
- Gibt `{ clients, countries, roles, chapters }` als `{ value: string, label: string }[]` zurueck
- Chapters extrahiert aus Resource-Daten oder als dedizierte Query
- Nur einmal pro Dashboard geladen, von allen Widgets geteilt
- [ ] **Task 2:** `WidgetFilterBar` Komponente erstellen → `src/components/dashboard/WidgetFilterBar.tsx`
```typescript
interface WidgetFilter {
type: "search" | "select" | "toggle";
key: string; // Config-Schluessel (z.B. "clientId")
label?: string; // Display-Label
placeholder?: string; // Fuer search/select
options?: { value: string; label: string }[]; // Fuer select
}
interface WidgetFilterBarProps {
filters: WidgetFilter[];
values: Record<string, unknown>;
onChange: (update: Record<string, unknown>) => void;
}
```
- Kompaktes Layout: horizontal, passt in Widget-Header-Bereich
- Kleine Inputs (text-xs, py-1) damit sie nicht zu viel Platz nehmen
- "Reset" Button wenn Filter aktiv
- Dark-Theme Support
### Phase 2: Widget Integration (parallel)
- [ ] **Task 3:** ProjectHealthWidget + Filter → `ProjectHealthWidget.tsx`
- Filter: `search` (Projektname), `select` (Client)
- Client-Daten laden via useWidgetFilterOptions
- Filtern: `rows.filter(r => matchesSearch && matchesClient)`
- Filter-State via `config.search`, `config.clientId`
- [ ] **Task 4:** BudgetForecastWidget + Filter → `BudgetForecastWidget.tsx`
- Gleiche Filter wie ProjectHealth: search + clientId
- Gleiche Logik, gleiche WidgetFilterBar Config
- [ ] **Task 5:** ChargeabilityWidget + Filter → `ChargeabilityWidget.tsx`
- Filter: `select` (Country), `select` (Role/Chapter)
- Country/Role-Daten via useWidgetFilterOptions
- Filtern ueber die Resource-Daten in der Chargeability-Antwort
- `toggle` (Include Proposed) — bereits vorhanden, in WidgetFilterBar integrieren
- [ ] **Task 6:** SkillGapWidget + Filter → `SkillGapWidget.tsx`
- Filter: `search` (Skill-Name)
- Einfache client-seitige Filterung der Skill-Liste
- [ ] **Task 7:** TopValueWidget + Filter → `TopValueWidget.tsx`
- Filter: `select` (Chapter) — bereits sortierbar, Chapter-Filter hinzufuegen
- [ ] **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
---
## Abhaengigkeiten
```
Task 1 (Hook) + Task 2 (WidgetFilterBar) → koennen parallel
Task 1+2 → Tasks 3-7 (alle parallel, verschiedene Dateien)
```
- Tasks 3-7 sind **vollstaendig parallel** (verschiedene Widget-Dateien)
- Tasks 1+2 muessen zuerst (shared Infrastructure)
- Task 1 muss vor Task 2 (API benoetigt fuer UI)
- Beide Tasks koennen in einer Sequenz implementiert werden (gleicher Agent)
---
## Akzeptanzkriterien
- [ ] `pnpm --filter @planarchy/web exec tsc --noEmit` — keine neuen Errors
- [ ] **ProjectHealth** filterbar nach Projektname + Client
- [ ] **BudgetForecast** filterbar nach Projektname + Client
- [ ] **Chargeability** filterbar nach Country + Role
- [ ] **SkillGap** filterbar nach Skill-Name
- [ ] **TopValue** filterbar nach Chapter
- [ ] Filter-State wird in Widget-Config persistiert (bleibt nach Reload)
- [ ] Reset-Button setzt alle Filter zurueck
- [ ] Dark-Theme funktioniert fuer alle Filter
- [ ] Filter-Optionen werden gecacht (nicht pro Widget neu geladen)
- [ ] 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
---
## Risiken & offene Fragen
### Risiken
- **Widget-Groesse:** Filter-Bar braucht Platz — bei kleinen Widgets koennte es eng werden
→ Mitigation: Kompakte Inputs (xs), collapsible Filter-Bar mit Funnel-Icon
- **Performance:** Zusaetzliche tRPC-Queries fuer Client/Country/Role Listen
→ Mitigation: Ein shared Hook mit 5-Minuten staleTime, von allen Widgets geteilt
### Offene Fragen
1. **Server-side vs Client-side Filtering?**
→ Empfehlung: Client-seitig, da Widget-Daten bereits komplett geladen sind (max 30-50 Rows)
2. **Soll der Filter-Bar im Widget-Header oder darunter angezeigt werden?**
→ Empfehlung: Direkt unter dem Titel, ueber der Tabelle — kompakt mit kleinen Inputs
3. **Sollen Filter-Optionen leer sein wenn keine Daten vorhanden?**
→ Ja, leere Dropdowns zeigen "(All)" als Default
- **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