"use client"; // Mail data layer. A dedicated Mail reader over the be-crm data door (crm.mail.*), distinct from // the Messenger chat and from the work-item Inbox. Live via the AppShell SDK; a small mock keeps the // demo working before the Shell + be-crm are connected. // // Live contract (be-crm): // query crm.mail.list {} -> MailThread[] // query crm.mail.history { threadId } -> MailMessage[] // cmd crm.mail.reply { threadId, content } -> { interactionId, threadId } // cmd crm.mail.internal { recipientUserId, subject?, text?, html? } -> { threadId } // cmd crm.mail.send { target, subject?, text?, html?, mirrorToUserId? } -> { commandId } // query crm.messenger.directory { kind, limit } -> people to compose to (reused) import { useCallback, useMemo, useState } from "react"; import { useAppShell, useQuery } from "@abe-kap/appshell-sdk/react"; import { isShellConfigured } from "./appshell"; export interface MailThread { threadId: string; subject: string | null; participants: string[]; unread: number; lastMessage?: string; lastAt?: string; } export interface MailMessage { interactionId: string; actorId: string | null; kind: string; occurredAt: string; html: string | null; text: string | null; } export interface MailPerson { id: string; name: string; kind: "staff" | "customer" } const SHELL = isShellConfigured(); /* ============================ Thread list ============================ */ export interface MailListData { live: boolean; loading: boolean; error: string | null; threads: MailThread[]; refetch: () => void; } export function useMailThreads(): MailListData { if (SHELL) { const q = useQuery("crm.mail.list", {}); return { live: true, loading: q.loading, error: q.error?.message ?? null, threads: q.data ?? [], refetch: q.refetch }; } return { live: false, loading: false, error: null, threads: MOCK_THREADS, refetch: () => {} }; } /* ============================ One thread ============================ */ export interface MailThreadData { loading: boolean; error: string | null; messages: MailMessage[]; reply: (content: string) => Promise; refetch: () => void; } export function useMailThread(threadId: string | null): MailThreadData { if (SHELL) return useLiveThread(threadId); return useMockThread(threadId); } function useLiveThread(threadId: string | null): MailThreadData { const { sdk } = useAppShell(); const q = useQuery("crm.mail.history", threadId ? { threadId } : { threadId: "" }); const reply = useCallback(async (content: string) => { if (!threadId) return; await sdk.command("crm.mail.reply", { threadId, content }); q.refetch(); }, [sdk, threadId, q]); return { loading: q.loading, error: q.error?.message ?? null, messages: threadId ? (q.data ?? []) : [], reply, refetch: q.refetch }; } /* ============================ Compose ============================ */ export interface ComposeData { directory: MailPerson[]; sendInternal: (recipientUserId: string, subject: string, text: string) => Promise; sendExternal: (target: string, subject: string, text: string, mirrorToUserId?: string) => Promise; } export function useMailCompose(onSent: () => void): ComposeData { if (SHELL) { const { sdk } = useAppShell(); const dirQ = useQuery("crm.messenger.directory", { kind: "all", limit: 100 }); const directory = useMemo(() => (dirQ.data ?? []).map((d) => ({ id: (d as unknown as { id: string }).id, name: (d as unknown as { displayName?: string; name?: string }).displayName ?? (d as unknown as { name?: string }).name ?? "", kind: (d as MailPerson).kind })), [dirQ.data]); const sendInternal = useCallback(async (recipientUserId: string, subject: string, text: string) => { await sdk.command("crm.mail.internal", { recipientUserId, subject, text, html: `

${escapeHtml(text)}

` }); onSent(); }, [sdk, onSent]); const sendExternal = useCallback(async (target: string, subject: string, text: string, mirrorToUserId?: string) => { await sdk.command("crm.mail.send", { target, subject, text, html: `

${escapeHtml(text)}

`, ...(mirrorToUserId ? { mirrorToUserId } : {}) }); onSent(); }, [sdk, onSent]); return { directory, sendInternal, sendExternal }; } return { directory: MOCK_PEOPLE, sendInternal: async () => onSent(), sendExternal: async () => onSent() }; } function escapeHtml(s: string): string { return s.replace(/&/g, "&").replace(//g, ">"); } /* ============================ Mock (demo mode) ============================ */ const now = () => new Date().toISOString(); const MOCK_PEOPLE: MailPerson[] = [ { id: "pp_sofia", name: "Sofia Ramirez", kind: "staff" }, { id: "cust_acme", name: "Acme Roofing (Client)", kind: "customer" }, ]; const MOCK_THREADS: MailThread[] = [ { threadId: "mt_1", subject: "Welcome to the Founders Club", participants: ["you", "system"], unread: 1, lastMessage: "Thanks for joining…", lastAt: now() }, { threadId: "mt_2", subject: "Storm response — East side", participants: ["you", "pp_sofia"], unread: 0, lastMessage: "Crew rolling out at 7", lastAt: now() }, ]; function useMockThread(threadId: string | null): MailThreadData { const [extra, setExtra] = useState([]); const base: MailMessage[] = threadId === "mt_1" ? [{ interactionId: "m1", actorId: "system", kind: "EMAIL", occurredAt: now(), html: "

Thanks for joining the Founders Club. Set up your account to get started.

", text: "Thanks for joining the Founders Club." }] : threadId === "mt_2" ? [{ interactionId: "m2", actorId: "pp_sofia", kind: "EMAIL", occurredAt: now(), html: "

Crew is rolling out at 7. Confirm the Henderson scope?

", text: "Crew rolling out at 7." }] : []; const reply = useCallback(async (content: string) => { setExtra((l) => [...l, { interactionId: `r_${l.length}`, actorId: "you", kind: "MESSAGE", occurredAt: now(), html: null, text: content }]); }, []); return { loading: false, error: null, messages: threadId ? [...base, ...extra] : [], reply, refetch: () => {} }; }