208f866d68
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>
177 lines
7.0 KiB
Markdown
177 lines
7.0 KiB
Markdown
# 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
|
|
|
|
```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
|
|
```
|
|
|
|
---
|
|
|
|
## 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<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
|