Last mile confirmation request , approval, payment Feature

This commit is contained in:
Hagernesh
2026-08-05 19:58:41 +00:00
parent 37d0cc966a
commit 1f4d659ffb
26 changed files with 1334 additions and 8 deletions

View File

@@ -46,6 +46,7 @@ import BookingContractPage from "./pages/bookings/BookingContractPage";
import BookingDetailPage from "./pages/bookings/BookingDetailPage";
import BookingsListPage from "./pages/bookings/BookingsListPage";
import EditBookingPage from "./pages/bookings/EditBookingPage";
import LastMileConfirmPage from "./pages/bookings/last-mile-confirm/LastMileConfirmPage";
import ContractDetailPage from "./pages/contracts/ContractDetailPage";
import ContractViewPage from "./pages/contracts/ContractViewPage";
import ContractsList from "./pages/contracts/ContractsList";
@@ -318,6 +319,10 @@ const App = () => {
/>
<Route path="/bookings/:id/edit" element={<EditBookingPage />} />
<Route path="/bookings/:id" element={<BookingDetailPage />} />
<Route
path="/bookings/:id/last-mile-confirm"
element={<LastMileConfirmPage />}
/>
<Route
path="/bookings/:id/contract"
element={<BookingContractPage />}

View File

@@ -217,4 +217,9 @@ export const URL_CONSTANTS = {
RECEIPT: (id: string) => `/api/warehouse-fee-invoices/${id}/receipt`,
PAY_ONLINE: (id: string) => `/api/warehouse-fee-invoices/${id}/pay-online`,
},
LAST_MILE_REQUESTS: {
BY_ID: (id: string) => `/last-mile-requests/${id}`,
SUBMIT: (id: string) => `/last-mile-requests/${id}/submit`,
},
};

View File

@@ -0,0 +1,160 @@
import { Button, Center, Checkbox, Loader, Stack, Text } from "@mantine/core";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import toast from "react-hot-toast";
import { bookingsService } from "@/services/bookings.service";
import { lastMileRequestsService } from "@/services/last-mile-requests.service";
import { CardTitle, PageShell, SectionCard } from "../BookingDetailPage/components/layout";
const errorMessage = (error: unknown, fallback: string) => {
const data = (error as { response?: { data?: { message?: string | string[] } } })
?.response?.data;
if (Array.isArray(data?.message)) return data.message.join(", ");
if (data?.message) return data.message;
return error instanceof Error ? error.message : fallback;
};
const STATUS_MESSAGE: Record<string, string> = {
SUBMITTED: "submitted",
APPROVED: "approved",
REJECTED: "rejected",
};
export default function LastMileConfirmPage() {
const { id } = useParams<{ id: string }>();
const [searchParams] = useSearchParams();
const requestId = searchParams.get("requestId");
const navigate = useNavigate();
const queryClient = useQueryClient();
const [selected, setSelected] = useState<string[]>([]);
const {
data: request,
isLoading: requestLoading,
isError: requestError,
} = useQuery({
queryKey: ["last-mile-request", requestId],
queryFn: () => lastMileRequestsService.get(requestId!),
enabled: !!requestId,
});
const { data: booking, isLoading: bookingLoading } = useQuery({
queryKey: ["booking", id],
queryFn: () => bookingsService.get(id!),
enabled: !!id,
});
const containerNumbers = booking?.containerNumbers ?? [];
const submitMutation = useMutation({
mutationFn: () => lastMileRequestsService.submit(requestId!, selected),
onSuccess: () => {
toast.success("Last-mile confirmation submitted");
queryClient.invalidateQueries({ queryKey: ["last-mile-request", requestId] });
navigate(`/bookings/${id}`);
},
onError: (e) => toast.error(errorMessage(e, "Could not submit confirmation")),
});
if (!requestId) {
return (
<PageShell>
<SectionCard p={22}>
<Text c="dimmed">Missing request id.</Text>
</SectionCard>
</PageShell>
);
}
if (requestLoading || bookingLoading) {
return (
<Center mih={300} p="xl">
<Loader color="edr-green" />
</Center>
);
}
if (requestError || !request) {
return (
<PageShell>
<SectionCard p={22}>
<Text c="dimmed">Could not load this confirmation request.</Text>
</SectionCard>
</PageShell>
);
}
if (request.status !== "AWAITING_CONFIRMATION") {
return (
<PageShell>
<SectionCard p={22}>
<CardTitle>Last-mile confirmation</CardTitle>
<Text mt={12}>
This request has already been {STATUS_MESSAGE[request.status]}.
</Text>
{request.status === "REJECTED" && request.rejectionReason && (
<Text mt={8} c="dimmed" fz="sm">
Reason: {request.rejectionReason}
</Text>
)}
</SectionCard>
</PageShell>
);
}
const allSelected =
containerNumbers.length > 0 && selected.length === containerNumbers.length;
const toggleAll = (checked: boolean) => {
setSelected(checked ? [...containerNumbers] : []);
};
const toggleOne = (containerNumber: string, checked: boolean) => {
setSelected((prev) =>
checked ? [...prev, containerNumber] : prev.filter((c) => c !== containerNumber),
);
};
return (
<PageShell>
<SectionCard p={22}>
<CardTitle>Confirm last-mile containers</CardTitle>
<Text mt={8} mb={16} fz="sm" c="dimmed">
Select which containers on this booking should be delivered via EDR
last-mile.
</Text>
<Stack gap={8}>
<Checkbox
label="Select all"
checked={allSelected}
onChange={(e) => toggleAll(e.currentTarget.checked)}
fw={700}
/>
{containerNumbers.map((c) => (
<Checkbox
key={c}
label={c}
checked={selected.includes(c)}
onChange={(e) => toggleOne(c, e.currentTarget.checked)}
ml={12}
/>
))}
</Stack>
<Button
mt={20}
disabled={selected.length === 0}
loading={submitMutation.isPending}
onClick={() => submitMutation.mutate()}
>
Submit confirmation
</Button>
</SectionCard>
</PageShell>
);
}

View File

@@ -0,0 +1,33 @@
import { URL_CONSTANTS } from "@/constants/URLS";
import { client } from "../utils/api";
const L = URL_CONSTANTS.LAST_MILE_REQUESTS;
export interface LastMileRequest {
id: string;
bookingId: string;
booking?: { id: string; reference?: string } | null;
trainScheduleId: string;
status: "AWAITING_CONFIRMATION" | "SUBMITTED" | "APPROVED" | "REJECTED";
requestedContainerNumbers?: string[] | null;
rejectionReason?: string | null;
createdAt: string;
updatedAt: string;
}
export const lastMileRequestsService = {
/** One last-mile confirmation request, by id. */
get: async (id: string): Promise<LastMileRequest> => {
const { data } = await client.get(L.BY_ID(id));
return data.data ?? data;
},
/** Confirm which containers on the booking go via EDR last-mile. */
submit: async (
id: string,
containerNumbers: string[],
): Promise<LastMileRequest> => {
const { data } = await client.post(L.SUBMIT(id), { containerNumbers });
return data.data ?? data;
},
};