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:
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { startTimer, stopTimer } from "@/app/admin/timer-actions";
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = seconds % 60;
|
||||
if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
||||
return `${m}:${String(s).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function TimerCell({
|
||||
clientId,
|
||||
activeEntryId,
|
||||
activeStartedAt,
|
||||
totalTrackedSeconds,
|
||||
}: {
|
||||
clientId: string;
|
||||
activeEntryId: string | null;
|
||||
activeStartedAt: Date | null;
|
||||
totalTrackedSeconds: number;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [, startTransition] = useTransition();
|
||||
const isRunning = activeEntryId !== null;
|
||||
|
||||
// Live elapsed counter
|
||||
const [elapsed, setElapsed] = useState(() => {
|
||||
if (!activeStartedAt) return 0;
|
||||
return Math.round((Date.now() - new Date(activeStartedAt).getTime()) / 1000);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRunning) { setElapsed(0); return; }
|
||||
const start = activeStartedAt ? new Date(activeStartedAt).getTime() : Date.now();
|
||||
const tick = () => setElapsed(Math.round((Date.now() - start) / 1000));
|
||||
tick();
|
||||
const id = setInterval(tick, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [isRunning, activeStartedAt]);
|
||||
|
||||
function handleToggle() {
|
||||
startTransition(async () => {
|
||||
if (isRunning && activeEntryId) {
|
||||
await stopTimer(activeEntryId);
|
||||
} else {
|
||||
await startTimer(clientId);
|
||||
}
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
const displayTotal = formatDuration(totalTrackedSeconds + (isRunning ? elapsed : 0));
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
title={isRunning ? "Ferma timer" : "Avvia timer"}
|
||||
className={`w-7 h-7 rounded-full flex items-center justify-center transition-colors shrink-0 ${
|
||||
isRunning
|
||||
? "bg-red-100 text-red-600 hover:bg-red-200"
|
||||
: "bg-[#1A463C]/10 text-[#1A463C] hover:bg-[#1A463C]/20"
|
||||
}`}
|
||||
>
|
||||
{isRunning ? (
|
||||
// Stop square
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 16 16">
|
||||
<rect x="3" y="3" width="10" height="10" rx="1.5" />
|
||||
</svg>
|
||||
) : (
|
||||
// Play triangle
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M4 3.5v9l9-4.5-9-4.5z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<span
|
||||
className={`text-xs tabular-nums font-mono ${
|
||||
isRunning ? "text-red-600 font-semibold" : "text-[#71717a]"
|
||||
}`}
|
||||
>
|
||||
{isRunning ? formatDuration(elapsed) : displayTotal}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user