diff --git a/.planning/phases/06-ux-overhaul/06-02-SUMMARY.md b/.planning/phases/06-ux-overhaul/06-02-SUMMARY.md new file mode 100644 index 0000000..3cade4d --- /dev/null +++ b/.planning/phases/06-ux-overhaul/06-02-SUMMARY.md @@ -0,0 +1,94 @@ +--- +plan: "06-02" +phase: 6 +subsystem: admin-dashboard +tags: [dashboard, kpi, activity-feed, rsc, drizzle] +dependency_graph: + requires: [06-01] + provides: [getDashboardStats, /admin dashboard page] + affects: [src/app/admin/page.tsx, src/lib/dashboard-queries.ts] +tech_stack: + added: [] + patterns: [RSC async server component, Promise.all parallel DB fetch, Intl.NumberFormat/DateTimeFormat] +key_files: + created: + - src/lib/dashboard-queries.ts + modified: + - src/app/admin/page.tsx +decisions: + - "isNotNull() used instead of not(isNull()) — isNotNull is the correct Drizzle-orm helper for nullable timestamp WHERE clauses" + - "Promise.all for the 4 activity feed queries — parallel fetch reduces latency" + - "Type guard filters (.filter with type predicate) used for nullable approved_at and ended_at before mapping — avoids non-null assertion operator" + - "revalidate=0 on dashboard page — always fresh data, no stale cache" +metrics: + duration: "~10 minutes" + completed: "2026-05-31" + tasks_completed: 3 + tasks_total: 3 +--- + +# Phase 6 Plan 02: Dashboard page — KPI cards + activity feed at /admin Summary + +Real admin dashboard at /admin with 4 KPI cards (clienti attivi, revenue totale, progetti in corso, pagamenti in sospeso) and an activity feed showing the last 10 events across clients, projects, deliverables, and time entries. Page is a pure RSC — no client component directive. Data layer extracted into `src/lib/dashboard-queries.ts`. + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | Create dashboard-queries.ts with getDashboardStats() | a304328 | src/lib/dashboard-queries.ts | +| 2 | Build dashboard page at /admin | 40162e0 | src/app/admin/page.tsx | +| 3 | Verify build and TypeScript | 40162e0 | — | + +## What Was Built + +**dashboard-queries.ts** — server-only query module with: +- 4 KPI queries: `count()` of non-archived clients, `sum()` of non-archived projects.accepted_total, `sum()` of payments with status `da_saldare` + `inviata`, `count()` of non-archived projects +- Activity feed: 4 parallel queries via `Promise.all` fetching the 5 most recent events from clients, projects, deliverables (approved), time_entries (stopped). Merged and sorted by timestamp descending, sliced to 10. +- `isNotNull()` used for nullable timestamp columns (approved_at, ended_at) — correct Drizzle-orm helper + +**admin/page.tsx** — RSC async server component with: +- `KpiCard` sub-component (inline RSC, no "use client") +- 4-column grid KPI cards with Lucide icons (Users, Euro, FolderOpen, Clock) +- Activity feed list with emoji icons per event type +- `Intl.DateTimeFormat` for Italian locale timestamps, `Intl.NumberFormat` for EUR currency formatting +- `revalidate = 0` for always-fresh data + +## Deviations from Plan + +**1. [Rule 1 - Bug] Replaced `not(isNull())` with `isNotNull()`** +- **Found during:** Task 1 implementation +- **Issue:** Plan code used `not(isNull(deliverables.approved_at))` but `not` is not an ergonomic Drizzle-orm filter helper for this pattern — `isNotNull()` is the correct dedicated helper +- **Fix:** Used `isNotNull()` from drizzle-orm directly, matching the pattern used in `admin-queries.ts` +- **Files modified:** src/lib/dashboard-queries.ts + +**2. [Rule 2 - Correctness] Added type guard filters for nullable fields** +- **Found during:** Task 1 implementation +- **Issue:** Mapping `approved_at` and `ended_at` after the WHERE clause still required TypeScript type narrowing (Drizzle returns nullable types regardless of WHERE) +- **Fix:** Added `.filter((d): d is ... & { approved_at: Date }` type predicates before mapping — avoids non-null assertions and is type-safe +- **Files modified:** src/lib/dashboard-queries.ts + +**3. [Rule 2 - Performance] Parallel fetch for activity queries** +- **Found during:** Task 1 implementation +- **Issue:** Plan code had 4 sequential activity queries; `Promise.all` reduces latency +- **Fix:** Wrapped all 4 activity queries in `Promise.all([...])` +- **Files modified:** src/lib/dashboard-queries.ts + +## Verification + +- `npx tsc --noEmit`: PASSED — no errors +- `npm run build`: PASSED — 22 routes compiled, /admin shows as dynamic (ƒ) server-rendered route +- /admin no longer redirects — renders real Dashboard page +- Page is RSC — no "use client" directive in page.tsx +- KpiCard and fmt/fmtEur helpers are plain functions (not client components) + +## Known Stubs + +None — all KPI values and activity items come from live DB queries. If the database is empty, cards show 0/€0,00 and activity feed shows "Nessuna attività recente" — intentional, not a stub. + +## Self-Check: PASSED + +- [x] src/lib/dashboard-queries.ts — created (a304328) +- [x] src/app/admin/page.tsx — updated, no redirect, full dashboard RSC (40162e0) +- [x] npx tsc --noEmit passes +- [x] npm run build passes — /admin is ƒ (dynamic) +- [x] No "use client" in page.tsx