'use client'; import { useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Loader2, Upload } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { apiFetch } from '@/lib/api/client'; import { toastError } from '@/lib/api/toast-error'; import { formatBerthRange } from '@/lib/templates/berth-range'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; interface Props { open: boolean; onOpenChange: (next: boolean) => void; interestId: string; onSuccess?: () => void; } export function ExternalEoiUploadDialog({ open, onOpenChange, interestId, onSuccess }: Props) { const qc = useQueryClient(); const [file, setFile] = useState(null); const [title, setTitle] = useState(''); const [signedAt, setSignedAt] = useState(() => new Date().toISOString().slice(0, 10)); const [signerNames, setSignerNames] = useState(''); const [notes, setNotes] = useState(''); // Fetched on open to power the default title: // "External EOI — — YYYY-MM-DD". Without // this the file lands as just "External EOI - " which is // unscannable in any list when a port has multiple deals closing on // the same day. const { data: interestData } = useQuery<{ data: { clientName: string | null } }>({ queryKey: ['interests', interestId], queryFn: () => apiFetch<{ data: { clientName: string | null } }>(`/api/v1/interests/${interestId}`), enabled: open, staleTime: 60_000, }); const { data: berthsData } = useQuery<{ data: Array<{ mooringNumber: string | null }> }>({ queryKey: ['interests', interestId, 'berths'], queryFn: () => apiFetch<{ data: Array<{ mooringNumber: string | null }> }>( `/api/v1/interests/${interestId}/berths`, ), enabled: open, staleTime: 60_000, }); const defaultTitle = useMemo(() => { const date = signedAt || new Date().toISOString().slice(0, 10); const moorings = (berthsData?.data ?? []) .map((b) => b.mooringNumber) .filter((m): m is string => !!m); const berthLabel = moorings.length > 0 ? formatBerthRange(moorings) : null; const clientName = interestData?.data?.clientName ?? null; const parts = ['External EOI']; if (clientName) parts.push(clientName); if (berthLabel) parts.push(berthLabel); parts.push(date); return parts.join(' — '); }, [interestData, berthsData, signedAt]); const mutation = useMutation<{ data?: { stageChanged?: boolean } }, Error, void>({ mutationFn: async () => { if (!file) throw new Error('No file selected'); const form = new FormData(); form.append('file', file); const effectiveTitle = title.trim() || defaultTitle; if (effectiveTitle) form.append('title', effectiveTitle); if (signedAt) form.append('signedAt', signedAt); if (signerNames) form.append('signerNames', signerNames); if (notes) form.append('notes', notes); const res = await fetch(`/api/v1/interests/${interestId}/external-eoi`, { method: 'POST', body: form, credentials: 'include', }); if (!res.ok) { const err = (await res.json().catch(() => ({ error: 'Upload failed' }))) as { error?: string; }; throw new Error(err.error ?? 'Upload failed'); } return (await res.json()) as { data?: { stageChanged?: boolean } }; }, onSuccess: (response) => { const stageChanged = response?.data?.stageChanged === true; toast.success( stageChanged ? 'External EOI uploaded. Stage advanced to EOI Signed.' : 'External EOI uploaded. Filed against this deal (stage unchanged).', ); qc.invalidateQueries({ queryKey: ['interests', interestId] }); qc.invalidateQueries({ queryKey: ['interests'] }); qc.invalidateQueries({ queryKey: ['documents'] }); setFile(null); setTitle(''); setSignerNames(''); setNotes(''); onOpenChange(false); onSuccess?.(); }, onError: (err: unknown) => { toastError(err, 'Upload failed'); }, }); return ( Upload externally-signed EOI For EOIs signed outside our signing service (paper, in person, alternate e-sign vendor). The uploaded PDF is filed against this interest and the pipeline stage is advanced to EOI Signed.
setFile(e.target.files?.[0] ?? null)} className="mt-1" />
setTitle(e.target.value)} placeholder={defaultTitle} className="mt-1" />

Leave blank to use the default shown above.

setSignedAt(e.target.value)} className="mt-1" />
setSignerNames(e.target.value)} placeholder="e.g. John Smith, Marina Director" className="mt-1" />

Recorded in the audit trail alongside the document.