'use client'; import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { CheckCircle2, ClipboardCopy, Clock, Mail, Send } from 'lucide-react'; import { formatDistanceToNowStrict } from 'date-fns'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { apiFetch } from '@/lib/api/client'; interface Props { interestId: string; /** Hide the button when EOI has already been sent / signed — at that * point the supplemental step is past its window. Caller passes the * current eoiStatus so we can render contextually. */ eoiStatus?: string | null; } interface IssueResponse { data: { link: string; expiresAt: string; emailSent: boolean; resent?: boolean; }; } interface TokenHistoryRow { id: string; token: string; createdAt: string; expiresAt: string; consumedAt: string | null; issuedBy: string | null; expired: boolean; } /** * One-click "Request more info" action. Fires the supplemental-info- * request endpoint, which emails the client a public form pre-filled * with what's on file. On success we display the generated link + a * copy-to-clipboard button in case the rep needs to share it through * another channel. * * Hidden once the EOI is `signed` — the supplemental step only makes * sense before the signed EOI freezes the data into the contract path. */ export function SupplementalInfoRequestButton({ interestId, eoiStatus }: Props) { const qc = useQueryClient(); const [link, setLink] = useState(null); // History query — the latest 20 issuances. Refetched after every // mutation so the rep sees the just-generated row appear immediately. const history = useQuery({ queryKey: ['supplemental-info', interestId, 'history'], queryFn: () => apiFetch<{ data: TokenHistoryRow[] }>( `/api/v1/interests/${interestId}/supplemental-info-request`, ), enabled: eoiStatus !== 'signed', staleTime: 30_000, }); const mutation = useMutation({ mutationFn: (vars: { sendEmail: boolean; tokenId?: string }) => apiFetch(`/api/v1/interests/${interestId}/supplemental-info-request`, { method: 'POST', body: vars, }), onSuccess: (res) => { setLink(res.data.link); if (res.data.resent) { toast.success('Email re-sent using the existing link.'); } else if (res.data.emailSent) { toast.success('Email sent. Link also shown below for sharing manually.'); } else { toast.message( 'Link generated. Click "Send by email" to mail it, or copy it to share manually.', ); } void qc.invalidateQueries({ queryKey: ['supplemental-info', interestId, 'history'] }); }, onError: (err) => toast.error(err instanceof Error ? err.message : 'Failed to generate the form link.'), }); if (eoiStatus === 'signed') return null; // Pick the latest unconsumed + unexpired token, if any. That's the // candidate for "Resend" — the rep wants the same link to land in the // client's inbox again. Older or consumed tokens stay in history but // can't be resent (consumed = form already submitted; expired = link // dead). const tokens = history.data?.data ?? []; const resendableToken = tokens.find((t) => !t.consumedAt && !t.expired) ?? null; return ( {/* shadcn's default CardContent ships with `pt-0 sm:pt-0` because it assumes a CardHeader sits above. This card is intentionally header-less, so we restore symmetric padding (`pt-` matches `p-`) at both base and `sm:` breakpoints. */}

Need more info before drafting the EOI?

Email the client a one-time link to a public form pre-filled with what we have on file. Submissions auto-update this client + interest record.

{resendableToken ? ( ) : null} {link ? ( <> ) : null}
{/* Issuance history — every past supplemental link for this interest, newest first. Lets the rep see whether a previous link is still outstanding (so they can Resend rather than mint a fresh one) and confirm whether the client ever submitted. Hidden when the list is empty. */} {tokens.length > 0 ? (
Issuance history
    {tokens.map((t) => { const created = new Date(t.createdAt); const status = t.consumedAt ? { label: 'Submitted', tone: 'text-emerald-700', icon: CheckCircle2 } : t.expired ? { label: 'Expired', tone: 'text-muted-foreground', icon: Clock } : { label: 'Active', tone: 'text-amber-700', icon: Clock }; const StatusIcon = status.icon; return (
  • {status.label} {formatDistanceToNowStrict(created, { addSuffix: true })}
  • ); })}
) : null}
); }