mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 13:38:20 +00:00
213 lines
6.6 KiB
TypeScript
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,
|
|
});
|
|
}
|