# Dashboard Widget Filter System — Plan ## Anforderungsanalyse **Was:** Einheitliches Filter-System fuer alle Dashboard-Widgets. Filter-Logik soll geteilt werden statt pro Widget dupliziert. **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 | **Design-Prinzip:** Ein shared `` 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 }, ]; ``` ### 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 ``` --- ## Betroffene Pakete & Dateien | 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 | --- ## Task-Liste ### Phase 1: Shared Infrastructure - [ ] **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; onChange: (update: Record) => 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 --- ## 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) --- ## 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) --- ## 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