diff --git a/src/components/dashboard/dashboard.tsx b/src/components/dashboard/dashboard.tsx index d052a4e..cdf5fb8 100644 --- a/src/components/dashboard/dashboard.tsx +++ b/src/components/dashboard/dashboard.tsx @@ -16,6 +16,7 @@ import { Rules } from "./rules"; import { AiAssistant } from "./ai-assistant"; import { TeamManagement } from "./team-management"; import { Messenger } from "./messenger"; +import { Mail } from "./mail"; import { Inbox } from "./inbox"; import "../../app/dashboard/dashboard.css"; @@ -48,6 +49,7 @@ export function Dashboard() { : active === "rules" ? : active === "ai" ? : active === "messenger" ? + : active === "mail" ? : active === "inbox" ? : active === "team" ? : } diff --git a/src/components/dashboard/mail.tsx b/src/components/dashboard/mail.tsx new file mode 100644 index 0000000..8fd4c8e --- /dev/null +++ b/src/components/dashboard/mail.tsx @@ -0,0 +1,216 @@ +"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 + ?