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()
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")
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.