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,55 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { db } from "@/db";
|
||||
import { time_entries } from "@/db/schema";
|
||||
import { eq, isNull } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
export async function startTimer(clientId: string): Promise<{ entryId: string }> {
|
||||
// Stop any currently running session (for any client) before starting a new one
|
||||
const running = await db
|
||||
.select({ id: time_entries.id })
|
||||
.from(time_entries)
|
||||
.where(isNull(time_entries.ended_at));
|
||||
|
||||
for (const r of running) {
|
||||
const now = new Date();
|
||||
const entry = await db
|
||||
.select({ started_at: time_entries.started_at })
|
||||
.from(time_entries)
|
||||
.where(eq(time_entries.id, r.id))
|
||||
.limit(1);
|
||||
if (entry[0]) {
|
||||
const secs = Math.round((now.getTime() - new Date(entry[0].started_at).getTime()) / 1000);
|
||||
await db
|
||||
.update(time_entries)
|
||||
.set({ ended_at: now, duration_seconds: secs })
|
||||
.where(eq(time_entries.id, r.id));
|
||||
}
|
||||
}
|
||||
|
||||
const id = nanoid();
|
||||
await db.insert(time_entries).values({ id, client_id: clientId });
|
||||
revalidatePath("/admin");
|
||||
return { entryId: id };
|
||||
}
|
||||
|
||||
export async function stopTimer(entryId: string): Promise<void> {
|
||||
const rows = await db
|
||||
.select({ started_at: time_entries.started_at })
|
||||
.from(time_entries)
|
||||
.where(eq(time_entries.id, entryId))
|
||||
.limit(1);
|
||||
|
||||
if (!rows[0]) return;
|
||||
|
||||
const now = new Date();
|
||||
const secs = Math.round((now.getTime() - new Date(rows[0].started_at).getTime()) / 1000);
|
||||
await db
|
||||
.update(time_entries)
|
||||
.set({ ended_at: now, duration_seconds: secs })
|
||||
.where(eq(time_entries.id, entryId));
|
||||
|
||||
revalidatePath("/admin");
|
||||
}
|
||||
Reference in New Issue
Block a user