"use server"; import { revalidatePath } from "next/cache"; import { db } from "@/db"; import { phases, tasks, deliverables, documents, payments, clients, comments, } from "@/db/schema"; import { eq } from "drizzle-orm"; import { z } from "zod"; // ── PHASES ──────────────────────────────────────────────────────────────────── 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 existingPhases = await db .select({ sort_order: phases.sort_order }) .from(phases) .where(eq(phases.client_id, clientId)); const maxOrder = existingPhases.reduce((max, p) => Math.max(max, p.sort_order), -1); await db.insert(phases).values({ client_id: clientId, 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); await db.insert(documents).values({ client_id: clientId, ...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)); // Split evenly between two payment rows (Acconto 50% + Saldo 50%) const half = (val / 2).toFixed(2); const paymentsRows = await db .select() .from(payments) .where(eq(payments.client_id, clientId)); 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}`); }