feat(bookings): Integrate booking list and detail pages with backend API

This commit is contained in:
ghost2023
2026-06-02 16:04:47 +03:00
parent 34bf4ade48
commit dab72217e1
6 changed files with 189 additions and 20 deletions

View File

@@ -0,0 +1,48 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { api } from "@/services/api";
import type { BookingListFilter } from "@/services/bookings.service";
export const useBookingList = (filter?: BookingListFilter) => {
const input = { filter };
return {
queryKey: api.bookings.list.queryKey(input),
queryFn: () => api.bookings.list.call(input),
};
};
export const useBooking = (id: string) => ({
queryKey: api.bookings.getById.queryKey({ id }),
queryFn: () => api.bookings.getById.call({ id }),
enabled: Boolean(id),
});
export const useUpdateBookingStatus = () => {
const qc = useQueryClient();
return useMutation({
mutationFn: ({
id,
action,
reason,
}: {
id: string;
action: string;
reason?: string;
}) => api.bookings.updateStatus.call({ id, action, reason }),
onSuccess: (_data, { id }) => {
qc.invalidateQueries({ queryKey: api.bookings.list.queryKey() });
qc.invalidateQueries({
queryKey: api.bookings.getById.queryKey({ id }),
});
},
});
};
export const useDeleteBooking = () => {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) => api.bookings.remove.call({ id }),
onSuccess: () =>
qc.invalidateQueries({ queryKey: api.bookings.list.queryKey() }),
});
};

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import {
AlertCircle,
AlertTriangle,
@@ -27,14 +27,9 @@ import {
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { cn } from "@/lib/utils";
import {
getBookingRequestById,
getBookingRequests,
saveBookingRequestsToStorage,
updateBookingRequestStatus,
BOOKING_STATUSES,
type BookingRequest,
} from "./booking-requests.mock";
import { api } from "@/services/api";
import { useUpdateBookingStatus } from "@/hooks/useBookings";
import { mapBookingToRequest, BOOKING_STATUSES } from "./booking-requests.mock";
import {
Badge,
Button,
@@ -233,9 +228,16 @@ const STATUS_CONFIG: Record<
export default function BookingRequestDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [booking, setBooking] = useState<BookingRequest | undefined>(
id ? getBookingRequestById(id) : undefined,
const { data: bookingData } = useQuery(
api.bookings.getById.queryOptions({
input: { id: id ?? "" },
enabled: Boolean(id),
}),
);
const updateStatus = useUpdateBookingStatus();
const booking = bookingData ? mapBookingToRequest(bookingData) : undefined;
if (!booking) {
return (
@@ -270,20 +272,20 @@ export default function BookingRequestDetailPage() {
booking.status,
);
const isPending = updateStatus.isPending;
function handleApprove() {
if (!booking) return;
const nextStatus =
const action =
booking.status === "RFQ_SUBMITTED"
? ("QUOTATION_SENT" as const)
: ("APPROVED" as const);
updateBookingRequestStatus(booking.id, nextStatus);
setBooking(getBookingRequestById(booking.id));
? "SEND_QUOTATION"
: "APPROVE";
updateStatus.mutate({ id: booking.id, action });
}
function handleReject() {
if (!booking) return;
updateBookingRequestStatus(booking.id, "CANCELLED");
setBooking(getBookingRequestById(booking.id));
updateStatus.mutate({ id: booking.id, action: "CANCEL", reason: "Cancelled by backoffice" });
}
return (

View File

@@ -1,5 +1,6 @@
import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import {
AlertCircle,
ArrowRight,
@@ -18,10 +19,11 @@ import {
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { cn } from "@/lib/utils";
import { api } from "@/services/api";
import {
getBookingRequests,
BOOKING_STATUSES,
type BookingRequest,
mapBookingToRequest,
} from "./booking-requests.mock";
import {
DataTable,
@@ -153,7 +155,13 @@ export default function BookingRequestsPage() {
const [query, setQuery] = useState("");
const [statusFilter, setStatusFilter] = useState<string | null>(null);
const bookingRequests = useMemo(() => getBookingRequests(), []);
const { data: bookingData } = useQuery(
api.bookings.list.queryOptions({ input: { filter: { page: pagination.pageIndex + 1, pageSize: pagination.pageSize } } }),
);
const bookingRequests = useMemo(
() => (bookingData?.items ?? []).map(mapBookingToRequest),
[bookingData],
);
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();

View File

@@ -1,3 +1,5 @@
import type { Freight } from "@edr/types";
export interface BookingRequest {
id: string;
reference: string;
@@ -131,6 +133,36 @@ export function updateBookingRequestStatus(id: string, newStatus: (typeof BOOKIN
saveBookingRequestsToStorage(requests);
}
export function mapBookingToRequest(booking: Freight.IBooking): BookingRequest {
return {
id: booking.id,
reference: booking.reference,
customer: booking.customerId,
status: booking.status as BookingRequest["status"],
scheduledDate: booking.scheduledDate,
totalAmount: booking.totalAmount,
paymentStatus: booking.paymentStatus,
contractType: booking.contractType,
serviceType:
booking.serviceType === "RAIL_ONLY" ? "RAIL" : (booking.serviceType as string),
tradeDirection: booking.tradeDirection as string,
originYard: booking.originStation,
destinationYard: booking.destinationStation,
cargoType: booking.freightType ?? booking.freightSubtype ?? "",
cargoTotalWeightVgm: booking.cargoTotalWeightVgm,
isHazardous: booking.isHazardous,
paymentCurrency: booking.paymentCurrency,
priorityScore: booking.priorityScore,
firstMilePickupAddress: booking.firstMilePickupAddress ?? null,
lastMileDeliveryAddress: booking.lastMileDeliveryAddress ?? null,
shippingLine: null,
pnrCode: null,
createdBy: booking.customerId,
createdAt: booking.createdAt,
updatedAt: booking.updatedAt,
};
}
export function getBookingRequests(): BookingRequest[] {
if (typeof window === "undefined" || !window.localStorage) {
return INITIAL_REQUESTS;

View File

@@ -27,6 +27,8 @@ import {
ruleEngineService,
RuleEngineListParams,
} from "./ruleEngine/ruleEngine.service";
import { bookingsService, BookingListFilter } from "./bookings.service";
import type { Freight, PaginatedResponse } from "@edr/types";
export const api = {
fileUploadSettings: {
@@ -220,4 +222,30 @@ export const api = {
() => ruleEngineService.getApprovalChain(),
),
},
bookings: {
list: endpoint<
{ filter?: BookingListFilter },
PaginatedResponse<Freight.IBooking>
>("bookings", "list", ({ filter }) => bookingsService.list(filter)),
getById: endpoint<{ id: string }, Freight.IBooking>(
"bookings",
"getById",
({ id }) => bookingsService.getById(id),
),
updateStatus: endpoint<
{ id: string; action: string; reason?: string },
Freight.IBooking
>("bookings", "updateStatus", ({ id, action, reason }) =>
bookingsService.updateStatus(id, { action, reason }),
),
remove: endpoint<{ id: string }, void>(
"bookings",
"remove",
({ id }) => bookingsService.remove(id),
),
},
};

View File

@@ -0,0 +1,51 @@
import type { Freight, PaginatedResponse } from "@edr/types";
import { api as client } from "../auth/http";
import { unwrap } from "@/utils/endpoint";
import { URL_CONSTANTS } from "@/constants/URLS";
const BASE = URL_CONSTANTS.BOOKINGS.BASE;
export interface BookingListFilter {
status?: string;
customerId?: string;
search?: string;
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: "ASC" | "DESC";
}
export const bookingsService = {
list: async (
filter?: BookingListFilter,
): Promise<PaginatedResponse<Freight.IBooking>> => {
const response = await client.get<PaginatedResponse<Freight.IBooking>>(
BASE,
{ params: filter },
);
return unwrap(response.data);
},
getById: async (id: string): Promise<Freight.IBooking> => {
const response = await client.get<Freight.IBooking>(
URL_CONSTANTS.BOOKINGS.BY_ID(id),
);
return unwrap(response.data);
},
updateStatus: async (
id: string,
payload: { action: string; reason?: string },
): Promise<Freight.IBooking> => {
const response = await client.patch<Freight.IBooking>(
`${URL_CONSTANTS.BOOKINGS.BY_ID(id)}/status`,
payload,
);
return unwrap(response.data);
},
remove: async (id: string): Promise<void> => {
await client.delete(URL_CONSTANTS.BOOKINGS.BY_ID(id));
},
};