docs(06): create Phase 6 UX Overhaul plans — sidebar + dashboard (2 plans, 2 waves)

This commit is contained in:
2026-05-31 20:06:53 +02:00
parent 280e177a67
commit 26a688cec8
3 changed files with 602 additions and 3 deletions
+20 -3
View File
@@ -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 | - |
@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@/Users/simonecavalli/IAMCAVALLI/.claude/get-shit-done/workflows/execute-plan.md
@/Users/simonecavalli/IAMCAVALLI/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@/Users/simonecavalli/IAMCAVALLI/.planning/ROADMAP.md
@/Users/simonecavalli/IAMCAVALLI/.planning/STATE.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Create AdminSidebar component</name>
<files>src/components/admin/AdminSidebar.tsx</files>
<read_first>
- src/components/admin/NavBar.tsx — copy links and logout logic
- src/app/admin/layout.tsx — understand current layout
</read_first>
<action>
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 (
<aside className="w-56 shrink-0 min-h-screen bg-[#1A463C] flex flex-col">
{/* Logo */}
<div className="px-5 py-5 border-b border-white/10">
<span className="font-bold text-white tracking-tight text-sm">iamcavalli</span>
</div>
{/* Nav links */}
<nav className="flex-1 px-3 py-4 flex flex-col gap-0.5">
{NAV_ITEMS.map(({ href, label, icon: Icon, exact }) => {
const active = isActive(href, exact);
return (
<Link
key={href}
href={href}
className={`flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors ${
active
? "bg-white/15 text-white font-medium"
: "text-white/65 hover:text-white hover:bg-white/10"
}`}
>
<Icon size={16} strokeWidth={1.8} />
{label}
</Link>
);
})}
</nav>
{/* Logout */}
<div className="px-3 py-4 border-t border-white/10">
<button
onClick={() => signOut({ callbackUrl: "/admin/login" })}
className="flex items-center gap-3 px-3 py-2 w-full rounded-md text-sm text-white/65 hover:text-white hover:bg-white/10 transition-colors"
>
<LogOut size={16} strokeWidth={1.8} />
Esci
</button>
</div>
</aside>
);
}
```
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.
</action>
</task>
<task type="auto">
<name>Task 2: Update AdminLayout to AppShell (sidebar + main)</name>
<files>src/app/admin/layout.tsx</files>
<action>
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 (
<div className="flex min-h-screen bg-gray-50">
{session && <AdminSidebar />}
<main className="flex-1 px-8 py-8 overflow-y-auto">
{children}
</main>
</div>
);
}
```
Remove the import of NavBar. The sidebar handles all navigation.
</action>
</task>
<task type="auto">
<name>Task 3: Move client list to /admin/clients/page.tsx</name>
<files>src/app/admin/clients/page.tsx, src/app/admin/page.tsx</files>
<read_first>
- src/app/admin/page.tsx — full content to copy
- src/app/admin/clients/ — check if page.tsx already exists
</read_first>
<action>
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.
</action>
</task>
<task type="auto">
<name>Task 4: Delete NavBar.tsx and verify TypeScript builds</name>
<files>src/components/admin/NavBar.tsx</files>
<action>
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.
</action>
</task>
</tasks>
<verification>
- [ ] `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
</verification>
@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@/Users/simonecavalli/IAMCAVALLI/.claude/get-shit-done/workflows/execute-plan.md
@/Users/simonecavalli/IAMCAVALLI/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@/Users/simonecavalli/IAMCAVALLI/.planning/ROADMAP.md
@/Users/simonecavalli/IAMCAVALLI/.planning/STATE.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Create dashboard-queries.ts with getDashboardStats()</name>
<files>src/lib/dashboard-queries.ts</files>
<read_first>
- src/lib/admin-queries.ts — understand DB query patterns (Drizzle, db import)
- src/db/schema.ts — tables: clients, projects, payments, phases, time_entries, deliverables
</read_first>
<action>
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<DashboardStats> {
// ── 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 };
}
```
</action>
</task>
<task type="auto">
<name>Task 2: Build dashboard page at /admin</name>
<files>src/app/admin/page.tsx</files>
<action>
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 (
<div className="bg-white rounded-xl border border-gray-200 p-5 flex items-start gap-4">
<div className={`p-2 rounded-lg ${color}`}>
<Icon size={20} strokeWidth={1.8} className="text-white" />
</div>
<div>
<p className="text-xs text-gray-500 font-medium uppercase tracking-wide">{label}</p>
<p className="text-2xl font-bold text-gray-900 mt-0.5">{value}</p>
{sub && <p className="text-xs text-gray-400 mt-0.5">{sub}</p>}
</div>
</div>
);
}
const ACTIVITY_ICONS: Record<string, string> = {
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 (
<div className="max-w-5xl">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
{/* KPI cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<KpiCard
label="Clienti attivi"
value={kpi.clientiAttivi}
icon={Users}
color="bg-[#1A463C]"
/>
<KpiCard
label="Revenue totale"
value={fmtEur(kpi.revenueTotale)}
sub="progetti non archiviati"
icon={Euro}
color="bg-emerald-600"
/>
<KpiCard
label="Progetti in corso"
value={kpi.progettiInCorso}
icon={FolderOpen}
color="bg-blue-600"
/>
<KpiCard
label="Pagamenti in sospeso"
value={fmtEur(kpi.pagamentiInSospeso)}
sub="da_saldare + inviata"
icon={Clock}
color="bg-amber-500"
/>
</div>
{/* Activity feed */}
<div className="bg-white rounded-xl border border-gray-200">
<div className="px-5 py-4 border-b border-gray-100">
<h2 className="text-sm font-semibold text-gray-700">Attività recente</h2>
</div>
{activity.length === 0 ? (
<p className="px-5 py-8 text-sm text-gray-400 text-center">Nessuna attività recente</p>
) : (
<ul className="divide-y divide-gray-50">
{activity.map((item, i) => (
<li key={i} className="px-5 py-3 flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<span className="text-base">{ACTIVITY_ICONS[item.type]}</span>
<div>
<p className="text-sm font-medium text-gray-800">{item.label}</p>
<p className="text-xs text-gray-500">{item.detail}</p>
</div>
</div>
<span className="text-xs text-gray-400 shrink-0">{fmt(item.timestamp)}</span>
</li>
))}
</ul>
)}
</div>
</div>
);
}
```
</action>
</task>
<task type="auto">
<name>Task 3: Verify build and TypeScript</name>
<files>src/lib/dashboard-queries.ts, src/app/admin/page.tsx</files>
<action>
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.
</action>
</task>
</tasks>
<verification>
- [ ] `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)
</verification>