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:
@@ -1,5 +1,5 @@
|
||||
import { db } from "@/db";
|
||||
import { clients, payments } from "@/db/schema";
|
||||
import { clients, payments, time_entries } from "@/db/schema";
|
||||
import { sql, and, eq } from "drizzle-orm";
|
||||
|
||||
export async function getAnalyticsByYear(year: number) {
|
||||
@@ -68,4 +68,50 @@ export async function getAvailableYears(): Promise<number[]> {
|
||||
const currentYear = new Date().getFullYear();
|
||||
if (!years.includes(currentYear)) years.push(currentYear);
|
||||
return years.sort((a, b) => b - a);
|
||||
}
|
||||
|
||||
// ── Time tracking ─────────────────────────────────────────────────────────────
|
||||
|
||||
export type ClientTimeRow = {
|
||||
clientId: string;
|
||||
clientName: string;
|
||||
totalSeconds: number;
|
||||
};
|
||||
|
||||
export async function getTimeByClient(year: number): Promise<ClientTimeRow[]> {
|
||||
const rows = await db
|
||||
.select({
|
||||
client_id: time_entries.client_id,
|
||||
total: sql<string>`coalesce(sum(${time_entries.duration_seconds}), 0)`,
|
||||
})
|
||||
.from(time_entries)
|
||||
.where(
|
||||
sql`${time_entries.ended_at} is not null
|
||||
and extract(year from ${time_entries.started_at}) = ${year}`
|
||||
)
|
||||
.groupBy(time_entries.client_id);
|
||||
|
||||
if (rows.length === 0) return [];
|
||||
|
||||
const allClients = await db.select({ id: clients.id, name: clients.name }).from(clients);
|
||||
const nameMap = new Map(allClients.map((c) => [c.id, c.name]));
|
||||
|
||||
return rows
|
||||
.map((r) => ({
|
||||
clientId: r.client_id,
|
||||
clientName: nameMap.get(r.client_id) ?? r.client_id,
|
||||
totalSeconds: parseInt(r.total),
|
||||
}))
|
||||
.sort((a, b) => b.totalSeconds - a.totalSeconds);
|
||||
}
|
||||
|
||||
export async function getTotalTrackedHours(year: number): Promise<number> {
|
||||
const [row] = await db
|
||||
.select({ total: sql<string>`coalesce(sum(${time_entries.duration_seconds}), 0)` })
|
||||
.from(time_entries)
|
||||
.where(
|
||||
sql`${time_entries.ended_at} is not null
|
||||
and extract(year from ${time_entries.started_at}) = ${year}`
|
||||
);
|
||||
return Math.round(parseInt(row?.total ?? "0") / 3600 * 10) / 10;
|
||||
}
|
||||
Reference in New Issue
Block a user