"use server"; import { revalidatePath } from "next/cache"; import { getServerSession } from "next-auth"; import { eq } from "drizzle-orm"; import { z } from "zod"; import { authOptions } from "@/lib/auth"; import { db } from "@/db"; import { clients, comments } from "@/db/schema"; import { assertClientOwnsEntity } from "@/lib/client-chat"; async function requireAdmin() { const session = await getServerSession(authOptions); if (!session) throw new Error("Non autorizzato"); } const replySchema = z.object({ // "general" ancora il messaggio al cliente, "phase" a una sua fase. Task e // deliverable restano fuori: nessuna UI li scrive più, da nessuna delle due parti. entity_type: z.enum(["general", "phase"]), entity_id: z.string().min(1), body: z.string().trim().min(1, "Il messaggio non può essere vuoto").max(2000), }); /** * Risposta dell'admin dall'inbox Conversazioni, sul canale da cui si sta * scrivendo. * * Fino alla chat a canali l'admin poteva rispondere SOLO sul thread generale: * i messaggi su fase/task/deliverable si leggevano con la loro etichetta ma la * risposta tornava sempre in Generale. Con i tab quella asimmetria diventava un * bug visibile — domanda in "Fase 2", risposta in un altro tab — quindi ora il * canale viaggia insieme al messaggio. */ export async function replyToConversation(clientId: string, formData: FormData) { await requireAdmin(); const parsed = replySchema.safeParse({ entity_type: formData.get("entity_type"), entity_id: formData.get("entity_id"), body: formData.get("body"), }); if (!clientId || !parsed.success) throw new Error("Dati mancanti"); const { entity_type, entity_id, body } = parsed.data; const rows = await db .select({ id: clients.id }) .from(clients) .where(eq(clients.id, clientId)) .limit(1); if (rows.length === 0) throw new Error("Cliente non trovato"); // Stessa verifica di appartenenza usata dalle route del portale: un entity_id // arbitrario nel form non deve poter scrivere nella chat di un altro cliente. const owns = await assertClientOwnsEntity(clientId, entity_type, entity_id); if (!owns) throw new Error("Canale non valido per questo cliente"); await db.insert(comments).values({ entity_type, entity_id, author: "admin", body, }); // Replying implies the admin has read the incoming messages. await db .update(clients) .set({ admin_last_read_at: new Date() }) .where(eq(clients.id, clientId)); revalidatePath("/admin/conversazioni"); revalidatePath(`/admin/clients/${clientId}`); } /** Mark a client's conversation as read up to now (clears unread dot/badge). */ export async function markConversationRead(clientId: string) { await requireAdmin(); if (!clientId) return; await db .update(clients) .set({ admin_last_read_at: new Date() }) .where(eq(clients.id, clientId)); revalidatePath("/admin/conversazioni"); }