Merge pull request #997 from Tria-plc/freight_feature/usermanagement

let customers dispute booking duty/tax advice so GL Ethiopia can re-a…
This commit is contained in:
marshal
2026-07-28 13:49:47 +03:00
committed by GitHub
9 changed files with 426 additions and 6 deletions

View File

@@ -10,12 +10,14 @@ import {
Paper,
Stack,
Text,
Textarea,
} from "@mantine/core";
import {
AlertTriangle,
Download,
Eye,
FileBadge,
MessageSquareWarning,
Receipt,
Upload,
} from "lucide-react";
@@ -84,6 +86,9 @@ export function BookingClearanceWorkflowBanner({
clearance.dutyRequired &&
clearance.dutyAdvice &&
!dutyPaid;
// A dispute clears the advice while it's open — show the "waiting on GL"
// state instead of the pay panel until GL re-advises.
const dutyDisputePending = Boolean(clearance.dutyDispute);
return (
<Paper withBorder radius="lg" p="lg" style={{ borderColor: BORDER }}>
@@ -109,11 +114,13 @@ export function BookingClearanceWorkflowBanner({
</Alert>
) : null}
{dutyPending && clearance.dutyAdvice ? (
{dutyDisputePending && clearance.dutyDispute ? (
<DutyDisputePendingCard dispute={clearance.dutyDispute} />
) : dutyPending && clearance.dutyAdvice ? (
<DutyAdvicePanel
dutyAdvice={clearance.dutyAdvice}
bookingId={booking.id}
onUploaded={() => void refetch()}
onChanged={() => void refetch()}
/>
) : null}
@@ -178,14 +185,17 @@ export function BookingClearanceWorkflowBanner({
function DutyAdvicePanel({
dutyAdvice,
bookingId,
onUploaded,
onChanged,
}: {
dutyAdvice: NonNullable<Freight.ClearanceView["dutyAdvice"]>;
bookingId: string;
onUploaded: () => void;
onChanged: () => void;
}) {
const [file, setFile] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
const [disputing, setDisputing] = useState(false);
const [note, setNote] = useState("");
const [submittingDispute, setSubmittingDispute] = useState(false);
const noticeFile = dutyAdvice.noticeFile;
return (
@@ -229,7 +239,7 @@ function DutyAdvicePanel({
try {
await bookingsService.uploadBookingClearanceDutySlip(bookingId, file);
toast.success("Payment slip uploaded");
onUploaded();
onChanged();
} catch (e) {
toast.error(e instanceof Error ? e.message : "Upload failed");
} finally {
@@ -239,11 +249,101 @@ function DutyAdvicePanel({
>
Submit payment slip
</Button>
{disputing ? (
<Stack gap={6}>
<Textarea
label="What's wrong with this amount?"
placeholder="Explain why you're disputing the advised duty/tax…"
minRows={2}
autosize
value={note}
onChange={(e) => setNote(e.currentTarget.value)}
/>
<Group gap="xs">
<Button
variant="default"
size="xs"
onClick={() => {
setDisputing(false);
setNote("");
}}
>
Cancel
</Button>
<Button
color="red"
size="xs"
loading={submittingDispute}
disabled={!note.trim()}
onClick={async () => {
setSubmittingDispute(true);
try {
await bookingsService.disputeBookingClearanceDuty(
bookingId,
note.trim(),
);
toast.success("Sent to GL Ethiopia for review");
setDisputing(false);
setNote("");
onChanged();
} catch (e) {
toast.error(e instanceof Error ? e.message : "Failed to send");
} finally {
setSubmittingDispute(false);
}
}}
>
Submit request
</Button>
</Group>
</Stack>
) : (
<Anchor
component="button"
type="button"
size="sm"
c="dimmed"
onClick={() => setDisputing(true)}
>
Not right? Request a change
</Anchor>
)}
</Stack>
</Paper>
);
}
/** The customer's dispute is open — GL Ethiopia owes a corrected advice. */
function DutyDisputePendingCard({
dispute,
}: {
dispute: NonNullable<Freight.ClearanceView["dutyDispute"]>;
}) {
return (
<Alert
color="orange"
radius="md"
icon={<MessageSquareWarning size={16} />}
title={
dispute.rounds > 1
? `Waiting on GL Ethiopia (round ${dispute.rounds})`
: "Waiting on GL Ethiopia"
}
>
<Stack gap={4}>
<Text size="sm" style={{ whiteSpace: "pre-wrap" }}>
{dispute.note}
</Text>
<Text size="xs" c="dimmed">
Sent {new Date(dispute.raisedAt).toLocaleString()} you'll see the
corrected amount here once GL Ethiopia re-advises.
</Text>
</Stack>
</Alert>
);
}
function GroupLabel({ icon: Icon, text }: { icon: typeof Receipt; text: string }) {
return (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>

View File

@@ -398,6 +398,16 @@ export const bookingsService = {
return data.data ?? data;
},
disputeBookingClearanceDuty: async (
id: string,
note: string,
): Promise<Freight.IBooking> => {
const { data } = await client.post(`/api/bookings/${id}/clearance/duty/dispute`, {
note,
});
return data.data ?? data;
},
getContractView: async (id: string): Promise<ContractView> => {
const { data } = await client.get(B.CONTRACT_VIEW(id));
return data.data ?? data;