"use server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { db } from "@/db"; import { phases, tasks, deliverables, documents, payments, clients, projects, comments, } from "@/db/schema"; import { eq, asc, inArray } from "drizzle-orm"; import { z } from "zod"; // ── CLIENT CRUD ─────────────────────────────────────────────────────────────── const clientSchema = z.object({ name: z.string().min(1, "Nome richiesto"), brand_name: z.string().min(1, "Brand name richiesto"), brief: z.string(), }); export async function updateClient(clientId: string, formData: FormData) { const parsed = clientSchema.safeParse({ name: formData.get("name"), brand_name: formData.get("brand_name"), brief: formData.get("brief") ?? "", }); if (!parsed.success) throw new Error(parsed.error.issues[0].message); await db.update(clients).set(parsed.data).where(eq(clients.id, clientId)); revalidatePath(`/admin/clients/${clientId}`); revalidatePath("/admin"); } export async function deleteClient(clientId: string) { await db.delete(clients).where(eq(clients.id, clientId)); revalidatePath("/admin"); redirect("/admin"); } export async function setClientArchived(clientId: string, archived: boolean) { await db.update(clients).set({ archived }).where(eq(clients.id, clientId)); revalidatePath(`/admin/clients/${clientId}`); revalidatePath("/admin"); } // ── PHASES ──────────────────────────────────────────────────────────────────── async function getDefaultProjectId(clientId: string): Promise { const rows = await db .select({ id: projects.id }) .from(projects) .where(eq(projects.client_id, clientId)) .orderBy(asc(projects.created_at)) .limit(1); return rows[0]?.id ?? null; } export async function addPhase(clientId: string, formData: FormData) { const title = (formData.get("title") as string)?.trim(); if (!title) throw new Error("Titolo fase richiesto"); const projectId = await getDefaultProjectId(clientId); if (!projectId) throw new Error("Nessun progetto trovato per questo cliente"); const existingPhases = await db .select({ sort_order: phases.sort_order }) .from(phases) .where(eq(phases.project_id, projectId)); const maxOrder = existingPhases.reduce((max, p) => Math.max(max, p.sort_order), -1); await db.insert(phases).values({ project_id: projectId, title, sort_order: maxOrder + 1, status: "upcoming", }); revalidatePath(`/admin/clients/${clientId}`); } export async function updatePhaseStatus( phaseId: string, clientId: string, status: string ) { const allowed = ["upcoming", "active", "done"]; if (!allowed.includes(status)) throw new Error("Stato non valido"); await db.update(phases).set({ status }).where(eq(phases.id, phaseId)); revalidatePath(`/admin/clients/${clientId}`); } // ── TASKS ───────────────────────────────────────────────────────────────────── export async function addTask( phaseId: string, clientId: string, formData: FormData ) { const title = (formData.get("title") as string)?.trim(); if (!title) throw new Error("Titolo task richiesto"); const existingTasks = await db .select({ sort_order: tasks.sort_order }) .from(tasks) .where(eq(tasks.phase_id, phaseId)); const maxOrder = existingTasks.reduce((max, t) => Math.max(max, t.sort_order), -1); await db.insert(tasks).values({ phase_id: phaseId, title, description: (formData.get("description") as string)?.trim() || null, sort_order: maxOrder + 1, status: "todo", }); revalidatePath(`/admin/clients/${clientId}`); } export async function updateTaskStatus( taskId: string, clientId: string, status: string ) { const allowed = ["todo", "in_progress", "done"]; if (!allowed.includes(status)) throw new Error("Stato non valido"); await db.update(tasks).set({ status }).where(eq(tasks.id, taskId)); revalidatePath(`/admin/clients/${clientId}`); } // ── DELIVERABLES ────────────────────────────────────────────────────────────── export async function addDeliverable( taskId: string, clientId: string, formData: FormData ) { const title = (formData.get("title") as string)?.trim(); const url = (formData.get("url") as string)?.trim() || null; if (!title) throw new Error("Titolo deliverable richiesto"); // approved_at is intentionally omitted — immutable constraint: never set by admin here await db .insert(deliverables) .values({ task_id: taskId, title, url, status: "pending" }); revalidatePath(`/admin/clients/${clientId}`); } // ── DOCUMENTS ───────────────────────────────────────────────────────────────── const docSchema = z.object({ label: z.string().min(1, "Etichetta richiesta"), url: z.string().url("URL non valido"), }); export async function addDocument(clientId: string, formData: FormData) { const parsed = docSchema.safeParse({ label: formData.get("label"), url: formData.get("url"), }); if (!parsed.success) throw new Error(parsed.error.issues[0].message); const projectId = await getDefaultProjectId(clientId); if (!projectId) throw new Error("Nessun progetto trovato per questo cliente"); await db.insert(documents).values({ project_id: projectId, ...parsed.data }); revalidatePath(`/admin/clients/${clientId}`); } export async function updateDocument( documentId: string, clientId: string, formData: FormData ) { const parsed = docSchema.safeParse({ label: formData.get("label"), url: formData.get("url"), }); if (!parsed.success) throw new Error(parsed.error.issues[0].message); await db .update(documents) .set(parsed.data) .where(eq(documents.id, documentId)); revalidatePath(`/admin/clients/${clientId}`); } export async function deleteDocument(documentId: string, clientId: string) { await db.delete(documents).where(eq(documents.id, documentId)); revalidatePath(`/admin/clients/${clientId}`); } // ── PAYMENTS ────────────────────────────────────────────────────────────────── export async function updatePaymentStatus( paymentId: string, clientId: string, status: string ) { const allowed = ["da_saldare", "inviata", "saldato"]; if (!allowed.includes(status)) throw new Error("Stato pagamento non valido"); const paid_at = status === "saldato" ? new Date() : null; await db .update(payments) .set({ status, paid_at }) .where(eq(payments.id, paymentId)); revalidatePath(`/admin/clients/${clientId}`); } export async function updateAcceptedTotal(clientId: string, formData: FormData) { const raw = (formData.get("accepted_total") as string)?.trim(); const val = parseFloat(raw); if (isNaN(val) || val < 0) throw new Error("Importo non valido"); // Update accepted_total on client row — denormalized field, quote_items never exposed await db .update(clients) .set({ accepted_total: val.toFixed(2) }) .where(eq(clients.id, clientId)); // Get all projects for this client, then update their payment stubs const projectRows = await db .select({ id: projects.id }) .from(projects) .where(eq(projects.client_id, clientId)); if (projectRows.length > 0) { const projectIds = projectRows.map((p) => p.id); const half = (val / 2).toFixed(2); const paymentsRows = await db .select() .from(payments) .where(inArray(payments.project_id, projectIds)); for (const p of paymentsRows) { await db.update(payments).set({ amount: half }).where(eq(payments.id, p.id)); } } revalidatePath(`/admin/clients/${clientId}`); } // ── COMMENTS (admin reply) ──────────────────────────────────────────────────── export async function postAdminComment(clientId: string, formData: FormData) { const entity = formData.get("entity") as string; const body = (formData.get("body") as string)?.trim(); if (!body || !entity) throw new Error("Dati mancanti"); const [entity_type, entity_id] = entity.split(":"); if (!entity_type || !entity_id) throw new Error("Formato entity non valido"); const allowedTypes = ["task", "deliverable"]; if (!allowedTypes.includes(entity_type)) throw new Error("entity_type non valido"); await db.insert(comments).values({ entity_type, entity_id, author: "admin", body }); revalidatePath(`/admin/clients/${clientId}`); }