fix(uat-batch-2): external-EOI five-bug bundle (a/b/c/d) + presign filename override
Tackles the linked B4 #5 findings on the external-EOI flow. Item (e) [Edit metadata affordance per row] is deferred to a later wave so it can share infra with the broader signing-flow rework. - (a) lying toast: uploadExternallySignedEoi now returns { stageChanged, newStage }. Client toasts conditionally so a Reservation+ deal that uploads paper-signing evidence no longer claims the stage advanced. - (b) View downloads instead of previewing: SignedPdfActions takes an onView callback; InterestEoiTab lifts a single FilePreviewDialog and passes the callback down. Click-View opens the in-app preview rather than the presigned URL (which the storage backend served as attachment). - (c) UUID filename on download: getDownloadUrl now passes the canonical filename through presignDownloadUrl; S3 backend adds a response-content-disposition override (filename + UTF-8 filename*) to the presign. Filesystem backend already passed it through. - (d) Discarded dateEoiSigned: external-eoi service splits document- metadata writes (always — dateEoiSigned, eoiStatus='signed') from stage advance (gated on past-EOI). Also fires evaluateRule('eoi_signed') so berth-rules stay in sync when an EOI is filed manually. - Default title for external-EOI dialog now derives "External EOI — <Client> — <berth range> — <date>" via the existing formatBerthRange helper; rep can override. tsc clean. 1419/1419 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Loader2, Upload } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -9,7 +9,9 @@ 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,
|
||||
@@ -34,12 +36,49 @@ export function ExternalEoiUploadDialog({ open, onOpenChange, interestId, onSucc
|
||||
const [signerNames, setSignerNames] = useState('');
|
||||
const [notes, setNotes] = useState('');
|
||||
|
||||
const mutation = useMutation({
|
||||
// Fetched on open to power the default title:
|
||||
// "External EOI — <Client> — <berth range> — YYYY-MM-DD". Without
|
||||
// this the file lands as just "External EOI - <date>" 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);
|
||||
if (title) form.append('title', title);
|
||||
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);
|
||||
@@ -54,10 +93,15 @@ export function ExternalEoiUploadDialog({ open, onOpenChange, interestId, onSucc
|
||||
};
|
||||
throw new Error(err.error ?? 'Upload failed');
|
||||
}
|
||||
return res.json();
|
||||
return (await res.json()) as { data?: { stageChanged?: boolean } };
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('External EOI uploaded — interest advanced to EOI Signed');
|
||||
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'] });
|
||||
@@ -100,9 +144,12 @@ export function ExternalEoiUploadDialog({ open, onOpenChange, interestId, onSucc
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Defaults to 'External EOI - <date>'"
|
||||
placeholder={defaultTitle}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Leave blank to use the default shown above.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Date signed</Label>
|
||||
|
||||
Reference in New Issue
Block a user