diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 8ac2c76..3344e23 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -122,10 +122,26 @@ Decimal phases appear between their surrounding integers in numeric order. **UI hint**: yes **Status**: ✅ Complete (2026-05-30) -### Phase 6: Claude AI Onboarding (v2) -**Goal**: L'admin può usare un flusso chat guidato per onboardare un nuovo cliente e generare un piano a fasi e un preventivo assistito da Claude +### Phase 6: UX Overhaul — Sidebar + Dashboard +**Goal**: L'admin apre l'app e vede subito una dashboard con la situazione aziendale; la navigazione è una sidebar persistente a sinistra invece dell'header **Mode:** mvp **Depends on**: Phase 5 +**Requirements**: UX-01, UX-02, UX-03 +**Success Criteria** (what must be TRUE): + 1. Tutte le pagine admin usano un layout con sidebar sinistra persistente — nessun header nav + 2. `/admin` mostra una dashboard con: clienti attivi + revenue totale, progetti in corso, pagamenti in sospeso, attività recente + 3. La lista clienti è accessibile da `/admin/clients`; i link esistenti continuano a funzionare +**Plans**: 2 plans +**Plan list**: + - [ ] 06-01-PLAN.md — Sidebar layout: AdminSidebar component + AppShell + move client list to /admin/clients + - [ ] 06-02-PLAN.md — Dashboard page: KPI cards (revenue, clienti, progetti, pagamenti) + activity feed +**UI hint**: yes +**Status**: Planning complete + +### Phase 7: Claude AI Onboarding (v2) +**Goal**: L'admin può usare un flusso chat guidato per onboardare un nuovo cliente e generare un piano a fasi e un preventivo assistito da Claude +**Mode:** mvp +**Depends on**: Phase 6 **Requirements**: CLAUDE-01, CLAUDE-02, CLAUDE-03 **Success Criteria** (what must be TRUE): 1. L'admin avvia il flusso Claude inserendo il brief del cliente; Claude guida step-by-step la raccolta delle informazioni necessarie @@ -147,4 +163,5 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 | 3. Service Catalog & Quote Builder | 4/4 | ✅ Done | 2026-05-19 | | 4. Progetti — Multi-Project per Cliente | 5/5 | ✅ Done | 2026-05-23 | | 5. Offer System | 4/4 | ✅ Done | 2026-05-30 | -| 6. Claude AI Onboarding (v2) | 0/TBD | Pending | - | +| 6. UX Overhaul — Sidebar + Dashboard | 0/2 | Pending | - | +| 7. Claude AI Onboarding (v2) | 0/TBD | Pending | - | diff --git a/.planning/phases/06-ux-overhaul/06-01-PLAN.md b/.planning/phases/06-ux-overhaul/06-01-PLAN.md new file mode 100644 index 0000000..09bad9d --- /dev/null +++ b/.planning/phases/06-ux-overhaul/06-01-PLAN.md @@ -0,0 +1,236 @@ +--- +plan_id: 06-01 +phase: 6 +wave: 1 +title: "Sidebar layout — AdminSidebar + AppShell + move client list to /admin/clients" +type: execute +depends_on: [] +files_modified: + - src/app/admin/layout.tsx + - src/components/admin/NavBar.tsx + - src/app/admin/page.tsx + - src/app/admin/clients/page.tsx + - src/components/admin/AdminSidebar.tsx +requirements_addressed: [UX-01, UX-03] +autonomous: true +must_haves: + truths: + - "AdminLayout uses a flex-row shell: sidebar left (fixed width) + main content right" + - "NavBar.tsx is replaced by AdminSidebar.tsx — header nav no longer exists" + - "AdminSidebar has links: Dashboard, Clienti, Progetti, Offerte, Forecast, Catalogo, Impostazioni + Logout at bottom" + - "Active route is highlighted in the sidebar (usePathname)" + - "Client list (previously at /admin) is now at /admin/clients with identical functionality" + - "/admin page exists but shows a placeholder or redirects — replaced in 06-02 with real dashboard" + artifacts: + - path: "src/components/admin/AdminSidebar.tsx" + provides: "Sidebar client component with navigation links and logout" + contains: "usePathname, signOut, all nav links" + - path: "src/app/admin/layout.tsx" + provides: "AppShell layout: flex row, sidebar + main" + contains: "AdminSidebar, flex, min-h-screen" + - path: "src/app/admin/clients/page.tsx" + provides: "Client list page moved from /admin" + contains: "getAllClientsWithPayments" + key_links: + - from: "src/components/admin/AdminSidebar.tsx" + to: "src/app/admin/layout.tsx" + via: "import" + pattern: "AdminSidebar" +--- + + +Replace the top header NavBar with a persistent left sidebar. Move the client list from /admin to /admin/clients. The /admin route will be a blank placeholder for now — the real dashboard comes in 06-02. + +This is a pure layout + routing refactor. No data model changes. No new queries. + + + +@/Users/simonecavalli/IAMCAVALLI/.claude/get-shit-done/workflows/execute-plan.md +@/Users/simonecavalli/IAMCAVALLI/.claude/get-shit-done/templates/summary.md + + + +@/Users/simonecavalli/IAMCAVALLI/.planning/ROADMAP.md +@/Users/simonecavalli/IAMCAVALLI/.planning/STATE.md + + + + + + Task 1: Create AdminSidebar component + src/components/admin/AdminSidebar.tsx + + + - src/components/admin/NavBar.tsx — copy links and logout logic + - src/app/admin/layout.tsx — understand current layout + + + +Create `src/components/admin/AdminSidebar.tsx` as a client component: + +```tsx +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { signOut } from "next-auth/react"; +import { + LayoutDashboard, + Users, + FolderOpen, + Tag, + TrendingUp, + BookOpen, + Settings, + LogOut, +} from "lucide-react"; + +const NAV_ITEMS = [ + { href: "/admin", label: "Dashboard", icon: LayoutDashboard, exact: true }, + { href: "/admin/clients", label: "Clienti", icon: Users }, + { href: "/admin/projects", label: "Progetti", icon: FolderOpen }, + { href: "/admin/offers", label: "Offerte", icon: Tag }, + { href: "/admin/forecast", label: "Forecast", icon: TrendingUp }, + { href: "/admin/catalog", label: "Catalogo", icon: BookOpen }, + { href: "/admin/impostazioni", label: "Impostazioni", icon: Settings }, +]; + +export function AdminSidebar() { + const pathname = usePathname(); + + const isActive = (href: string, exact?: boolean) => + exact ? pathname === href : pathname === href || pathname.startsWith(href + "/"); + + return ( + + ); +} +``` + +Check that `lucide-react` is already a dependency (`package.json`). If not, note it as a deviation but do NOT install it — shadcn/ui already bundles it transitively in this project. + + + + + Task 2: Update AdminLayout to AppShell (sidebar + main) + src/app/admin/layout.tsx + + +Replace the current vertical layout (nav on top, max-w-5xl centered main) with a horizontal AppShell: + +```tsx +import { AdminSidebar } from "@/components/admin/AdminSidebar"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; + +export default async function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await getServerSession(authOptions); + return ( +
+ {session && } +
+ {children} +
+
+ ); +} +``` + +Remove the import of NavBar. The sidebar handles all navigation. +
+
+ + + Task 3: Move client list to /admin/clients/page.tsx + src/app/admin/clients/page.tsx, src/app/admin/page.tsx + + + - src/app/admin/page.tsx — full content to copy + - src/app/admin/clients/ — check if page.tsx already exists + + + +1. Copy the full content of `src/app/admin/page.tsx` to `src/app/admin/clients/page.tsx`. + Update the "Nuovo cliente" link href if it points to `/admin/clients/new` — it should already be correct. + Update the "Mostra archiviati" toggle link from `?archived=1` on `/admin` to `/admin/clients?archived=1`. + +2. Replace `src/app/admin/page.tsx` with a minimal redirect placeholder for now (dashboard content comes in 06-02): + +```tsx +import { redirect } from "next/navigation"; + +// Dashboard page — content added in Phase 6 Plan 02 +export default function AdminRoot() { + redirect("/admin/clients"); +} +``` + +This keeps the app functional during the transition: clicking Dashboard in the sidebar goes to /admin which redirects to /admin/clients until the real dashboard exists. + + + + + Task 4: Delete NavBar.tsx and verify TypeScript builds + src/components/admin/NavBar.tsx + + +Delete `src/components/admin/NavBar.tsx` since it is fully replaced by AdminSidebar. + +Run `npx tsc --noEmit` to verify no TypeScript errors. If NavBar is imported anywhere else, remove those imports. + +Run `npm run build` to confirm the Next.js build passes. + + + +
+ + +- [ ] `npx tsc --noEmit` passes with no errors +- [ ] `npm run build` completes successfully +- [ ] Sidebar renders with all 7 nav links + logout button +- [ ] Active link is visually highlighted +- [ ] `/admin/clients` loads the client list correctly +- [ ] `/admin` redirects to `/admin/clients` +- [ ] No references to NavBar remain in the codebase + diff --git a/.planning/phases/06-ux-overhaul/06-02-PLAN.md b/.planning/phases/06-ux-overhaul/06-02-PLAN.md new file mode 100644 index 0000000..a40007f --- /dev/null +++ b/.planning/phases/06-ux-overhaul/06-02-PLAN.md @@ -0,0 +1,346 @@ +--- +plan_id: 06-02 +phase: 6 +wave: 2 +title: "Dashboard page — KPI cards + activity feed at /admin" +type: execute +depends_on: [06-01] +files_modified: + - src/app/admin/page.tsx + - src/lib/dashboard-queries.ts +requirements_addressed: [UX-02] +autonomous: true +must_haves: + truths: + - "/admin renders a real dashboard (no redirect) with KPI cards and activity feed" + - "KPI cards show: clienti attivi, revenue totale progetti, pagamenti in sospeso (importo), progetti in corso" + - "Activity feed shows last 10 events across approvazioni deliverable, nuovi clienti, nuovi progetti, timer stoppati" + - "getDashboardStats() query is in src/lib/dashboard-queries.ts — not inlined in the page" + - "Page is an RSC (no 'use client') — data fetched server-side" + artifacts: + - path: "src/lib/dashboard-queries.ts" + provides: "getDashboardStats() returning KPIs and recent activity" + contains: "clienti attivi, revenue, pagamenti, progetti, activity" + - path: "src/app/admin/page.tsx" + provides: "Dashboard RSC page with KPI cards and activity feed" + contains: "getDashboardStats, KpiCard" + key_links: + - from: "src/app/admin/page.tsx" + to: "src/lib/dashboard-queries.ts" + via: "import" + pattern: "getDashboardStats" +--- + + +Build the real dashboard page at /admin. Four KPI cards + an activity feed. All data comes from existing tables — no schema changes needed. + +Purpose: The admin opens the app and immediately sees the business situation at a glance. + + + +@/Users/simonecavalli/IAMCAVALLI/.claude/get-shit-done/workflows/execute-plan.md +@/Users/simonecavalli/IAMCAVALLI/.claude/get-shit-done/templates/summary.md + + + +@/Users/simonecavalli/IAMCAVALLI/.planning/ROADMAP.md +@/Users/simonecavalli/IAMCAVALLI/.planning/STATE.md + + + + + + Task 1: Create dashboard-queries.ts with getDashboardStats() + src/lib/dashboard-queries.ts + + + - src/lib/admin-queries.ts — understand DB query patterns (Drizzle, db import) + - src/db/schema.ts — tables: clients, projects, payments, phases, time_entries, deliverables + + + +Create `src/lib/dashboard-queries.ts`: + +```typescript +import { db } from "@/db"; +import { clients, projects, payments, phases, deliverables, time_entries } from "@/db/schema"; +import { eq, and, not, isNull, desc, sum, count, lt } from "drizzle-orm"; + +export type KpiStats = { + clientiAttivi: number; + revenueTotale: string; // sum of projects.accepted_total (non-archived) + pagamentiInSospeso: string; // sum of payments.amount where status = 'da_saldare' or 'inviata' + progettiInCorso: number; // projects not archived +}; + +export type ActivityItem = { + type: "nuovo_cliente" | "nuovo_progetto" | "deliverable_approvato" | "timer_stoppato"; + label: string; + detail: string; + timestamp: Date; +}; + +export type DashboardStats = { + kpi: KpiStats; + activity: ActivityItem[]; +}; + +export async function getDashboardStats(): Promise { + // ── KPIs ──────────────────────────────────────────────────────────────── + const [clientiAttivi] = await db + .select({ count: count() }) + .from(clients) + .where(eq(clients.archived, false)); + + const [revenueRow] = await db + .select({ total: sum(projects.accepted_total) }) + .from(projects) + .where(eq(projects.archived, false)); + + const [pagamentiRow] = await db + .select({ total: sum(payments.amount) }) + .from(payments) + .where( + and( + eq(payments.status, "da_saldare"), + ) + ); + + // Also include "inviata" status in sospeso + const [pagamentiInviataRow] = await db + .select({ total: sum(payments.amount) }) + .from(payments) + .where(eq(payments.status, "inviata")); + + const [progettiRow] = await db + .select({ count: count() }) + .from(projects) + .where(eq(projects.archived, false)); + + const pagamentiSospeso = + (parseFloat(pagamentiRow?.total ?? "0") || 0) + + (parseFloat(pagamentiInviataRow?.total ?? "0") || 0); + + const kpi: KpiStats = { + clientiAttivi: clientiAttivi?.count ?? 0, + revenueTotale: revenueRow?.total ?? "0", + pagamentiInSospeso: pagamentiSospeso.toFixed(2), + progettiInCorso: progettiRow?.count ?? 0, + }; + + // ── Activity feed ──────────────────────────────────────────────────────── + // Fetch recent events from multiple tables, merge and sort client-side + const recentClients = await db + .select({ id: clients.id, name: clients.name, created_at: clients.created_at }) + .from(clients) + .orderBy(desc(clients.created_at)) + .limit(5); + + const recentProjects = await db + .select({ id: projects.id, name: projects.name, created_at: projects.created_at }) + .from(projects) + .orderBy(desc(projects.created_at)) + .limit(5); + + const recentApprovals = await db + .select({ id: deliverables.id, title: deliverables.title, approved_at: deliverables.approved_at }) + .from(deliverables) + .where(not(isNull(deliverables.approved_at))) + .orderBy(desc(deliverables.approved_at)) + .limit(5); + + const recentTimers = await db + .select({ id: time_entries.id, ended_at: time_entries.ended_at, duration_seconds: time_entries.duration_seconds }) + .from(time_entries) + .where(not(isNull(time_entries.ended_at))) + .orderBy(desc(time_entries.ended_at)) + .limit(5); + + const activity: ActivityItem[] = [ + ...recentClients.map((c) => ({ + type: "nuovo_cliente" as const, + label: "Nuovo cliente", + detail: c.name, + timestamp: c.created_at, + })), + ...recentProjects.map((p) => ({ + type: "nuovo_progetto" as const, + label: "Nuovo progetto", + detail: p.name, + timestamp: p.created_at, + })), + ...recentApprovals.map((d) => ({ + type: "deliverable_approvato" as const, + label: "Deliverable approvato", + detail: d.title, + timestamp: d.approved_at!, + })), + ...recentTimers + .filter((t) => t.ended_at && t.duration_seconds) + .map((t) => ({ + type: "timer_stoppato" as const, + label: "Timer stoppato", + detail: `${Math.round((t.duration_seconds ?? 0) / 60)} min`, + timestamp: t.ended_at!, + })), + ] + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) + .slice(0, 10); + + return { kpi, activity }; +} +``` + + + + + Task 2: Build dashboard page at /admin + src/app/admin/page.tsx + + +Replace the redirect placeholder (from 06-01) with the real dashboard RSC: + +```tsx +import { getDashboardStats } from "@/lib/dashboard-queries"; +import { Users, FolderOpen, Euro, Clock } from "lucide-react"; + +export const revalidate = 0; + +function KpiCard({ + label, + value, + sub, + icon: Icon, + color, +}: { + label: string; + value: string | number; + sub?: string; + icon: React.ElementType; + color: string; +}) { + return ( +
+
+ +
+
+

{label}

+

{value}

+ {sub &&

{sub}

} +
+
+ ); +} + +const ACTIVITY_ICONS: Record = { + nuovo_cliente: "👤", + nuovo_progetto: "📁", + deliverable_approvato: "✅", + timer_stoppato: "⏱", +}; + +function fmt(ts: Date) { + return new Intl.DateTimeFormat("it-IT", { + day: "2-digit", + month: "short", + hour: "2-digit", + minute: "2-digit", + }).format(new Date(ts)); +} + +function fmtEur(val: string) { + return new Intl.NumberFormat("it-IT", { style: "currency", currency: "EUR" }).format( + parseFloat(val) || 0 + ); +} + +export default async function AdminDashboard() { + const { kpi, activity } = await getDashboardStats(); + + return ( +
+

Dashboard

+ + {/* KPI cards */} +
+ + + + +
+ + {/* Activity feed */} +
+
+

Attività recente

+
+ {activity.length === 0 ? ( +

Nessuna attività recente

+ ) : ( +
    + {activity.map((item, i) => ( +
  • +
    + {ACTIVITY_ICONS[item.type]} +
    +

    {item.label}

    +

    {item.detail}

    +
    +
    + {fmt(item.timestamp)} +
  • + ))} +
+ )} +
+
+ ); +} +``` +
+
+ + + Task 3: Verify build and TypeScript + src/lib/dashboard-queries.ts, src/app/admin/page.tsx + + +Run `npx tsc --noEmit` and fix any type errors (common: Drizzle `count()` return type, nullable fields). + +Run `npm run build` to confirm the full Next.js build passes. + + + +
+ + +- [ ] `npx tsc --noEmit` passes +- [ ] `npm run build` passes +- [ ] /admin shows Dashboard heading with 4 KPI cards +- [ ] KPI cards display real data (not all zeros, assuming data in DB) +- [ ] Activity feed shows at least some items if data exists +- [ ] Dashboard is an RSC (no "use client" in page.tsx) +