Files
simone 6e97a53b73 docs(06-02): complete dashboard KPI + activity feed plan
- getDashboardStats() in dashboard-queries.ts
- RSC dashboard at /admin with 4 KPI cards + activity feed
- npm run build passes
2026-05-31 20:12:54 +02:00

4.8 KiB

plan, phase, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
plan phase subsystem tags dependency_graph tech_stack key_files decisions metrics
06-02 6 admin-dashboard
dashboard
kpi
activity-feed
rsc
drizzle
requires provides affects
06-01
getDashboardStats
/admin dashboard page
src/app/admin/page.tsx
src/lib/dashboard-queries.ts
added patterns
RSC async server component
Promise.all parallel DB fetch
Intl.NumberFormat/DateTimeFormat
created modified
src/lib/dashboard-queries.ts
src/app/admin/page.tsx
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
duration completed tasks_completed tasks_total
~10 minutes 2026-05-31 3 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

  • src/lib/dashboard-queries.ts — created (a304328)
  • src/app/admin/page.tsx — updated, no redirect, full dashboard RSC (40162e0)
  • npx tsc --noEmit passes
  • npm run build passes — /admin is ƒ (dynamic)
  • No "use client" in page.tsx