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:
@@ -1,15 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, useCallback } from "react";
|
||||
import { createContext, useContext, useState, useCallback, useMemo } from "react";
|
||||
import type { ClientView } from "@/lib/client-view";
|
||||
|
||||
/**
|
||||
* Stato di sola UI della chat: quale canale è aperto, se il pannello è visibile,
|
||||
* se è a tutto schermo. I messaggi NON stanno qui — vivono in ChatPanel, che è
|
||||
* l'unico a leggerli, a fare polling e a tenerne la copia optimistic.
|
||||
*
|
||||
* Il canale sta invece nel context perché lo decide anche chi è fuori dal
|
||||
* pannello: la bolla su una PhaseCard apre la chat già sulla fase giusta.
|
||||
*/
|
||||
interface ChatContextValue {
|
||||
isOpen: boolean;
|
||||
selectedPhaseId: string | null; // null = "Generale"
|
||||
expanded: boolean;
|
||||
/** Chiave del canale attivo: clientId per "Generale", phases.id per una fase. */
|
||||
activeChannel: string;
|
||||
phases: ClientView["phases"];
|
||||
clientId: string;
|
||||
openChat: (phaseId?: string) => void;
|
||||
closeChat: () => void;
|
||||
toggleExpanded: () => void;
|
||||
setActiveChannel: (key: string) => void;
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ChatContextValue | null>(null);
|
||||
@@ -30,20 +42,36 @@ export function ChatProvider({
|
||||
clientId: string;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedPhaseId, setSelectedPhaseId] = useState<string | null>(null);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
// Il canale "Generale" è identificato dall'id del cliente: stessa convenzione
|
||||
// di comments.entity_id, così non serve tradurre nulla in scrittura.
|
||||
const [activeChannel, setActiveChannel] = useState<string>(clientId);
|
||||
|
||||
const openChat = useCallback((phaseId?: string) => {
|
||||
setSelectedPhaseId(phaseId ?? null);
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeChat = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ChatContext.Provider value={{ isOpen, selectedPhaseId, phases, clientId, openChat, closeChat }}>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
const openChat = useCallback(
|
||||
(phaseId?: string) => {
|
||||
setActiveChannel(phaseId ?? clientId);
|
||||
setIsOpen(true);
|
||||
},
|
||||
[clientId]
|
||||
);
|
||||
|
||||
const closeChat = useCallback(() => setIsOpen(false), []);
|
||||
const toggleExpanded = useCallback(() => setExpanded((v) => !v), []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
isOpen,
|
||||
expanded,
|
||||
activeChannel,
|
||||
phases,
|
||||
clientId,
|
||||
openChat,
|
||||
closeChat,
|
||||
toggleExpanded,
|
||||
setActiveChannel,
|
||||
}),
|
||||
[isOpen, expanded, activeChannel, phases, clientId, openChat, closeChat, toggleExpanded]
|
||||
);
|
||||
|
||||
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user