"use client"; // Inbox data layer. The inbox is a personalized work/awareness feed IIOS projects from events // (NEEDS_REPLY, MENTION, …). The CRM lists it and transitions item state; items are never created // here. Mock when the Shell isn't configured; live via the be-crm data door (crm.inbox.*) otherwise. // // Live contract (be-crm): // query crm.inbox.list { state? } -> InboxItem[] // cmd crm.inbox.transition { id, state, reason? } -> InboxItem import { useCallback, useEffect, useMemo, useState } from "react"; import { useAppShell, useQuery } from "@abe-kap/appshell-sdk/react"; import { isShellConfigured } from "./appshell"; import type { MailThread } from "./mail-api"; export type InboxState = "OPEN" | "SNOOZED" | "DONE" | "ARCHIVED" | "CANCELLED" | "STALE"; export interface UiInboxItem { id: string; kind: string; state: InboxState; title: string; summary?: string; priority: string; threadId?: string; createdAt: string; } const SHELL = isShellConfigured(); export interface InboxData { live: boolean; loading: boolean; error: string | null; items: UiInboxItem[]; transition: (id: string, state: InboxState) => Promise; refetch: () => void; } export function useInboxData(state?: InboxState): InboxData { return SHELL ? useLiveInbox(state) : useMockInbox(state); } function useLiveInbox(state?: InboxState): InboxData { const { sdk } = useAppShell(); const q = useQuery("crm.inbox.list", state ? { state } : {}); // Mail lives in crm-mail threads, NOT the inbox projection — fold it into the one unified // surface. Mail has no inbox work-item state, so it only shows in the Open (or unfiltered) view. const showMail = !state || state === "OPEN"; const mq = useQuery("crm.mail.list", {}); // The SDK's useQuery only refetches when the ACTION changes, not the variables — so a filter // change (same action, new { state }) wouldn't reload. Force a refetch when the filter changes. const refetchInbox = q.refetch; useEffect(() => { refetchInbox(); }, [state, refetchInbox]); const items = useMemo(() => { const inboxItems = q.data ?? []; const mailItems: UiInboxItem[] = showMail ? (mq.data ?? []).map((t) => ({ id: `mail:${t.threadId}`, kind: "MAIL", state: "OPEN" as InboxState, title: t.subject || "(no subject)", ...(t.lastMessage ? { summary: t.lastMessage } : {}), priority: t.unread > 0 ? "HIGH" : "LOW", threadId: t.threadId, createdAt: t.lastAt ?? "", })) : []; // Newest first; mail and inbox items interleave by time. return [...mailItems, ...inboxItems].sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? "")); }, [q.data, mq.data, showMail]); const transition = useCallback(async (id: string, next: InboxState) => { await sdk.command("crm.inbox.transition", { id, state: next }); q.refetch(); }, [sdk, q]); return { live: true, loading: q.loading || (showMail && mq.loading), // Don't let a mail-list hiccup blank the whole inbox — surface only the inbox error. error: q.error?.message ?? null, items, transition, refetch: () => { q.refetch(); mq.refetch(); }, }; } const MOCK_ITEMS: UiInboxItem[] = [ { id: "in_1", kind: "MENTION", state: "OPEN", title: "Sofia mentioned you", summary: "@you — can you confirm the Henderson scope?", priority: "HIGH", threadId: "th_mock_1", createdAt: new Date().toISOString() }, { id: "in_2", kind: "NEEDS_REPLY", state: "OPEN", title: "Reply needed — Storm response", summary: "Dan: Crew is rolling out at 7.", priority: "MEDIUM", threadId: "th_mock_2", createdAt: new Date().toISOString() }, { id: "in_3", kind: "SUPPORT_UPDATE", state: "OPEN", title: "Ticket TK-204 updated", summary: "Customer replied on the roof-leak case.", priority: "LOW", createdAt: new Date().toISOString() }, ]; function useMockInbox(state?: InboxState): InboxData { const [items, setItems] = useState(MOCK_ITEMS); const filtered = useMemo(() => (state ? items.filter((i) => i.state === state) : items), [items, state]); const transition = useCallback(async (id: string, next: InboxState) => { setItems((l) => l.map((i) => (i.id === id ? { ...i, state: next } : i))); }, []); return { live: false, loading: false, error: null, items: filtered, transition, refetch: () => {} }; }