"use client"; // ============================================================ // Mail — a dedicated reader for app-to-app and email messages, // powered by IIOS via the be-crm data door (crm.mail.*). Distinct // from Messenger (chat) and from the work-item Inbox: this shows the // actual mail (subject + rendered body) and lets you read + reply + // compose. HTML bodies render inside a sandboxed iframe (no scripts). // ============================================================ import { type CSSProperties, useEffect, useState } from "react"; import { Avatar, Btn, Field, Icon, Modal, PageHead, Pill, useToast } from "./ui"; import { useMailThreads, useMailThread, useMailCompose, type MailThread, type MailPerson } from "@/lib/mail-api"; const timeOf = (iso?: string) => { if (!iso) return ""; const d = new Date(iso); return Number.isNaN(+d) ? "" : d.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); }; const CUSTOMER_GRAD = "linear-gradient(135deg,#10b981,#059669)"; const inputStyle: CSSProperties = { width: "100%", padding: "9px 12px", borderRadius: 10, border: "1px solid var(--border)", background: "var(--panel)", color: "var(--text)", fontSize: 14, outline: "none", }; export function Mail() { const list = useMailThreads(); const toast = useToast(); const [selected, setSelected] = useState(null); const [newOpen, setNewOpen] = useState(false); useEffect(() => { if ((!selected || !list.threads.some((t) => t.threadId === selected)) && list.threads[0]) { setSelected(list.threads[0].threadId); } }, [list.threads, selected]); const current = list.threads.find((t) => t.threadId === selected) ?? null; return (
setNewOpen(true)}>New mail} /> {!list.live && (
Demo mode — running on mock data. It goes live once the Shell + be-crm are connected.
)}
{current ? ( toast.push({ tone: "error", title: "Failed", desc: m })} /> ) : (

Select or compose a message

)}
setNewOpen(false)} onSent={() => { setNewOpen(false); list.refetch(); toast.push({ tone: "success", title: "Sent" }); }} onError={(m) => toast.push({ tone: "error", title: "Couldn't send", desc: m })} />
); } function ThreadRow({ t, active, onClick }: { t: MailThread; active: boolean; onClick: () => void }) { return ( ); } function Reader({ thread, onError }: { thread: MailThread; onError: (m: string) => void }) { const t = useMailThread(thread.threadId); const [draft, setDraft] = useState(""); const [sending, setSending] = useState(false); async function reply() { const text = draft.trim(); if (!text || sending) return; setDraft(""); setSending(true); try { await t.reply(text); } catch (e) { setDraft(text); onError((e as Error).message); } finally { setSending(false); } } return ( <>
{thread.subject || "(no subject)"}
{thread.participants.length} participant(s)
{t.loading && t.messages.length === 0 &&
Loading…
} {!t.loading && t.messages.length === 0 &&
No messages in this thread.
} {t.messages.map((m) => (
{m.kind === "EMAIL" ? "Email" : "Reply"}{m.actorId ? ` · ${m.actorId.replace(/^(pp_|cust_)/, "").slice(0, 8)}` : ""} {timeOf(m.occurredAt)}
{m.html ?