feat(chat): chat a canali nel portale cliente e inbox admin per canale

Il portale aveva una sola conversazione con un selettore di fase in un
dropdown: il cliente non vedeva dove c'era del non letto, e una risposta
admin poteva atterrare su un'entità diversa da quella della domanda.

Ora i messaggi si organizzano in canali — "Generale" più uno per fase —
derivati in un solo posto (src/lib/chat-channels.ts) così che le due
sponde concordino sulla stessa chiave. Task e deliverable non sono più
scrivibili ma lo storico non resta orfano: rientra nel canale della fase
proprietaria conservando il nome dell'entità come badge.

- migration 0021 (additiva, già applicata in prod): client_channel_reads
  per il letto/non-letto per canale lato cliente, più il primo indice mai
  esistito su comments (entity_id, created_at)
- GET/POST /api/client/chat: polling dei messaggi e ricevuta di lettura
- ChatPanel: tab per canale, pallino di non letto, modalità full-screen
- inbox admin: tab per canale con targeting dell'entità corretta in
  risposta e snapshot di adminLastReadAt sul thread

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 17:50:44 +02:00
parent 53f1758f52
commit ac74a81a72
14 changed files with 1320 additions and 332 deletions
+10 -3
View File
@@ -12,7 +12,6 @@ import { ClientDashboard } from "@/components/client-dashboard";
import { OtpGate } from "@/components/client/OtpGate";
import { PreviewBanner } from "@/components/client/PreviewBanner";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import type { Comment } from "@/db/schema";
import { normalizeTaskStatus } from "@/lib/task-status";
export const revalidate = 0;
@@ -147,7 +146,11 @@ export default async function ClientPage({
<ClientDashboard
view={projectViewToClientView(client, view)}
token={client.token}
comments={view.comments as unknown as Comment[]}
chat={{
projectId: view.project.id,
messages: view.comments,
reads: view.channel_reads,
}}
preview={preview}
/>
</>
@@ -193,7 +196,11 @@ export default async function ClientPage({
<ClientDashboard
view={projectViewToClientView(client, view)}
token={client.token}
comments={view.comments as unknown as Comment[]}
chat={{
projectId: view.project.id,
messages: view.comments,
reads: view.channel_reads,
}}
embedded
preview={preview}
/>