mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 13:38:20 +00:00
add Excel import functionality for container bookings
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
Button,
|
||||
Center,
|
||||
Divider,
|
||||
FileButton,
|
||||
Group,
|
||||
Loader,
|
||||
Modal,
|
||||
@@ -31,7 +32,9 @@ import {
|
||||
CalendarDays,
|
||||
CheckCircle2,
|
||||
ChevronLeft,
|
||||
FileDown,
|
||||
FileText,
|
||||
FileUp,
|
||||
MapPin,
|
||||
Package,
|
||||
Receipt,
|
||||
@@ -54,6 +57,10 @@ import {
|
||||
type GlShipmentQuantities,
|
||||
} from "./gl-booking-form/total";
|
||||
import { ContractCapacityNotice } from "./gl-booking-form/ContractCapacityNotice";
|
||||
import {
|
||||
downloadContainerImportTemplate,
|
||||
parseContainerExcel,
|
||||
} from "./gl-booking-form/container-excel";
|
||||
import {
|
||||
fieldStyles,
|
||||
StepCard,
|
||||
@@ -392,6 +399,57 @@ export default function GlCreateBookingForm() {
|
||||
// bulk needs a positive quantity with hazardous/reefer portions bounded by it.
|
||||
const [showErrors, setShowErrors] = useState(false);
|
||||
|
||||
// Excel import: one row per container. All-or-nothing — a file with any bad
|
||||
// row is rejected with row-numbered errors so nothing is silently dropped.
|
||||
const [importErrors, setImportErrors] = useState<string[]>([]);
|
||||
const [importSummary, setImportSummary] = useState<string | null>(null);
|
||||
const importResetRef = useRef<(() => void) | null>(null);
|
||||
const excelOpts = {
|
||||
allowedSizes: containerSizes,
|
||||
includeHazardous: contract?.isHazardous ?? false,
|
||||
includeReefer: contract?.isReefer ?? false,
|
||||
};
|
||||
|
||||
const handleImportFile = async (file: File | null) => {
|
||||
// Reset the hidden input so re-picking the same (fixed) file re-fires.
|
||||
importResetRef.current?.();
|
||||
if (!file) return;
|
||||
const { rows, errors } = await parseContainerExcel(file, excelOpts);
|
||||
if (errors.length > 0) {
|
||||
setImportSummary(null);
|
||||
setImportErrors(errors);
|
||||
return;
|
||||
}
|
||||
// Replace only the lines for sizes present in the file; a contracted size
|
||||
// the file omits keeps whatever was already entered for it.
|
||||
setContainerLines((prev) =>
|
||||
containerSizes.map((size) => {
|
||||
const imported = rows.filter((r) => r.containerSize === size);
|
||||
if (imported.length === 0) {
|
||||
return (
|
||||
prev.find((l) => l.containerSize === size) ?? {
|
||||
containerSize: size,
|
||||
units: [emptyUnit()],
|
||||
}
|
||||
);
|
||||
}
|
||||
return {
|
||||
containerSize: size,
|
||||
units: imported.map((r) => ({
|
||||
containerNumber: r.containerNumber,
|
||||
sealNumber: r.sealNumber,
|
||||
vgmTons: r.vgmTons,
|
||||
hazardous: r.hazardous,
|
||||
reefer: r.reefer,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
);
|
||||
setImportErrors([]);
|
||||
setShowErrors(false);
|
||||
setImportSummary(`Imported ${rows.length} container(s) from ${file.name}.`);
|
||||
};
|
||||
|
||||
const unitErrors = useMemo<UnitErrors[][]>(() => {
|
||||
if (!isContainer) return [];
|
||||
const numberCounts = new Map<string, number>();
|
||||
@@ -740,6 +798,83 @@ export default function GlCreateBookingForm() {
|
||||
description="Enter the quantity and per-container details for each size in the contract scope."
|
||||
/>
|
||||
<Stack gap={18}>
|
||||
{containerSizes.length > 0 && (
|
||||
<Paper withBorder radius="md" p="md" style={{ borderColor: "#E6ECF2" }}>
|
||||
<Group justify="space-between" wrap="wrap" gap="sm">
|
||||
<Box>
|
||||
<Text fz={13} fw={600}>
|
||||
Import containers from Excel
|
||||
</Text>
|
||||
<Text fz={12} c="dimmed">
|
||||
One row per container. Importing fills the lines below
|
||||
for the sizes in the file.
|
||||
</Text>
|
||||
</Box>
|
||||
<Group gap="sm">
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
radius="md"
|
||||
leftSection={<FileDown size={14} />}
|
||||
onClick={() => downloadContainerImportTemplate(excelOpts)}
|
||||
>
|
||||
Download template
|
||||
</Button>
|
||||
<FileButton
|
||||
resetRef={importResetRef}
|
||||
accept=".xlsx,.xls"
|
||||
onChange={handleImportFile}
|
||||
>
|
||||
{(props) => (
|
||||
<Button
|
||||
{...props}
|
||||
size="xs"
|
||||
radius="md"
|
||||
color="edr-green"
|
||||
leftSection={<FileUp size={14} />}
|
||||
>
|
||||
Import Excel
|
||||
</Button>
|
||||
)}
|
||||
</FileButton>
|
||||
</Group>
|
||||
</Group>
|
||||
{importErrors.length > 0 && (
|
||||
<Alert
|
||||
color="red"
|
||||
variant="light"
|
||||
radius="md"
|
||||
icon={<AlertCircle size={16} />}
|
||||
title="Import failed — fix the file and try again"
|
||||
mt="sm"
|
||||
>
|
||||
<Stack gap={4}>
|
||||
{importErrors.slice(0, 8).map((msg, i) => (
|
||||
<Text key={i} fz="xs">
|
||||
{msg}
|
||||
</Text>
|
||||
))}
|
||||
{importErrors.length > 8 && (
|
||||
<Text fz="xs" c="dimmed">
|
||||
…and {importErrors.length - 8} more.
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Alert>
|
||||
)}
|
||||
{importSummary && (
|
||||
<Alert
|
||||
color="edr-green"
|
||||
variant="light"
|
||||
radius="md"
|
||||
icon={<CheckCircle2 size={16} />}
|
||||
mt="sm"
|
||||
>
|
||||
<Text fz="xs">{importSummary}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
</Paper>
|
||||
)}
|
||||
<ContractCapacityNotice contractId={contract.id} isContainer />
|
||||
{containerLines.length === 0 ? (
|
||||
<Text fz="sm" c="dimmed">
|
||||
|
||||
Reference in New Issue
Block a user