mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 10:40:58 +00:00
Merge branch 'contrat-backup2' of github.com:Tria-plc/edr-platform into contrat-backup2
This commit is contained in:
@@ -62,10 +62,11 @@ export function GlClearanceUploadModal({
|
||||
setLoading(true);
|
||||
try {
|
||||
if (isDo) {
|
||||
const iso = vesselDate ? vesselDate.toISOString().slice(0, 10) : undefined;
|
||||
if (isBooking) {
|
||||
await bookingsService.uploadDeliveryOrder(entityId, file);
|
||||
await bookingsService.uploadDeliveryOrder(entityId, file, iso);
|
||||
} else {
|
||||
await contractsService.uploadDeliveryOrder(entityId, file);
|
||||
await contractsService.uploadDeliveryOrder(entityId, file, iso);
|
||||
}
|
||||
toast.success(replaceMode ? "Delivery Order updated" : "Delivery Order uploaded");
|
||||
} else {
|
||||
@@ -117,7 +118,15 @@ export function GlClearanceUploadModal({
|
||||
size="sm"
|
||||
required
|
||||
/>
|
||||
) : null}
|
||||
) : (
|
||||
<DateInput
|
||||
label="Vessel departure date (optional)"
|
||||
value={vesselDate}
|
||||
onChange={(v) => setVesselDate(v ? new Date(v) : null)}
|
||||
size="sm"
|
||||
clearable
|
||||
/>
|
||||
)}
|
||||
|
||||
<PhasedFileDropzone
|
||||
label={isDo ? "Delivery Order file" : "Release Order file"}
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
Badge,
|
||||
Button,
|
||||
Group,
|
||||
Modal,
|
||||
NumberInput,
|
||||
Paper,
|
||||
SegmentedControl,
|
||||
Select,
|
||||
Stack,
|
||||
Stepper,
|
||||
@@ -13,6 +15,7 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { DateTimePicker } from "@mantine/dates";
|
||||
import { PhasedFileDropzone, PhasedMultiFileDropzone } from "@/components/contracts/PhasedFileDropzone";
|
||||
import {
|
||||
TransitPermitMultiUpload,
|
||||
@@ -25,6 +28,7 @@ import {
|
||||
FileText,
|
||||
PackageCheck,
|
||||
Receipt,
|
||||
ShieldAlert,
|
||||
Ship,
|
||||
Truck,
|
||||
Upload,
|
||||
@@ -68,6 +72,10 @@ export type ClearanceViewLike = Pick<
|
||||
| "finalInvoice"
|
||||
| "vesselDepartureDate"
|
||||
| "linkedBookingId"
|
||||
| "riskLevel"
|
||||
| "riskAssignedAt"
|
||||
| "secondDuty"
|
||||
| "importReleaseGranted"
|
||||
> & { operationReady?: boolean };
|
||||
|
||||
export type MilestoneRow = NonNullable<ClearanceViewLike["milestones"]>[number];
|
||||
@@ -91,6 +99,7 @@ export function isBookingMilestoneDone(
|
||||
function computeImportActiveStep(
|
||||
clearance: ClearanceViewLike,
|
||||
bookingCreated: boolean,
|
||||
bookingMilestones: MilestoneRow[],
|
||||
): number {
|
||||
if (!isMilestoneDone(clearance.milestones, "DOCUMENTS_APPROVED")) return 0;
|
||||
if (!isMilestoneDone(clearance.milestones, "DECLARED")) return 1;
|
||||
@@ -111,8 +120,17 @@ function computeImportActiveStep(
|
||||
if (!clearance.preClearanceFinalized) return 5;
|
||||
if (!isMilestoneDone(clearance.milestones, "DO_COLLECTED")) return 6;
|
||||
if (!bookingCreated) return 7;
|
||||
if (!clearance.t1?.closed) return 8;
|
||||
return 9;
|
||||
if (!clearance.gatepassGranted) return 8;
|
||||
if (!clearance.t1?.closed) return 9;
|
||||
if (!isBookingMilestoneDone(bookingMilestones, "RISK_ASSIGNED")) return 10;
|
||||
// Additional duty round is optional — resolved once skipped or paid.
|
||||
const secondDutyResolved =
|
||||
clearance.secondDuty?.skipped ||
|
||||
clearance.secondDuty?.paid ||
|
||||
isBookingMilestoneDone(bookingMilestones, "SECOND_DUTY_PAID");
|
||||
if (!secondDutyResolved) return 11;
|
||||
if (!clearance.importReleaseGranted) return 12;
|
||||
return 13;
|
||||
}
|
||||
|
||||
function t1FilesFromWorkflow(
|
||||
@@ -133,7 +151,9 @@ function declarationFilesFromWorkflow(
|
||||
workflowFiles: Freight.ClearanceWorkflowFile[],
|
||||
): Array<{ code: string; label: string; file: { id: string; name: string } }> {
|
||||
return workflowFiles
|
||||
.filter((f) => f.category === "declaration" && f.file)
|
||||
.filter(
|
||||
(f) => f.category === "declaration" && f.code !== "import_release" && f.file,
|
||||
)
|
||||
.map((f) => ({
|
||||
code: f.code,
|
||||
label: f.label,
|
||||
@@ -205,9 +225,15 @@ export function PhasedClearanceActionPanel({
|
||||
// The server only builds the t1 block once a booking is linked — use it as the
|
||||
// booking-created signal on pages that don't pass bookingCreated (GL DJ detail).
|
||||
const effectiveBookingCreated = bookingCreated || Boolean(clearance.t1);
|
||||
// The booking that carries the post-booking steps (gate pass, risk, duty, release).
|
||||
const actionBookingId =
|
||||
clearance.t1?.bookingId ?? clearance.linkedBookingId ?? bookingId ?? null;
|
||||
const activeStep = useMemo(
|
||||
() => (isImport ? computeImportActiveStep(clearance, effectiveBookingCreated) : 0),
|
||||
[clearance, isImport, effectiveBookingCreated],
|
||||
() =>
|
||||
isImport
|
||||
? computeImportActiveStep(clearance, effectiveBookingCreated, bookingMilestones)
|
||||
: 0,
|
||||
[clearance, isImport, effectiveBookingCreated, bookingMilestones],
|
||||
);
|
||||
|
||||
if (isImport) {
|
||||
@@ -508,6 +534,21 @@ export function PhasedClearanceActionPanel({
|
||||
)}
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
label="Gate pass"
|
||||
description="GL Djibouti grants after wagon allocation"
|
||||
icon={
|
||||
clearance.gatepassGranted ? <CheckCircle2 size={14} /> : <Truck size={14} />
|
||||
}
|
||||
>
|
||||
<ImportGatepassStep
|
||||
bookingId={actionBookingId}
|
||||
clearance={clearance}
|
||||
canAct={showDj && canDj}
|
||||
onChanged={onChanged}
|
||||
/>
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
label="T1 transport documents"
|
||||
description="GL Djibouti uploads after wagon allocation; GL Ethiopia closes on arrival"
|
||||
@@ -525,6 +566,64 @@ export function PhasedClearanceActionPanel({
|
||||
onDownloadFile={onDownloadFile}
|
||||
/>
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
label="Customs risk"
|
||||
description="GL Ethiopia assigns Green / Yellow / Red"
|
||||
icon={
|
||||
isBookingMilestoneDone(bookingMilestones, "RISK_ASSIGNED") ? (
|
||||
<CheckCircle2 size={14} />
|
||||
) : (
|
||||
<ShieldAlert size={14} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<RiskStep
|
||||
bookingId={actionBookingId}
|
||||
clearance={clearance}
|
||||
canAct={showEt && canEt}
|
||||
done={isBookingMilestoneDone(bookingMilestones, "RISK_ASSIGNED")}
|
||||
onChanged={onChanged}
|
||||
/>
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
label="Additional duty & tax"
|
||||
description="GL Ethiopia advises if more duty applies"
|
||||
icon={<Receipt size={14} />}
|
||||
>
|
||||
<SecondDutyStep
|
||||
bookingId={actionBookingId}
|
||||
clearance={clearance}
|
||||
canAct={showEt && canEt}
|
||||
riskAssigned={isBookingMilestoneDone(bookingMilestones, "RISK_ASSIGNED")}
|
||||
onChanged={onChanged}
|
||||
onViewFile={onViewFile}
|
||||
onDownloadFile={onDownloadFile}
|
||||
/>
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
label="Import release"
|
||||
description="GL Ethiopia uploads the release document"
|
||||
icon={
|
||||
clearance.importReleaseGranted ? (
|
||||
<CheckCircle2 size={14} />
|
||||
) : (
|
||||
<FileText size={14} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<ImportReleaseStep
|
||||
bookingId={actionBookingId}
|
||||
clearance={clearance}
|
||||
canAct={showEt && canEt}
|
||||
workflowFiles={workflowFiles}
|
||||
onChanged={onChanged}
|
||||
onViewFile={onViewFile}
|
||||
onDownloadFile={onDownloadFile}
|
||||
/>
|
||||
</Stepper.Step>
|
||||
</Stepper>
|
||||
</Paper>
|
||||
</Stack>
|
||||
@@ -717,6 +816,453 @@ function ImportT1Section({
|
||||
);
|
||||
}
|
||||
|
||||
/** GL DJ grants the import gate pass once wagons are allocated (captures time). */
|
||||
function ImportGatepassStep({
|
||||
bookingId,
|
||||
clearance,
|
||||
canAct,
|
||||
onChanged,
|
||||
}: {
|
||||
bookingId: string | null;
|
||||
clearance: ClearanceViewLike;
|
||||
canAct: boolean;
|
||||
onChanged?: () => void;
|
||||
}) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [at, setAt] = useState<Date | null>(new Date());
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
if (clearance.gatepassGranted) {
|
||||
return (
|
||||
<StepStatus
|
||||
done
|
||||
pendingLabel=""
|
||||
doneLabel={`Gate pass granted${
|
||||
clearance.gatepassAt ? ` · ${new Date(clearance.gatepassAt).toLocaleString()}` : ""
|
||||
}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const wagonAllocated = Boolean(clearance.train?.wagonAllocated);
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<StepStatus
|
||||
done={false}
|
||||
pendingLabel={
|
||||
wagonAllocated
|
||||
? "Wagons allocated — GL Djibouti can grant the gate pass."
|
||||
: "Available once wagons are allocated."
|
||||
}
|
||||
doneLabel=""
|
||||
/>
|
||||
{canAct && bookingId ? (
|
||||
<>
|
||||
<Button
|
||||
color="edr-green"
|
||||
leftSection={<Truck size={16} />}
|
||||
disabled={!wagonAllocated}
|
||||
onClick={() => {
|
||||
setAt(new Date());
|
||||
setOpened(true);
|
||||
}}
|
||||
>
|
||||
Grant gate pass
|
||||
</Button>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title={<Text fw={700}>Grant gate pass</Text>}
|
||||
radius="md"
|
||||
size="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<DateTimePicker
|
||||
label="Gate pass time"
|
||||
value={at}
|
||||
onChange={(v) => setAt(v ? new Date(v) : null)}
|
||||
required
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={() => setOpened(false)} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await contractsService.grantGatepass(
|
||||
bookingId,
|
||||
(at ?? new Date()).toISOString(),
|
||||
);
|
||||
toast.success("Gate pass granted");
|
||||
setOpened(false);
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Grant
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const RISK_LEVEL_COLOR: Record<string, string> = {
|
||||
GREEN: "green",
|
||||
YELLOW: "yellow",
|
||||
RED: "red",
|
||||
};
|
||||
|
||||
/** GL ET assigns the customs examination risk (visible to the customer). */
|
||||
function RiskStep({
|
||||
bookingId,
|
||||
clearance,
|
||||
canAct,
|
||||
done,
|
||||
onChanged,
|
||||
}: {
|
||||
bookingId: string | null;
|
||||
clearance: ClearanceViewLike;
|
||||
canAct: boolean;
|
||||
done: boolean;
|
||||
onChanged?: () => void;
|
||||
}) {
|
||||
const [level, setLevel] = useState<string>("GREEN");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
if (done || clearance.riskLevel) {
|
||||
return (
|
||||
<Group gap="sm">
|
||||
<Badge
|
||||
color={RISK_LEVEL_COLOR[clearance.riskLevel ?? ""] ?? "gray"}
|
||||
variant="filled"
|
||||
radius="sm"
|
||||
>
|
||||
{clearance.riskLevel ?? "Assigned"}
|
||||
</Badge>
|
||||
<Text size="sm" c="dimmed">
|
||||
Customs risk assigned
|
||||
{clearance.riskAssignedAt
|
||||
? ` · ${new Date(clearance.riskAssignedAt).toLocaleString()}`
|
||||
: ""}
|
||||
. The customer can see this level.
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
if (!canAct || !bookingId) {
|
||||
return (
|
||||
<StepStatus
|
||||
done={false}
|
||||
pendingLabel="Waiting for GL Ethiopia to assign the customs risk level."
|
||||
doneLabel=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<SegmentedControl
|
||||
fullWidth
|
||||
value={level}
|
||||
onChange={setLevel}
|
||||
data={[
|
||||
{ label: "Green", value: "GREEN" },
|
||||
{ label: "Yellow", value: "YELLOW" },
|
||||
{ label: "Red", value: "RED" },
|
||||
]}
|
||||
/>
|
||||
<Group justify="space-between">
|
||||
<Text size="xs" c="dimmed">
|
||||
The customer sees the assigned risk level.
|
||||
</Text>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
color="edr-green"
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await contractsService.assignRisk(bookingId, {
|
||||
riskLevel: level as Freight.CustomsRiskLevel,
|
||||
});
|
||||
toast.success("Customs risk assigned");
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Assign risk
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
/** Optional post-arrival additional duty/tax round (GL ET advises; customer pays slip). */
|
||||
function SecondDutyStep({
|
||||
bookingId,
|
||||
clearance,
|
||||
canAct,
|
||||
riskAssigned,
|
||||
onChanged,
|
||||
onViewFile,
|
||||
onDownloadFile,
|
||||
}: {
|
||||
bookingId: string | null;
|
||||
clearance: ClearanceViewLike;
|
||||
canAct: boolean;
|
||||
riskAssigned: boolean;
|
||||
onChanged?: () => void;
|
||||
onViewFile?: (file: { name: string; url: string }) => void;
|
||||
onDownloadFile?: (file: { id: string; name: string }) => void;
|
||||
}) {
|
||||
const [dutyRequired, setDutyRequired] = useState(true);
|
||||
const [amount, setAmount] = useState<number | string>("");
|
||||
const [currency, setCurrency] = useState("ETB");
|
||||
const [serial, setSerial] = useState("");
|
||||
const [attachment, setAttachment] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const duty = clearance.secondDuty ?? null;
|
||||
|
||||
if (duty?.skipped) {
|
||||
return (
|
||||
<StepStatus done pendingLabel="" doneLabel="No additional duty or tax applies." />
|
||||
);
|
||||
}
|
||||
|
||||
if (duty?.advised) {
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Text size="sm">
|
||||
Additional duty advised:{" "}
|
||||
<strong>
|
||||
{duty.amount?.toLocaleString()} {duty.currency}
|
||||
</strong>
|
||||
{duty.declarationSerial ? ` · ${duty.declarationSerial}` : ""}
|
||||
</Text>
|
||||
{duty.noticeFile ? (
|
||||
<PhasedUploadedFileRow
|
||||
label="Additional Duty / Tax Notice"
|
||||
file={duty.noticeFile}
|
||||
onView={onViewFile}
|
||||
onDownload={onDownloadFile}
|
||||
compact
|
||||
/>
|
||||
) : null}
|
||||
{duty.slipFile ? (
|
||||
<PhasedUploadedFileRow
|
||||
label="Customer payment slip"
|
||||
file={duty.slipFile}
|
||||
onView={onViewFile}
|
||||
onDownload={onDownloadFile}
|
||||
compact
|
||||
/>
|
||||
) : null}
|
||||
<StepStatus
|
||||
done={duty.paid}
|
||||
pendingLabel="Waiting for the customer to pay and attach the slip in the portal."
|
||||
doneLabel="Additional duty paid — slip received."
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (!canAct || !bookingId) {
|
||||
return (
|
||||
<StepStatus
|
||||
done={false}
|
||||
pendingLabel="GL Ethiopia decides whether additional duty/tax applies."
|
||||
doneLabel=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{!riskAssigned ? (
|
||||
<Alert color="blue" variant="light" icon={<Clock size={16} />}>
|
||||
Usually decided after the customs risk is assigned.
|
||||
</Alert>
|
||||
) : null}
|
||||
<Paper withBorder radius="md" p="md" bg="var(--mantine-color-gray-0)">
|
||||
<Stack gap="md">
|
||||
<Switch
|
||||
label="Additional duty/tax applies"
|
||||
description="Turn off if no further duty or tax is due after arrival."
|
||||
checked={dutyRequired}
|
||||
onChange={(e) => setDutyRequired(e.currentTarget.checked)}
|
||||
/>
|
||||
{dutyRequired ? (
|
||||
<>
|
||||
<Group grow align="flex-start">
|
||||
<NumberInput
|
||||
label="Amount"
|
||||
placeholder="0.00"
|
||||
value={amount}
|
||||
onChange={setAmount}
|
||||
min={0}
|
||||
size="sm"
|
||||
thousandSeparator=","
|
||||
/>
|
||||
<Select
|
||||
label="Currency"
|
||||
data={["ETB", "USD"]}
|
||||
value={currency}
|
||||
onChange={(v) => setCurrency(v ?? "ETB")}
|
||||
size="sm"
|
||||
/>
|
||||
</Group>
|
||||
<TextInput
|
||||
label="Declaration / payment code"
|
||||
placeholder="Customs payment reference"
|
||||
value={serial}
|
||||
onChange={(e) => setSerial(e.currentTarget.value)}
|
||||
size="sm"
|
||||
/>
|
||||
<PhasedFileDropzone
|
||||
label="Duty notice attachment"
|
||||
description="Any file type — shown to the customer in the portal."
|
||||
accept="*/*"
|
||||
value={attachment}
|
||||
onChange={setAttachment}
|
||||
onPreview={onViewFile}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={loading}
|
||||
disabled={dutyRequired && (amount === "" || Number(amount) <= 0 || !attachment)}
|
||||
fullWidth
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await contractsService.adviseSecondDuty(bookingId, {
|
||||
dutyRequired,
|
||||
amount: dutyRequired ? Number(amount) : undefined,
|
||||
currency,
|
||||
declarationSerial: serial || undefined,
|
||||
attachment: dutyRequired ? attachment : null,
|
||||
});
|
||||
toast.success(
|
||||
dutyRequired ? "Additional duty advised to customer" : "No additional duty recorded",
|
||||
);
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{dutyRequired ? "Send to customer" : "No additional duty"}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
/** GL ET uploads the import release document (auto-completes IMPORT_RELEASE_GRANTED). */
|
||||
function ImportReleaseStep({
|
||||
bookingId,
|
||||
clearance,
|
||||
canAct,
|
||||
workflowFiles = [],
|
||||
onChanged,
|
||||
onViewFile,
|
||||
onDownloadFile,
|
||||
}: {
|
||||
bookingId: string | null;
|
||||
clearance: ClearanceViewLike;
|
||||
canAct: boolean;
|
||||
workflowFiles?: Freight.ClearanceWorkflowFile[];
|
||||
onChanged?: () => void;
|
||||
onViewFile?: (file: { name: string; url: string }) => void;
|
||||
onDownloadFile?: (file: { id: string; name: string }) => void;
|
||||
}) {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const releaseFile = findWorkflowFile(workflowFiles, "import_release");
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
{releaseFile ? (
|
||||
<PhasedUploadedFileRow
|
||||
label="Import Release"
|
||||
file={releaseFile}
|
||||
onView={onViewFile}
|
||||
onDownload={onDownloadFile}
|
||||
compact
|
||||
/>
|
||||
) : null}
|
||||
{clearance.importReleaseGranted ? (
|
||||
<StepStatus done pendingLabel="" doneLabel="Import release granted." />
|
||||
) : canAct && bookingId ? (
|
||||
<>
|
||||
<PhasedFileDropzone
|
||||
label="Import release document"
|
||||
description="Any file type."
|
||||
accept="*/*"
|
||||
value={file}
|
||||
onChange={setFile}
|
||||
replaceMode={Boolean(releaseFile)}
|
||||
onPreview={onViewFile}
|
||||
/>
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={loading}
|
||||
disabled={!file}
|
||||
leftSection={<Upload size={16} />}
|
||||
onClick={async () => {
|
||||
if (!file) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await contractsService.uploadGlDocuments(bookingId, {
|
||||
import_release: file,
|
||||
});
|
||||
setFile(null);
|
||||
toast.success("Import release uploaded");
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Upload failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{releaseFile ? "Replace import release" : "Upload import release"}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<StepStatus
|
||||
done={false}
|
||||
pendingLabel="Waiting for GL Ethiopia to upload the import release document."
|
||||
doneLabel=""
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export function StepStatus({
|
||||
done,
|
||||
pendingLabel,
|
||||
|
||||
Reference in New Issue
Block a user