mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 00:08:18 +00:00
Last mile confirmation request , approval, payment Feature
This commit is contained in:
@@ -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 />}
|
||||
|
||||
@@ -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`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user