'use client'; /** * Shared send-document dialog (Phase 7). * * Used by: * - {@link SendBerthPdfDialog} (berths/) — single berth, recipient picker. * - {@link SendBrochureDialog} (clients/, interests/) — brochure picker. * - The interest "send from interest" pattern reuses both via a wrapper. * * §14.7 mitigations enforced client-side: * - Recipient email is shown verbatim in the confirm step (no quick-send). * - Pre-send dry-run calls /preview first; the Send button is disabled * until the unresolved-tokens list is empty. * - Body length capped at 50KB; char count visible. */ import { useEffect, useMemo, useState } from 'react'; import { useMutation, useQuery } from '@tanstack/react-query'; import { Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Input } from '@/components/ui/input'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; const BODY_MAX = 50_000; export type DocumentKind = 'berth_pdf' | 'brochure'; interface SendDocumentDialogProps { open: boolean; onOpenChange: (open: boolean) => void; documentKind: DocumentKind; /** Pre-filled recipient. Leave both blank to let the rep type one. */ recipient: { clientId?: string; email?: string; interestId?: string }; /** Either a berthId (for berth_pdf) or brochureId (for brochure). */ context: { berthId?: string; brochureId?: string }; /** Title displayed in the dialog header. */ title: string; /** Short context line under the title (e.g. "Berth A1 — primary version"). */ subtitle?: string; onSent?: () => void; } interface PreviewResponse { data: { html: string; markdown: string; unresolved: string[] }; } export function SendDocumentDialog({ open, onOpenChange, documentKind, recipient, context, title, subtitle, onSent, }: SendDocumentDialogProps) { const [step, setStep] = useState<'compose' | 'confirm'>('compose'); const [emailOverride, setEmailOverride] = useState(recipient.email ?? ''); const [customBody, setCustomBody] = useState(''); useEffect(() => { if (open) { setStep('compose'); setEmailOverride(recipient.email ?? ''); setCustomBody(''); } }, [open, recipient.email]); const recipientForApi = useMemo( () => ({ clientId: recipient.clientId, email: emailOverride || recipient.email, interestId: recipient.interestId, }), [recipient.clientId, recipient.email, recipient.interestId, emailOverride], ); // Live preview via /api/v1/document-sends/preview. Re-runs whenever the // body text or recipient changes (debounce-by-react-query for free). const previewQuery = useQuery({ queryKey: [ 'document-sends-preview', documentKind, context.berthId ?? null, context.brochureId ?? null, recipientForApi.clientId ?? null, recipientForApi.email ?? null, customBody, ], queryFn: () => apiFetch('/api/v1/document-sends/preview', { method: 'POST', body: { documentKind, recipient: recipientForApi, berthId: context.berthId, brochureId: context.brochureId, customBodyMarkdown: customBody.trim() ? customBody : undefined, }, }), enabled: open && Boolean(recipientForApi.clientId || recipientForApi.email), }); type SendResp = { data: { error?: string; deliveredAsAttachment: boolean } }; const sendMutation = useMutation({ mutationFn: async (): Promise => { const endpoint = documentKind === 'berth_pdf' ? '/api/v1/document-sends/berth-pdf' : '/api/v1/document-sends/brochure'; const body = documentKind === 'berth_pdf' ? { berthId: context.berthId, recipient: recipientForApi, customBodyMarkdown: customBody.trim() ? customBody : undefined, } : { brochureId: context.brochureId, recipient: recipientForApi, customBodyMarkdown: customBody.trim() ? customBody : undefined, }; return (await apiFetch(endpoint, { method: 'POST', body })) as SendResp; }, onSuccess: (resp) => { if (resp.data.error) { toast.error(`Send failed: ${resp.data.error}`); } else { toast.success( resp.data.deliveredAsAttachment ? 'Sent as attachment' : 'Sent (large file delivered as download link)', ); onSent?.(); onOpenChange(false); } }, onError: (err) => { toastError(err); }, }); const unresolved = previewQuery.data?.data.unresolved ?? []; const previewHtml = previewQuery.data?.data.html ?? ''; const recipientResolved = Boolean(recipientForApi.clientId || recipientForApi.email); const canPreview = recipientResolved; const canSend = step === 'confirm' && unresolved.length === 0 && !sendMutation.isPending; return ( {title} {subtitle && {subtitle}} {step === 'compose' ? (
setEmailOverride(e.target.value)} placeholder={recipient.email ? '' : 'recipient@example.com'} />

{recipient.clientId ? 'Defaults to the client primary email; override here if needed.' : 'Type the address you want to send to.'}