// ============================================================================ // public/screens/inbox.jsx — Unified Inbox (Phase 6B) // ============================================================================ function InboxScreen() { const [threads, setThreads] = React.useState(null); const [pagination, setPag] = React.useState({}); const [stats, setStats] = React.useState(null); const [channel, setChannel] = React.useState("all"); const [status, setStatus] = React.useState("open"); const [search, setSearch] = React.useState(""); const [activeThread, setActiveThread] = React.useState(null); const [messages, setMessages] = React.useState([]); const [reply, setReply] = React.useState(""); const load = React.useCallback(async (page = 1) => { const p = new URLSearchParams({ page, limit: 25, channel, status }); if (search) p.set("search", search); const r = await VoaisAPI.get("/api/inbox?" + p.toString()); if (r.ok && r.data?.ok) { setThreads(r.data.threads); setPag(r.data.pagination); } }, [channel, status, search]); const loadStats = React.useCallback(async () => { const r = await VoaisAPI.get("/api/inbox/stats"); if (r.ok && r.data?.ok) setStats(r.data); }, []); React.useEffect(() => { load(); loadStats(); }, [load, loadStats]); const openThread = async (id) => { const r = await VoaisAPI.get("/api/inbox/" + id); if (r.ok && r.data?.ok) { setActiveThread(r.data.thread); setMessages(r.data.messages); loadStats(); } }; const sendReply = async () => { if (!reply.trim() || !activeThread) return; await VoaisAPI.post("/api/inbox/" + activeThread.id + "/reply", { content: reply }); setReply(""); openThread(activeThread.id); }; const updateThread = async (id, fields) => { await VoaisAPI.patch("/api/inbox/" + id, fields); if (activeThread?.id === id) openThread(id); load(); }; if (threads === null) return ; const channelIcon = { call: I.phone, whatsapp: I.whatsapp, email: I.email, sms: I.message, web: I.globe }; return (
{/* Left: thread list */}
{/* Channel tabs */}
{["all", "call", "whatsapp", "email"].map(ch => ( ))}
{/* Search */}
setSearch(e.target.value)} onKeyDown={e => { if (e.key === "Enter") load(); }} style={{ paddingLeft: 32, fontSize: 12 }}/>
{/* Thread list */}
{threads.map(t => { const ChIcon = channelIcon[t.channel] || I.message; const isActive = activeThread?.id === t.id; return (
openThread(t.id)} style={{ padding: "12px 14px", cursor: "pointer", borderBottom: "1px solid var(--line)", background: isActive ? "var(--accent-soft)" : t.unread_count > 0 ? "var(--surface-2)" : "transparent", }}>
0 ? 600 : 400, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}> {t.display_name || t.phone || t.email || "Unknown"} {t.unread_count > 0 && {t.unread_count}} {t.pinned ? : null}
{t.last_message_preview || "No messages"}
{t.intent && t.intent !== "unset" && {t.intent}} {t.last_message_at ? timeAgoShort(t.last_message_at) : ""}
); })} {threads.length === 0 &&
No conversations found.
}
{/* Right: thread detail / empty state */}
{!activeThread ? (
Select a conversation
Pick a thread from the left to view messages.
) : ( <> {/* Thread header */}
{activeThread.display_name || activeThread.phone || "Unknown"}
{activeThread.phone || ""} · {activeThread.channel}
updateThread(activeThread.id, { pinned: activeThread.pinned ? 0 : 1 })}> {activeThread.pinned ? "Unpin" : "Pin"} updateThread(activeThread.id, { status: "closed" })}> Close
{/* Messages */}
{messages.map((m, i) => (
{m.from_role === "contact" || m.from_role === "caller" ? (activeThread.display_name || "Contact") : m.from_role === "ai" ? "AI" : "You"}
{m.content}
{m.created_at ? new Date(m.created_at).toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit" }) : ""} {m.status && m.direction === "outbound" && {m.status === "read" ? "✓✓" : m.status === "delivered" ? "✓✓" : m.status === "sent" ? "✓" : ""}}
))} {messages.length === 0 &&
No messages in this thread.
}
{/* Reply box */}
setReply(e.target.value)} onKeyDown={e => { if (e.key === "Enter") sendReply(); }}/> } onClick={sendReply} disabled={!reply.trim()}>Send
)}
); } function timeAgoShort(d) { const s = Math.floor((Date.now()-new Date(d).getTime())/1000); if(s<60) return "now"; if(s<3600) return Math.floor(s/60)+"m"; if(s<86400) return Math.floor(s/3600)+"h"; return Math.floor(s/86400)+"d"; } window.InboxScreen = InboxScreen;