c1cc13a99a
Nuova pagina admin /admin/conversazioni: vista WhatsApp-style (lista conversazioni a sinistra, thread + risposta a destra) che aggrega i messaggi di tutti i clienti dalla tabella comments, cross-cliente. - Migration additiva 0014: clients.admin_last_read_at per tracciare letto/non-letto (pallini + badge in sidebar). Applicata a prod. - conversations-queries.ts: aggregazione entity_id → cliente (general/phase/task/deliverable), getConversations / getConversationThread / getUnreadConversationsCount. - Risposte admin salvate come commento "general" (visibili anche nella chat cliente e nel CommentsTab della scheda). - Voce di menu + badge non-letti (AdminSidebar/AdminShell/layout). - Token semantici per dual light/dark; refresh manuale (no polling). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
60 lines
1.8 KiB
TypeScript
60 lines
1.8 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { getServerSession } from "next-auth";
|
|
import { eq } from "drizzle-orm";
|
|
import { authOptions } from "@/lib/auth";
|
|
import { db } from "@/db";
|
|
import { clients, comments } from "@/db/schema";
|
|
|
|
async function requireAdmin() {
|
|
const session = await getServerSession(authOptions);
|
|
if (!session) throw new Error("Non autorizzato");
|
|
}
|
|
|
|
/**
|
|
* Admin reply from the Conversazioni inbox. Per project decision, replies are
|
|
* saved as a "general" comment on the client (entity_id = clientId), so they
|
|
* surface in the client's general chat and in the client detail CommentsTab.
|
|
*/
|
|
export async function replyToConversation(clientId: string, formData: FormData) {
|
|
await requireAdmin();
|
|
const body = (formData.get("body") as string)?.trim();
|
|
if (!clientId || !body) throw new Error("Dati mancanti");
|
|
|
|
// Validate the client exists (entity_id integrity).
|
|
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");
|
|
|
|
await db.insert(comments).values({
|
|
entity_type: "general",
|
|
entity_id: clientId,
|
|
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");
|
|
}
|