feat: client edit/delete/archive + time tracker + analytics time section

Schema:
- clients.archived boolean (default false)
- time_entries table (client_id, started_at, ended_at, duration_seconds)

Client management:
- /admin/clients/[id]/edit — form pre-compilato con nome, brand, brief
- ClientActions: Modifica / Archivia / Elimina con doppia conferma
- setClientArchived: toggle archiviazione senza perdere dati
- deleteClient: elimina con cascade, redirect a /admin
- Admin list: toggle "Mostra archiviati" via ?archived=1, righe archiviate opache

Time tracker:
- startTimer: crea sessione, ferma automaticamente quella precedente
- stopTimer: chiude sessione, calcola duration_seconds
- TimerCell: ▶/⏹ per ogni cliente, contatore live in secondi, totale cumulativo
- Una sola sessione attiva alla volta su tutta la lista

Analytics:
- Sezione "Fatturato" (invariata) + sezione "Tempo tracciato" separata
- Ore totali per anno + barre orizzontali per cliente
- getTotalTrackedHours, getTimeByClient queries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simone Cavalli
2026-05-16 21:28:01 +02:00
parent d322162c0a
commit 0f48570cd7
12 changed files with 656 additions and 153 deletions
+23 -13
View File
@@ -6,6 +6,7 @@ import { PaymentsTab } from "@/components/admin/tabs/PaymentsTab";
import { DocumentsTab } from "@/components/admin/tabs/DocumentsTab";
import { CommentsTab } from "@/components/admin/tabs/CommentsTab";
import { PhasesViewToggle } from "@/components/admin/kanban/PhasesViewToggle";
import { ClientActions } from "@/components/admin/ClientActions";
import Link from "next/link";
export const revalidate = 0;
@@ -19,28 +20,37 @@ export default async function ClientDetailPage({
const detail = await getClientFullDetail(id);
if (!detail) notFound();
const { client, phases, payments, documents, notes, comments } = detail;
const { client, phases, payments, documents, comments } = detail;
return (
<div>
<div className="mb-4">
<Link href="/admin" className="text-sm text-gray-500 hover:text-gray-700">
<Link href="/admin" className="text-sm text-[#71717a] hover:text-[#1a1a1a]">
Clienti
</Link>
</div>
<div className="mb-6 flex items-start justify-between">
<div className="mb-6 flex items-start justify-between gap-4 flex-wrap">
<div>
<h1 className="text-2xl font-bold text-gray-900">{client.name}</h1>
<p className="text-sm text-gray-500">{client.brand_name}</p>
<h1 className="text-2xl font-bold text-[#1a1a1a]">{client.name}</h1>
<p className="text-sm text-[#71717a]">{client.brand_name}</p>
{client.archived && (
<span className="inline-block mt-1 text-xs font-medium bg-[#f4f4f5] text-[#71717a] px-2 py-0.5 rounded-full">
Archiviato
</span>
)}
</div>
<div className="flex flex-col items-end gap-2">
<a
href={`/c/${client.token}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-[#1A463C] hover:underline font-mono bg-[#1A463C]/5 px-2 py-1 rounded"
>
Link cliente
</a>
<ClientActions clientId={client.id} archived={client.archived ?? false} />
</div>
<a
href={`/c/${client.token}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-600 hover:underline font-mono bg-blue-50 px-2 py-1 rounded"
>
Link cliente
</a>
</div>
<Tabs defaultValue="phases" className="w-full">