Files
CapaKraken/plan.md
T
Hartmut 208f866d68 feat: shared widget filter system for all dashboard widgets
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>
2026-03-23 09:21:46 +01:00

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: 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

    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

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