"use server"; import { z } from "zod"; import { db } from "@/db"; import { leads, quotes, tags, clientTranscripts } from "@/db/schema"; import { nanoid } from "nanoid"; import { eq, and } from "drizzle-orm"; import { createLeadSchema, updateLeadSchema, createActivitySchema, LEAD_STAGES, } from "@/lib/lead-validators"; import { revalidatePath } from "next/cache"; import { createActivity, updateLeadStage } from "@/lib/lead-service"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth"; export async function createLead(data: z.infer) { const parsed = createLeadSchema.safeParse(data); if (!parsed.success) { return { success: false, error: parsed.error.issues[0].message }; } try { const [lead] = await db .insert(leads) .values({ name: parsed.data.name, email: parsed.data.email || null, phone: parsed.data.phone || null, company: parsed.data.company || null, status: parsed.data.status || "contacted", notes: parsed.data.notes || null, }) .returning(); revalidatePath("/admin/leads"); return { success: true, lead }; } catch (error) { console.error("createLead error:", error); return { success: false, error: "Errore nella creazione del lead" }; } } export async function updateLead(id: string, data: z.infer) { const parsed = updateLeadSchema.safeParse(data); if (!parsed.success) { return { success: false, error: parsed.error.issues[0].message }; } try { const updateData: Record = { updated_at: new Date() }; if (parsed.data.name !== undefined) updateData.name = parsed.data.name; if (parsed.data.email !== undefined) updateData.email = parsed.data.email || null; if (parsed.data.phone !== undefined) updateData.phone = parsed.data.phone || null; if (parsed.data.company !== undefined) updateData.company = parsed.data.company || null; if (parsed.data.status !== undefined) updateData.status = parsed.data.status; if (parsed.data.notes !== undefined) updateData.notes = parsed.data.notes || null; const [updated] = await db .update(leads) .set(updateData) .where(eq(leads.id, id)) .returning(); revalidatePath("/admin/leads"); revalidatePath(`/admin/leads/${id}`); return { success: true, lead: updated }; } catch (error) { console.error("updateLead error:", error); return { success: false, error: "Errore nell'aggiornamento" }; } } export async function deleteLead(id: string) { try { await db.delete(leads).where(eq(leads.id, id)); revalidatePath("/admin/leads"); return { success: true }; } catch (error) { console.error("deleteLead error:", error); return { success: false, error: "Errore nell'eliminazione" }; } } // Activity logging export async function logActivity(data: z.infer) { const parsed = createActivitySchema.safeParse(data); if (!parsed.success) { return { success: false, error: parsed.error.issues[0].message }; } try { const activity = await createActivity({ lead_id: parsed.data.lead_id, type: parsed.data.type, activity_date: new Date(parsed.data.activity_date), duration_minutes: parsed.data.duration_minutes, notes: parsed.data.notes, }); revalidatePath(`/admin/leads/${parsed.data.lead_id}`); return { success: true, activity }; } catch (error) { console.error("logActivity error:", error); return { success: false, error: "Errore nella registrazione" }; } } // Quote assignment const assignQuoteSchema = z.object({ lead_id: z.string().min(1), quote_token: z.string().optional(), }); export async function assignQuoteToLead(data: z.infer) { const parsed = assignQuoteSchema.safeParse(data); if (!parsed.success) { return { success: false, error: parsed.error.issues[0].message }; } try { // Link quote to lead and update lead status const token = parsed.data.quote_token; const leadId = parsed.data.lead_id; if (!token) { return { success: false, error: "Token preventivo richiesto" }; } // Find quote by token const [quote] = await db .select() .from(quotes) .where(eq(quotes.token, token)) .limit(1); if (!quote) { return { success: false, error: "Preventivo non trovato" }; } // Update quote to link to lead (if not already linked) await db .update(quotes) .set({ lead_id: leadId }) .where(eq(quotes.id, quote.id)); // Update lead status to "proposal_sent" await updateLeadStage(leadId, "proposal_sent"); revalidatePath(`/admin/leads/${leadId}`); return { success: true }; } catch (error) { console.error("assignQuoteToLead error:", error); return { success: false, error: "Errore nell'assegnazione" }; } } async function requireAdmin() { const session = await getServerSession(authOptions); if (!session) throw new Error("Non autorizzato"); } // ── Inline-edit field update (Phase 14 database-view) ─────────────────────── const EDITABLE_FIELDS = ["name", "email", "phone", "company", "status", "next_action"] as const; type EditableField = (typeof EDITABLE_FIELDS)[number]; export async function updateLeadField( leadId: string, fieldName: EditableField, value: string ) { await requireAdmin(); if (!EDITABLE_FIELDS.includes(fieldName)) { throw new Error(`Campo non editabile: ${fieldName}`); } if (fieldName === "name") { const s = value.trim(); if (s.length === 0) throw new Error("Nome richiesto"); await db.update(leads).set({ name: s, updated_at: new Date() }).where(eq(leads.id, leadId)); } else if (fieldName === "status") { if (!LEAD_STAGES.includes(value as (typeof LEAD_STAGES)[number])) { throw new Error("Stato non valido"); } await db.update(leads).set({ status: value, updated_at: new Date() }).where(eq(leads.id, leadId)); } else { // email | phone | company | next_action — nullable text fields, empty clears const s = value.trim(); await db .update(leads) .set({ [fieldName]: s || null, updated_at: new Date() }) .where(eq(leads.id, leadId)); } revalidatePath("/admin/leads"); revalidatePath(`/admin/leads/${leadId}`); } // ── Lead tags (Phase 14, CRM-09) — polymorphic `tags` table, entity_type="leads" ─ const LEADS_TAG_ENTITY = "leads"; export async function addLeadTag(leadId: string, value: string) { await requireAdmin(); const trimmed = value.trim(); if (trimmed.length === 0) throw new Error("Valore richiesto"); await db .insert(tags) .values({ entity_type: LEADS_TAG_ENTITY, entity_id: leadId, name: trimmed }) .onConflictDoNothing(); revalidatePath("/admin/leads"); revalidatePath(`/admin/leads/${leadId}`); } export async function removeLeadTag(leadId: string, value: string) { await requireAdmin(); await db .delete(tags) .where( and( eq(tags.entity_type, LEADS_TAG_ENTITY), eq(tags.entity_id, leadId), eq(tags.name, value) ) ); revalidatePath("/admin/leads"); revalidatePath(`/admin/leads/${leadId}`); } export async function renameLeadTag(oldValue: string, newValue: string) { await requireAdmin(); const next = newValue.trim(); if (next.length === 0) throw new Error("Nuovo nome richiesto"); if (next === oldValue) return; await db .update(tags) .set({ name: next }) .where(and(eq(tags.entity_type, LEADS_TAG_ENTITY), eq(tags.name, oldValue))); revalidatePath("/admin/leads"); } // ── Transcript actions (Phase 20 — KB-02) ──────────────────────────────────── const addTranscriptSchema = z.object({ lead_id: z.string().min(1), title: z.string().optional(), content: z.string().min(1, "Il testo del transcript è obbligatorio"), call_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Data non valida (YYYY-MM-DD)"), }); export async function addTranscript(data: z.infer) { await requireAdmin(); const parsed = addTranscriptSchema.safeParse(data); if (!parsed.success) { return { success: false, error: parsed.error.issues[0].message }; } try { const [transcript] = await db .insert(clientTranscripts) .values({ id: nanoid(), lead_id: parsed.data.lead_id, title: parsed.data.title || null, content: parsed.data.content, call_date: parsed.data.call_date, }) .returning(); revalidatePath(`/admin/leads/${parsed.data.lead_id}`); return { success: true, transcript }; } catch (error) { console.error("addTranscript error:", error); return { success: false, error: "Errore nel salvataggio del transcript" }; } } export async function deleteTranscript(transcriptId: string, leadId: string) { await requireAdmin(); if (!transcriptId) throw new Error("ID transcript richiesto"); try { await db .delete(clientTranscripts) .where(eq(clientTranscripts.id, transcriptId)); revalidatePath(`/admin/leads/${leadId}`); return { success: true }; } catch (error) { console.error("deleteTranscript error:", error); return { success: false, error: "Errore nell'eliminazione del transcript" }; } }