feat: implement PDF regeneration for contracts and enhance document upload handling in clearance sections

This commit is contained in:
Marshal
2026-06-28 20:12:45 +00:00
parent 1b37914201
commit fc1adce909
5 changed files with 362 additions and 269 deletions

View File

@@ -68,7 +68,7 @@ export function ClearanceReviewSection({
const qc = useQueryClient();
const [queryNotes, setQueryNotes] = useState<Record<string, string>>({});
const [openQuery, setOpenQuery] = useState<Record<string, boolean>>({});
const [outputFiles, setOutputFiles] = useState<Record<string, File>>({});
const [uploadingKey, setUploadingKey] = useState<string | null>(null);
const { view, viewer } = useFileViewer();
const { data: clearance, isLoading } = useQuery({
@@ -100,13 +100,17 @@ export function ClearanceReviewSection({
});
const outputMutation = useMutation({
mutationFn: () => bookingsService.uploadClearanceOutput(bookingId, outputFiles),
mutationFn: (files: Record<string, File>) =>
bookingsService.uploadClearanceOutput(bookingId, files),
onSuccess: () => {
toast.success("Output documents uploaded");
setOutputFiles({});
toast.success("Document uploaded");
setUploadingKey(null);
refresh();
},
onError: () => toast.error("Upload failed"),
onError: () => {
toast.error("Upload failed");
setUploadingKey(null);
},
});
const finalizeMutation = useMutation({
@@ -223,99 +227,100 @@ export function ClearanceReviewSection({
<SectionCard
icon={Upload}
title="Customs output documents"
subtitle="Upload the cleared/customs paperwork to hand back to the customer."
subtitle="Upload each document individually — changes save immediately."
accent="edr-green"
>
<Stack gap={10}>
{glDocs.map((doc) => (
<Group key={doc.fileKey} justify="space-between" wrap="nowrap">
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
<FileText size={16} color="var(--mantine-color-edr-green-6)" />
<Text fz="13px" c="edr-text" truncate>
{doc.label}
{doc.required ? " *" : ""}
</Text>
</Group>
<Group gap={8} wrap="nowrap">
{doc.file ? (
<>
{isViewable({
name: doc.file.name,
url: fileViewUrl(doc.file.id),
}) && (
<Tooltip label="View">
{glDocs.map((doc) => {
const isUploading =
uploadingKey === doc.fileKey && outputMutation.isPending;
return (
<Group key={doc.fileKey} justify="space-between" wrap="nowrap">
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
<FileText size={16} color="var(--mantine-color-edr-green-6)" />
<Text fz="13px" c="edr-text" truncate>
{doc.label}
{doc.required ? " *" : ""}
</Text>
</Group>
<Group gap={8} wrap="nowrap">
{doc.file ? (
<>
{isViewable({
name: doc.file.name,
url: fileViewUrl(doc.file.id),
}) && (
<Tooltip label="View">
<Box
component="button"
type="button"
onClick={() =>
view({
name: doc.file!.name,
url: fileViewUrl(doc.file!.id),
})
}
c="edr-green"
style={{
display: "flex",
background: "transparent",
border: "none",
cursor: "pointer",
}}
>
<Eye size={15} />
</Box>
</Tooltip>
)}
<Tooltip label="Download">
<Box
component="button"
type="button"
onClick={() =>
view({
name: doc.file!.name,
url: fileViewUrl(doc.file!.id),
})
}
component="a"
href={fileViewUrl(doc.file.id, true)}
c="edr-green"
style={{
display: "flex",
background: "transparent",
border: "none",
cursor: "pointer",
}}
style={{ display: "flex" }}
>
<Eye size={15} />
<Download size={15} />
</Box>
</Tooltip>
)}
<Tooltip label="Download">
<Box
component="a"
href={fileViewUrl(doc.file.id, true)}
c="edr-green"
style={{ display: "flex" }}
>
<Download size={15} />
</Box>
</Tooltip>
</>
) : (
<Text fz="12px" c="edr-muted">
Not uploaded
</Text>
)}
<FileButton
onChange={(f) =>
f && setOutputFiles((o) => ({ ...o, [doc.fileKey]: f }))
}
accept="application/pdf,image/*"
>
{(props) => (
<Button
{...props}
size="compact-xs"
variant="light"
color="edr-green"
leftSection={<Upload size={13} />}
>
{outputFiles[doc.fileKey] ? "Selected" : "Upload"}
</Button>
</>
) : (
<Text fz="12px" c="edr-muted">
Not uploaded
</Text>
)}
</FileButton>
<FileButton
onChange={(f) => {
if (!f) return;
setUploadingKey(doc.fileKey);
outputMutation.mutate({ [doc.fileKey]: f });
}}
accept="application/pdf,image/*"
disabled={isUploading}
>
{(props) => (
<Button
{...props}
size="compact-xs"
variant="light"
color="edr-green"
leftSection={
isUploading ? (
<Loader size={12} color="edr-green" />
) : (
<Upload size={13} />
)
}
loading={isUploading}
>
{doc.file ? "Replace" : "Upload"}
</Button>
)}
</FileButton>
</Group>
</Group>
</Group>
))}
);
})}
</Stack>
<Group justify="flex-end" mt="md">
<Button
variant="light"
color="edr-green"
radius="md"
leftSection={<Upload size={15} />}
disabled={Object.keys(outputFiles).length === 0}
loading={outputMutation.isPending}
onClick={() => outputMutation.mutate()}
>
Upload output documents
</Button>
</Group>
</SectionCard>
)}

View File

@@ -92,7 +92,7 @@ export function ContractClearanceReviewSection({
}: ContractClearanceReviewSectionProps) {
const [queryNotes, setQueryNotes] = useState<Record<string, string>>({});
const [openQuery, setOpenQuery] = useState<Record<string, boolean>>({});
const [outputFiles, setOutputFiles] = useState<Record<string, File>>({});
const [uploadingKey, setUploadingKey] = useState<string | null>(null);
const { view, viewer } = useFileViewer();
const reviewerTeam = selfClear ? "Operations" : "Global Logistics";
@@ -242,110 +242,106 @@ export function ContractClearanceReviewSection({
<SectionCard
icon={Upload}
title="GL output documents"
subtitle="Upload IM4/IM5/EX3/EX8/T1 and other cleared paperwork."
subtitle="Upload each document individually — changes save immediately."
accent="edr-green"
>
<Stack gap={10}>
{glDocs.map((doc) => (
<Group key={doc.fileKey} justify="space-between" wrap="nowrap">
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
<FileText size={16} color="var(--mantine-color-edr-green-6)" />
<Text fz="13px" c="edr-text" truncate>
{doc.label}
{doc.required ? " *" : ""}
</Text>
</Group>
<Group gap={8} wrap="nowrap">
{doc.file ? (
<>
{isViewable({
name: doc.file.name,
url: fileViewUrl(doc.file.id),
}) && (
<Tooltip label="View">
{glDocs.map((doc) => {
const isUploading =
uploadingKey === doc.fileKey && uploadOutputDocuments.isPending;
return (
<Group key={doc.fileKey} justify="space-between" wrap="nowrap">
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
<FileText size={16} color="var(--mantine-color-edr-green-6)" />
<Text fz="13px" c="edr-text" truncate>
{doc.label}
{doc.required ? " *" : ""}
</Text>
</Group>
<Group gap={8} wrap="nowrap">
{doc.file ? (
<>
{isViewable({
name: doc.file.name,
url: fileViewUrl(doc.file.id),
}) && (
<Tooltip label="View">
<Box
component="button"
type="button"
onClick={() =>
view({
name: doc.file!.name,
url: fileViewUrl(doc.file!.id),
})
}
c="edr-green"
style={{
display: "flex",
background: "transparent",
border: "none",
cursor: "pointer",
}}
>
<Eye size={15} />
</Box>
</Tooltip>
)}
<Tooltip label="Download">
<Box
component="button"
type="button"
onClick={() =>
view({
name: doc.file!.name,
url: fileViewUrl(doc.file!.id),
})
}
component="a"
href={fileViewUrl(doc.file.id, true)}
c="edr-green"
style={{
display: "flex",
background: "transparent",
border: "none",
cursor: "pointer",
}}
style={{ display: "flex" }}
>
<Eye size={15} />
<Download size={15} />
</Box>
</Tooltip>
)}
<Tooltip label="Download">
<Box
component="a"
href={fileViewUrl(doc.file.id, true)}
c="edr-green"
style={{ display: "flex" }}
>
<Download size={15} />
</Box>
</Tooltip>
</>
) : (
<Text fz="12px" c="edr-muted">
Not uploaded
</Text>
)}
{!readOnly && (
<FileButton
onChange={(f) =>
f && setOutputFiles((o) => ({ ...o, [doc.fileKey]: f }))
}
accept="application/pdf,image/*"
>
{(props) => (
<Button
{...props}
size="compact-xs"
variant="light"
color="edr-green"
leftSection={<Upload size={13} />}
>
{outputFiles[doc.fileKey] ? "Selected" : "Upload"}
</Button>
)}
</FileButton>
)}
</>
) : (
<Text fz="12px" c="edr-muted">
Not uploaded
</Text>
)}
{!readOnly && (
<FileButton
onChange={(f) => {
if (!f) return;
setUploadingKey(doc.fileKey);
uploadOutputDocuments.mutate(
{ [doc.fileKey]: f },
{ onSuccess: () => { setUploadingKey(null); onChanged?.(); },
onError: () => setUploadingKey(null) },
);
}}
accept="application/pdf,image/*"
disabled={isUploading}
>
{(props) => (
<Button
{...props}
size="compact-xs"
variant="light"
color="edr-green"
leftSection={
isUploading ? (
<Loader size={12} color="edr-green" />
) : (
<Upload size={13} />
)
}
loading={isUploading}
>
{doc.file ? "Replace" : "Upload"}
</Button>
)}
</FileButton>
)}
</Group>
</Group>
</Group>
))}
);
})}
</Stack>
{!readOnly && (
<Group justify="flex-end" mt="md">
<Button
variant="light"
color="edr-green"
radius="md"
leftSection={<Upload size={15} />}
disabled={Object.keys(outputFiles).length === 0}
loading={uploadOutputDocuments.isPending}
onClick={() =>
uploadOutputDocuments.mutate(outputFiles, {
onSuccess: () => {
setOutputFiles({});
onChanged?.();
},
})
}
>
Upload output documents
</Button>
</Group>
)}
</SectionCard>
)}