e6a9774cfe
- Admin client list: remove Timer column, rename Acconto→Importo incassato and Saldo→Importo da saldare (calculated from payment amounts, not badges) - Project workspace timer: pass projectId prop to TimerCell so it calls startTimer(projectId) directly instead of startTimerForClient(projectId) which was failing with "nessun progetto trovato" - Public client page: pass client.token (real DB token) not the URL path segment (which may be a slug) to ClientDashboard → fixes approve/chat APIs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useTransition } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { startTimer, startTimerForClient, 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,
|
|
projectId,
|
|
activeEntryId,
|
|
activeStartedAt,
|
|
totalTrackedSeconds,
|
|
}: {
|
|
clientId: string;
|
|
projectId?: 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 if (projectId) {
|
|
await startTimer(projectId);
|
|
} else {
|
|
await startTimerForClient(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>
|
|
);
|
|
} |