import * as React from "react"; import { useState } from "react"; import { ArrowLeft, Mail, Plus } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "./button"; import { Empty, EmptyContent, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, } from "./empty"; import { ConversationList, ChatThread } from "./ai-conversations"; import type { AiConvContact, AiConvListItemData, AiConvMessage, AiConvEmailPayload, } from "./ai-conversations"; /** * OpportunityEmailNotesTab — WealthX DS (Level 5) * * Content for the "Email & Notes" tab of `OpportunityDetailsDrawer` * (financial-drawers.tsx). Scopes the existing Conversations building * blocks to a single opportunity's applicant(s) so staff can read and * reply to a client's email without leaving the Pipeline board. * * Gmail-style drill-in layout (chosen for the drawer's width constraints): * conversations render as full-width rows (name · subject · snippet · * timestamp); clicking a row swaps the panel to the full-width thread with * a back control, `emailOnly` composer (no Chat/Email toggle, no AI * hand-off), and email reply fields. * * Covers Email only — "Notes" is deferred to a follow-up story per the * BUILD-2313 refinement (see docs/feature-prep/kanban-client-emails.md). * * Pass as the `emailAndNotes` slot of `OpportunityDetailsDrawer`. */ export interface OpportunityEmailNotesTabProps { /** Email conversations tied to this opportunity's applicant(s) (main + co, when joint). */ conversations: AiConvListItemData[]; /** Messages for every conversation above, keyed by conversation id. */ messagesByConversationId: Record; /** When set, opens directly on that thread; otherwise opens on the row list. */ defaultConversationId?: string; /** * This opportunity's applicant(s) (main + co, when joint) — independent of * `conversations` so a broker can start a first email even when none * exist yet. Powers the "New Email" affordance; omit to hide it. */ applicants?: AiConvContact[]; /** Fired when the advisor sends a reply from an existing thread. */ onSendEmail?: (conversationId: string, payload: AiConvEmailPayload) => void; /** Fired when the advisor sends a brand-new email (no prior conversation). */ onComposeEmail?: ( contact: AiConvContact, payload: AiConvEmailPayload, ) => void; className?: string; } const PANEL_CLASS = "flex h-[calc(100vh_-_5.5rem)] min-h-[440px] border border-border"; export function OpportunityEmailNotesTab({ conversations, messagesByConversationId, defaultConversationId, applicants, onSendEmail, onComposeEmail, className, }: OpportunityEmailNotesTabProps) { const [selectedId, setSelectedId] = useState( defaultConversationId ?? null, ); const [pickingRecipient, setPickingRecipient] = useState(false); const [composeContact, setComposeContact] = useState( null, ); const canCompose = Boolean(applicants && applicants.length > 0); const handleComposeStart = () => { if (!applicants || applicants.length === 0) return; if (applicants.length === 1) { setComposeContact(applicants[0]); return; } setPickingRecipient(true); }; if (composeContact) { return (
{ onComposeEmail?.(composeContact, payload); setComposeContact(null); }} onBack={() => setComposeContact(null)} showBackButton className="min-w-0 flex-1" />
); } if (pickingRecipient) { return (
New Email — select recipient
{applicants?.map((contact) => ( ))}
); } if (conversations.length === 0) { return ( No emails to this client yet Email conversations with the applicant(s) on this loan will show up here as soon as one starts. {canCompose && ( )} ); } const selected = selectedId ? conversations.find((c) => c.id === selectedId) : undefined; if (!selected) { return (
); } const messages = messagesByConversationId[selected.id] ?? []; // The reply composer is its own section pinned at the bottom of the // drawer; the email chain scrolls past it. 5.5rem = drawer tab bar + // the drawer's p-5 tab-content padding, so the panel's bottom edge // lands on the drawer's bottom edge. return (
onSendEmail?.(selected.id, payload)} onBack={() => setSelectedId(null)} showBackButton className="min-w-0 flex-1" />
); }