Files
edr-platform/apps/edr-freight-web/backoffice/src/hooks/bookings/useBookings.ts

213 lines
6.6 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { isAxiosError } from "axios";
import toast from "react-hot-toast";
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
import { api } from "@/services/api";
import {
bookingsService,
type BookingListFilter,
} from "@/services/bookings.service";
import { invalidateBookingDetail } from "@/utils/queryInvalidation";
const parseApiError = (error: unknown, fallback: string) => {
if (isAxiosError(error)) {
const message = error.response?.data?.message;
if (Array.isArray(message)) return message.join(", ");
if (typeof message === "string") return message;
}
if (error instanceof Error && error.message) return error.message;
return fallback;
};
export function useBookingList(filter?: BookingListFilter, enabled = true) {
return useQuery({
queryKey: QUERY_KEYS.BOOKINGS.list(filter),
queryFn: () => bookingsService.list(filter),
enabled,
});
}
export function useBookingListSummary(filter?: BookingListFilter, enabled = true) {
return useQuery({
queryKey: QUERY_KEYS.BOOKINGS.listSummary(filter),
queryFn: () => bookingsService.getListSummary(filter),
enabled,
});
}
export function useBookingDetail(id: string | undefined) {
return useQuery({
queryKey: QUERY_KEYS.BOOKINGS.byId(id ?? ""),
queryFn: () => bookingsService.getById(id!),
enabled: Boolean(id),
});
}
export function useBookingMutations(bookingId: string) {
const qc = useQueryClient();
const onSuccess = (data: { id: string }, message: string) => {
toast.success(message);
void invalidateBookingDetail(qc, data.id);
};
const staffAccept = useMutation({
mutationFn: (validityDays: number) =>
api.bookings.staffAccept.call({ id: bookingId, validityDays }),
onSuccess: (data) => onSuccess(data, "Booking accepted for approval"),
onError: () => toast.error("Failed to accept booking"),
});
const requestChanges = useMutation({
mutationFn: (note: string) =>
api.bookings.requestChanges.call({ id: bookingId, note }),
onSuccess: (data) => onSuccess(data, "Changes requested from customer"),
onError: () => toast.error("Failed to request changes"),
});
const staffReject = useMutation({
mutationFn: (reason: string) =>
api.bookings.staffReject.call({ id: bookingId, reason }),
onSuccess: (data) => onSuccess(data, "Booking rejected"),
onError: () => toast.error("Failed to reject booking"),
});
const reviewOperation = useMutation({
mutationFn: (payload: {
decision: "ACCEPT" | "REQUEST_CHANGES";
note?: string;
}) => api.bookings.reviewOperation.call({ id: bookingId, ...payload }),
onSuccess: (data) => onSuccess(data, "Operation request reviewed"),
onError: (error) => {
toast.error(parseApiError(error, "Failed to review operation request"));
// The transition may have committed even when the response errored (e.g.
// a post-accept step failed). Refetch so the UI shows the true state
// instead of requiring a manual refresh.
void invalidateBookingDetail(qc, bookingId);
},
});
const approveStep = useMutation({
mutationFn: ({
stepId,
requiredRole,
}: {
stepId: string;
requiredRole: string;
}) =>
api.bookings.approveStep.call({
id: bookingId,
stepId,
requiredRole,
}),
onSuccess: (data) => onSuccess(data, "Approval step completed"),
onError: (error) => toast.error(parseApiError(error, "Failed to approve step")),
});
const rejectStep = useMutation({
mutationFn: ({
stepId,
reason,
}: {
stepId: string;
reason: string;
}) =>
api.bookings.rejectStep.call({
id: bookingId,
stepId,
reason,
}),
onSuccess: (data) => onSuccess(data, "Booking rejected at approval step"),
onError: (error) => toast.error(parseApiError(error, "Failed to reject step")),
});
const generateContract = useMutation({
mutationFn: () => api.bookings.generateContract.call({ id: bookingId }),
onSuccess: (data) => onSuccess(data, "Contract generated"),
onError: () => toast.error("Failed to generate contract"),
});
const signContract = useMutation({
mutationFn: (payload: {
role: "CUSTOMER" | "STAFF";
signatureImageBase64: string;
signerDisplayName: string;
consentText?: string;
}) => bookingsService.signContract(bookingId, payload),
onSuccess: (data) => onSuccess(data, "Contract signed"),
onError: () => toast.error("Failed to sign contract"),
});
const payBooking = useMutation({
mutationFn: () => api.bookings.payBooking.call({ id: bookingId }),
onSuccess: (data) => onSuccess(data, "Payment completed"),
onError: () => toast.error("Failed to complete payment"),
});
const startTransit = useMutation({
mutationFn: () => api.bookings.startTransit.call({ id: bookingId }),
onSuccess: (data) => onSuccess(data, "Marked in transit"),
onError: () => toast.error("Failed to start transit"),
});
const complete = useMutation({
mutationFn: () => api.bookings.complete.call({ id: bookingId }),
onSuccess: (data) => onSuccess(data, "Booking completed"),
onError: () => toast.error("Failed to complete booking"),
});
const cancel = useMutation({
mutationFn: (reason: string) =>
api.bookings.cancel.call({ id: bookingId, reason }),
onSuccess: (data) => onSuccess(data, "Booking cancelled"),
onError: () => toast.error("Failed to cancel booking"),
});
const isPending =
staffAccept.isPending ||
requestChanges.isPending ||
staffReject.isPending ||
approveStep.isPending ||
rejectStep.isPending ||
generateContract.isPending ||
signContract.isPending ||
payBooking.isPending ||
startTransit.isPending ||
complete.isPending ||
reviewOperation.isPending ||
cancel.isPending;
return {
staffAccept,
requestChanges,
staffReject,
reviewOperation,
approveStep,
rejectStep,
generateContract,
signContract,
payBooking,
startTransit,
complete,
cancel,
isPending,
downloadContract: () => bookingsService.downloadContract(bookingId),
};
}
export function useBookingEtClearanceQueue(enabled = true) {
return useQuery({
queryKey: QUERY_KEYS.BOOKINGS.clearanceQueue("ET"),
queryFn: () => bookingsService.getEtClearanceQueue(),
enabled,
});
}
export function useBookingDjClearanceQueue(enabled = true) {
return useQuery({
queryKey: QUERY_KEYS.BOOKINGS.clearanceQueue("DJ"),
queryFn: () => bookingsService.getDjClearanceQueue(),
enabled,
});
}