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
+17 -1
View File
@@ -26,6 +26,7 @@ export const clients = pgTable("clients", {
accepted_total: numeric("accepted_total", { precision: 10, scale: 2 }).default(
"0"
),
archived: boolean("archived").notNull().default(false),
created_at: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
@@ -130,6 +131,19 @@ export const notes = pgTable("notes", {
.defaultNow(),
});
// ============ TIME ENTRIES (admin time tracking per client) ============
export const time_entries = pgTable("time_entries", {
id: text("id")
.primaryKey()
.$defaultFn(() => nanoid()),
client_id: text("client_id")
.notNull()
.references(() => clients.id, { onDelete: "cascade" }),
started_at: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
ended_at: timestamp("ended_at", { withTimezone: true }),
duration_seconds: integer("duration_seconds"), // set on stop
});
// ============ SERVICE CATALOG (admin-only, used for quote generation) ============
export const service_catalog = pgTable("service_catalog", {
id: text("id")
@@ -242,4 +256,6 @@ export type NewNote = typeof notes.$inferInsert;
export type ServiceCatalog = typeof service_catalog.$inferSelect;
export type NewServiceCatalog = typeof service_catalog.$inferInsert;
export type QuoteItem = typeof quote_items.$inferSelect;
export type NewQuoteItem = typeof quote_items.$inferInsert;
export type NewQuoteItem = typeof quote_items.$inferInsert;
export type TimeEntry = typeof time_entries.$inferSelect;
export type NewTimeEntry = typeof time_entries.$inferInsert;