diff --git a/apps/edr-freight-web/backoffice/src/hooks/useBookings.ts b/apps/edr-freight-web/backoffice/src/hooks/useBookings.ts new file mode 100644 index 000000000..5ee2e5749 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/hooks/useBookings.ts @@ -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() }), + }); +}; diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx index 86b4f483e..3bb75b6bf 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestDetailPage.tsx @@ -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( - 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 ( diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx index d59658131..643257bbd 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx @@ -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(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(); diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/booking-requests.mock.ts b/apps/edr-freight-web/backoffice/src/pages/bookings/booking-requests.mock.ts index a5d13cbc0..9adb8eb33 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/booking-requests.mock.ts +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/booking-requests.mock.ts @@ -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; diff --git a/apps/edr-freight-web/backoffice/src/services/api.ts b/apps/edr-freight-web/backoffice/src/services/api.ts index f45b3e29e..f0d585859 100644 --- a/apps/edr-freight-web/backoffice/src/services/api.ts +++ b/apps/edr-freight-web/backoffice/src/services/api.ts @@ -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 + >("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), + ), + }, }; diff --git a/apps/edr-freight-web/backoffice/src/services/bookings.service.ts b/apps/edr-freight-web/backoffice/src/services/bookings.service.ts new file mode 100644 index 000000000..e823b2146 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/services/bookings.service.ts @@ -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> => { + const response = await client.get>( + BASE, + { params: filter }, + ); + return unwrap(response.data); + }, + + getById: async (id: string): Promise => { + const response = await client.get( + URL_CONSTANTS.BOOKINGS.BY_ID(id), + ); + return unwrap(response.data); + }, + + updateStatus: async ( + id: string, + payload: { action: string; reason?: string }, + ): Promise => { + const response = await client.patch( + `${URL_CONSTANTS.BOOKINGS.BY_ID(id)}/status`, + payload, + ); + return unwrap(response.data); + }, + + remove: async (id: string): Promise => { + await client.delete(URL_CONSTANTS.BOOKINGS.BY_ID(id)); + }, +};