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
+32 -28
View File
@@ -3,15 +3,29 @@ import { getAllClientsWithPayments } from "@/lib/admin-queries";
import { ClientRow } from "@/components/admin/ClientRow";
import { Button } from "@/components/ui/button";
export const revalidate = 0; // always fresh — admin needs real-time data
export const revalidate = 0;
export default async function AdminDashboard() {
const clients = await getAllClientsWithPayments();
export default async function AdminDashboard({
searchParams,
}: {
searchParams: Promise<{ archived?: string }>;
}) {
const { archived } = await searchParams;
const showArchived = archived === "1";
const clients = await getAllClientsWithPayments(showArchived);
return (
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-[#1a1a1a]">Clienti</h1>
<div className="flex items-center gap-4">
<h1 className="text-2xl font-bold text-[#1a1a1a]">Clienti</h1>
<a
href={showArchived ? "/admin" : "/admin?archived=1"}
className="text-xs text-[#71717a] hover:text-[#1A463C] underline underline-offset-2"
>
{showArchived ? "Nascondi archiviati" : "Mostra archiviati"}
</a>
</div>
<Button asChild>
<Link href="/admin/clients/new">+ Nuovo cliente</Link>
</Button>
@@ -19,36 +33,26 @@ export default async function AdminDashboard() {
{clients.length === 0 ? (
<div className="text-center py-20 text-[#71717a]">
<p>Nessun cliente ancora.</p>
<p className="mt-2">
<Link
href="/admin/clients/new"
className="text-[#1A463C] hover:underline font-medium"
>
Crea il primo cliente
</Link>
</p>
<p>{showArchived ? "Nessun cliente archiviato." : "Nessun cliente ancora."}</p>
{!showArchived && (
<p className="mt-2">
<Link href="/admin/clients/new" className="text-[#1A463C] hover:underline font-medium">
Crea il primo cliente
</Link>
</p>
)}
</div>
) : (
<div className="bg-white rounded-lg border border-[#e5e7eb] overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-[#f9f9f9] border-b border-[#e5e7eb]">
<tr>
<th className="text-left py-3 px-4 font-medium text-[#71717a]">
Cliente
</th>
<th className="text-left py-3 px-4 font-medium text-[#71717a]">
Totale
</th>
<th className="text-left py-3 px-4 font-medium text-[#71717a]">
Acconto
</th>
<th className="text-left py-3 px-4 font-medium text-[#71717a]">
Saldo
</th>
<th className="text-left py-3 px-4 font-medium text-[#71717a]">
Link
</th>
<th className="text-left py-3 px-4 font-medium text-[#71717a]">Cliente</th>
<th className="text-left py-3 px-4 font-medium text-[#71717a]">Totale</th>
<th className="text-left py-3 px-4 font-medium text-[#71717a]">Acconto</th>
<th className="text-left py-3 px-4 font-medium text-[#71717a]">Saldo</th>
<th className="text-left py-3 px-4 font-medium text-[#71717a]">Timer</th>
<th className="text-left py-3 px-4 font-medium text-[#71717a]">Link</th>
</tr>
</thead>
<tbody>