diff --git a/src/components/dashboard/inbox-sdk.tsx b/src/components/dashboard/inbox-sdk.tsx index 43fb8df..1c0fbed 100644 --- a/src/components/dashboard/inbox-sdk.tsx +++ b/src/components/dashboard/inbox-sdk.tsx @@ -12,19 +12,12 @@ import "@insignia/iios-messaging-ui/styles.css"; import { isShellConfigured } from "@/lib/appshell"; import { CrmInboxAdapter } from "@/lib/crm-inbox-adapter"; import type { DataDoor } from "@/lib/crm-messaging-adapter"; -import { PageHead } from "./ui"; const SHELL = isShellConfigured(); export function InboxSdk() { return (
- {!SHELL && (
Demo mode — running on the SDK's mock inbox adapter. diff --git a/src/components/dashboard/inbox.tsx b/src/components/dashboard/inbox.tsx deleted file mode 100644 index c79ac1c..0000000 --- a/src/components/dashboard/inbox.tsx +++ /dev/null @@ -1,152 +0,0 @@ -"use client"; - -// ============================================================ -// Inbox — the ONE unified communication surface. It lists everything -// IIOS surfaces for you (mentions, needs-reply, system alerts, support -// updates, …) AND the mail behind them: click an item tied to a thread -// and its conversation opens on the right to read + reply. Compose new -// mail from here too. Items come from crm.inbox.*; threads from crm.mail.*. -// ============================================================ - -import { useEffect, useState } from "react"; -import { Btn, Icon, PageHead, Pill, useToast } from "./ui"; -import { useInboxData, type InboxState, type UiInboxItem } from "@/lib/inbox-api"; -import { MailReader, NewMailModal } from "./mail"; - -const KIND_LABEL: Record = { - MAIL: "Mail", - MENTION: "Mention", NEEDS_REPLY: "Needs reply", NEEDS_REVIEW: "Needs review", NEEDS_APPROVAL: "Needs approval", - SUPPORT_UPDATE: "Support", MEETING_FOLLOWUP: "Meeting", DIGEST: "Digest", SYSTEM_ALERT: "Alert", CRM_OWNER_INTEREST: "Owner", -}; -const FILTERS: { value: InboxState; label: string }[] = [ - { value: "OPEN", label: "Open" }, { value: "SNOOZED", label: "Snoozed" }, { value: "DONE", label: "Done" }, { value: "ARCHIVED", label: "Archived" }, -]; - -export function Inbox() { - const [filter, setFilter] = useState("OPEN"); - const inbox = useInboxData(filter); - const toast = useToast(); - const [selectedId, setSelectedId] = useState(null); - const [newOpen, setNewOpen] = useState(false); - - useEffect(() => { - if ((!selectedId || !inbox.items.some((i) => i.id === selectedId)) && inbox.items[0]) setSelectedId(inbox.items[0].id); - }, [inbox.items, selectedId]); - - const selected = inbox.items.find((i) => i.id === selectedId) ?? null; - - return ( -
- setNewOpen(true)}>New mail} - /> - {!inbox.live && ( -
- Demo mode — running on mock data. It goes live once the Shell + be-crm are connected. -
- )} - -
- {FILTERS.map((f) => ( - setFilter(f.value)}>{f.label} - ))} -
- -
- {/* Left — the unified item list */} - - - {/* Right — read the mail behind the item, or the item detail */} -
- {selected ? ( - toast.push({ tone: "error", title: "Failed", desc: m })} - onDone={() => inbox.transition(selected.id, "DONE")} - onSnooze={() => inbox.transition(selected.id, "SNOOZED")} - onArchive={() => inbox.transition(selected.id, "ARCHIVED")} - /> - ) : ( -
-

Select an item to read

-
- )} -
-
- - setNewOpen(false)} - onSent={() => { setNewOpen(false); inbox.refetch(); toast.push({ tone: "success", title: "Sent" }); }} - onError={(m) => toast.push({ tone: "error", title: "Couldn't send", desc: m })} - /> -
- ); -} - -function ItemRow({ it, active, onClick }: { it: UiInboxItem; active: boolean; onClick: () => void }) { - const isMention = it.kind === "MENTION"; - return ( - - ); -} - -function Detail({ it, onError, onDone, onSnooze, onArchive }: { - it: UiInboxItem; onError: (m: string) => void; onDone: () => void; onSnooze: () => void; onArchive: () => void; -}) { - return ( - <> - {/* Item actions bar — only for real inbox work-items. Mail isn't an inbox item - (no crm.inbox.transition), so it gets read/reply only, no Done/Snooze/Archive. */} - {it.state === "OPEN" && it.kind !== "MAIL" && ( -
- Snooze - Done - Archive -
- )} - - {it.threadId ? ( - // A message/mail item → open the conversation to read + reply. - // key by threadId: the SDK's useQuery only refetches when the ACTION changes, not the - // variables — so switching items must remount MailReader to load the new thread's history. -
- -
- ) : ( - // A non-threaded item (e.g. a system alert) → show its detail. -
-
{it.title}
- {it.summary &&
{it.summary}
} -
- )} - - ); -} diff --git a/src/components/dashboard/mail.tsx b/src/components/dashboard/mail.tsx deleted file mode 100644 index 9ee09da..0000000 --- a/src/components/dashboard/mail.tsx +++ /dev/null @@ -1,231 +0,0 @@ -"use client"; - -// ============================================================ -// Mail components used INSIDE the Inbox (not a separate tab). -// The Inbox is the one unified surface — mentions, system messages -// and mail all live there. These render the mail body + reply, and -// compose a new message. HTML bodies render in a sandboxed iframe. -// ============================================================ - -import { type CSSProperties, useEffect, useRef, useState } from "react"; -import { Avatar, Btn, Field, Icon, Modal, Pill } from "./ui"; -import { useMailThread, useMailCompose, type MailAttachment, type MailPerson } from "@/lib/mail-api"; -import { useUploadAttachment, useDownloadUrl, isImage, type UploadedAttachment } from "@/lib/media-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", -}; - -function fmtBytes(n: number): string { - if (!n) return ""; - if (n < 1024) return `${n} B`; - if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`; - return `${(n / (1024 * 1024)).toFixed(1)} MB`; -} - -/** Resolves a signed URL for a stored attachment and renders it inline (image) or as a file chip. */ -function MailAttachmentView({ att }: { att: MailAttachment }) { - const getUrl = useDownloadUrl(); - const [url, setUrl] = useState(null); - useEffect(() => { - let alive = true; - getUrl(att.contentRef, att.mimeType).then((u) => { if (alive) setUrl(u); }).catch(() => {}); - return () => { alive = false; }; - }, [att.contentRef, att.mimeType, getUrl]); - - const label = att.filename || "Attachment"; - if (isImage(att.mimeType)) { - return url - ? {label} - :
Loading image…
; - } - return ( - - - {label} - {att.sizeBytes > 0 && {fmtBytes(att.sizeBytes)}} - - ); -} - -/** A small staged-file chip shown in a composer before send, with a remove button. */ -function StagedChip({ file, onRemove }: { file: UploadedAttachment; onRemove: () => void }) { - return ( -
- - {file.filename} - {fmtBytes(file.sizeBytes)} - -
- ); -} - -/** Reader + reply for one mail thread. Used in the Inbox detail pane when an item has a threadId. */ -export function MailReader({ threadId, subject, onError }: { threadId: string; subject: string; onError: (m: string) => void }) { - const t = useMailThread(threadId); - const upload = useUploadAttachment(); - const [draft, setDraft] = useState(""); - const [sending, setSending] = useState(false); - const [staged, setStaged] = useState(null); - const [uploading, setUploading] = useState(false); - const fileRef = useRef(null); - - async function onPickFile(e: React.ChangeEvent) { - const file = e.target.files?.[0]; - e.target.value = ""; - if (!file) return; - setUploading(true); - try { setStaged(await upload(file)); } - catch (err) { onError((err as Error).message); } - finally { setUploading(false); } - } - - async function reply() { - const text = draft.trim(); - if ((!text && !staged) || sending) return; - const att = staged ?? undefined; - setDraft(""); setStaged(null); setSending(true); - try { await t.reply(text, att); } - catch (e) { setDraft(text); setStaged(att ?? null); onError((e as Error).message); } - finally { setSending(false); } - } - - return ( -
-
-
{subject || "(no subject)"}
-
-
- {t.loading && t.messages.length === 0 &&
Loading…
} - {!t.loading && t.messages.length === 0 &&
No messages.
} - {t.messages.map((m) => ( -
-
- {m.kind === "EMAIL" ? "Email" : "Reply"}{m.actorId ? ` · ${m.actorId.replace(/^(pp_|cust_)/, "").slice(0, 8)}` : ""} - {timeOf(m.occurredAt)} -
- {m.html - ?