Files
pn-new-crm/src/components/interests/mark-externally-signed-dialog.tsx

128 lines
4.1 KiB
TypeScript
Raw Normal View History

'use client';
/**
* Confirms a "mark as signed externally without file" action on an
* interest's contract / reservation / EOI sub-status. Used by the
* relevant interest tabs when the rep has paper or a digital copy
* filed elsewhere and doesn't want to duplicate-store it in the CRM
* just to flip the doc-status forward.
*
* The action is destructive in the sense that downstream berth-rules
* fire (eoi_signed / contract_signed triggers), so a confirmation
* step + optional reason capture is required.
*/
import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Loader2 } from 'lucide-react';
import { toast } from 'sonner';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
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';
type DocType = 'eoi' | 'reservation' | 'contract';
const LABELS: Record<DocType, { title: string; description: string }> = {
eoi: {
title: 'Mark EOI as signed externally?',
description:
'Flips the EOI sub-status to "signed" without uploading a file. Audit log captures who marked it + your reason. If a file turns up later, you can still upload it via the regular flow.',
},
reservation: {
title: 'Mark reservation as signed externally?',
description:
'Flips the reservation sub-status to "signed" without uploading a file. Audit log captures who marked it + your reason.',
},
contract: {
title: 'Mark contract as signed externally?',
description:
'Flips the contract sub-status to "signed" without uploading a file. The contract-signed berth-rule fires automatically (you can disable in admin/berth-rules).',
},
};
export function MarkExternallySignedDialog({
open,
onOpenChange,
interestId,
docType,
onSuccess,
}: {
open: boolean;
onOpenChange: (o: boolean) => void;
interestId: string;
docType: DocType;
onSuccess?: () => void;
}) {
const [reason, setReason] = useState('');
const qc = useQueryClient();
const labels = LABELS[docType];
const mutation = useMutation({
mutationFn: async () => {
await apiFetch(`/api/v1/interests/${interestId}/mark-externally-signed`, {
method: 'POST',
body: { docType, reason: reason.trim() || undefined },
});
},
onSuccess: () => {
toast.success(`Marked ${docType} as signed externally`);
qc.invalidateQueries({ queryKey: ['interest', interestId] });
qc.invalidateQueries({ queryKey: ['interests'] });
qc.invalidateQueries({ queryKey: ['documents'] });
onOpenChange(false);
onSuccess?.();
},
onError: (e) => toastError(e),
});
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{labels.title}</AlertDialogTitle>
<AlertDialogDescription>{labels.description}</AlertDialogDescription>
</AlertDialogHeader>
<div className="space-y-2">
<Label htmlFor="mes-reason">Reason (optional)</Label>
<Textarea
id="mes-reason"
rows={3}
placeholder="e.g. paper signed in-office; scan filed in shared drive"
value={reason}
onChange={(e) => setReason(e.target.value.slice(0, 2000))}
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel disabled={mutation.isPending}>Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={mutation.isPending}
onClick={(e) => {
e.preventDefault();
mutation.mutate();
}}
>
{mutation.isPending ? (
<Loader2 className="size-4 animate-spin" aria-hidden />
) : (
'Mark as signed'
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}