Shared infrastructure: - WidgetFilterBar: declarative filter component (search, select, toggle) - useWidgetFilterOptions: cached hook for clients, countries, roles, chapters Widget integration (5 widgets): - ProjectHealth: search (name) + select (client) - BudgetForecast: search (name) + select (client) - Chargeability: select (chapter) + toggle (include proposed) - SkillGap: search (skill name) - TopValue: select (chapter) Backend: added clientId/clientName to ProjectHealth and BudgetForecast query results for client-based filtering. Filter state persisted via widget config (survives page reload). All filters use compact 11px inputs with full dark theme support. Co-Authored-By: claude-flow <ruv@ruv.net>
7.0 KiB
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 <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
// 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
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:
useWidgetFilterOptionsHook 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
- Cached Queries fuer Clients, Countries, Roles (alle mit
-
Task 2:
WidgetFilterBarKomponente erstellen →src/components/dashboard/WidgetFilterBar.tsxinterface 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
- Filter:
-
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
- Filter:
-
Task 6: SkillGapWidget + Filter →
SkillGapWidget.tsx- Filter:
search(Skill-Name) - Einfache client-seitige Filterung der Skill-Liste
- Filter:
-
Task 7: TopValueWidget + Filter →
TopValueWidget.tsx- Filter:
select(Chapter) — bereits sortierbar, Chapter-Filter hinzufuegen
- Filter:
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
- Server-side vs Client-side Filtering? → Empfehlung: Client-seitig, da Widget-Daten bereits komplett geladen sind (max 30-50 Rows)
- Soll der Filter-Bar im Widget-Header oder darunter angezeigt werden? → Empfehlung: Direkt unter dem Titel, ueber der Tabelle — kompakt mit kleinen Inputs
- Sollen Filter-Optionen leer sein wenn keine Daten vorhanden? → Ja, leere Dropdowns zeigen "(All)" als Default