mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 00:45:41 +00:00
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:
@@ -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 }}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user