mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
178 lines
5.3 KiB
TypeScript
178 lines
5.3 KiB
TypeScript
import { useCallback, useState } from "react";
|
||
|
||
import {
|
||
getBookingActions,
|
||
getNextPendingApprovalStep,
|
||
type BookingActionContext,
|
||
type BookingActionDef,
|
||
} from "@/features/bookings/booking-actions.config";
|
||
import { useAuth } from "@/auth/useAuth";
|
||
import { useBookingDetail, useBookingMutations } from "@/hooks/bookings/useBookings";
|
||
|
||
/** A contract validity window must be a whole number of days, 1–365. */
|
||
function isValidValidityDays(value: string): boolean {
|
||
const days = Number(value.trim());
|
||
return Number.isInteger(days) && days >= 1 && days <= 365;
|
||
}
|
||
|
||
/** An adjusted price must be a non-negative number. */
|
||
function isValidAmount(value: string): boolean {
|
||
if (!value.trim()) return false;
|
||
const amount = Number(value.trim());
|
||
return Number.isFinite(amount) && amount >= 0;
|
||
}
|
||
|
||
export function useBookingActionDialog(
|
||
bookingId: string,
|
||
context: BookingActionContext,
|
||
) {
|
||
const [pendingAction, setPendingAction] = useState<BookingActionDef | null>(null);
|
||
const [inputValue, setInputValue] = useState("");
|
||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||
const [dialogOpen, setDialogOpen] = useState(false);
|
||
|
||
const needsApprovalSteps =
|
||
pendingAction?.id === "approve" || pendingAction?.id === "rejectApproval";
|
||
|
||
const needsApprovalContext =
|
||
context.status === "PENDING_APPROVAL" ||
|
||
context.status === "APPROVED_PENDING_SIGNATURE";
|
||
|
||
const { data: detail, isLoading: detailLoading } = useBookingDetail(
|
||
needsApprovalSteps || needsApprovalContext ? bookingId : undefined,
|
||
);
|
||
|
||
const mergedContext: BookingActionContext = {
|
||
...context,
|
||
approvalSteps: detail?.approvalSteps ?? context.approvalSteps,
|
||
reference: detail?.reference ?? context.reference,
|
||
};
|
||
|
||
const { user } = useAuth();
|
||
const mutations = useBookingMutations(bookingId);
|
||
const actions = getBookingActions(mergedContext, user);
|
||
|
||
const openAction = useCallback((action: BookingActionDef) => {
|
||
setPendingAction(action);
|
||
setInputValue("");
|
||
setSelectedFile(null);
|
||
setDialogOpen(true);
|
||
}, []);
|
||
|
||
const closeDialog = useCallback(() => {
|
||
setDialogOpen(false);
|
||
setPendingAction(null);
|
||
setInputValue("");
|
||
setSelectedFile(null);
|
||
}, []);
|
||
|
||
const runAction = useCallback(() => {
|
||
if (!pendingAction) return;
|
||
|
||
const onSuccess = () => closeDialog();
|
||
|
||
switch (pendingAction.id) {
|
||
case "accept": {
|
||
const days = Number(inputValue.trim());
|
||
if (!Number.isInteger(days) || days < 1 || days > 365) return;
|
||
mutations.staffAccept.mutate(days, { onSuccess });
|
||
break;
|
||
}
|
||
case "requestChanges":
|
||
mutations.requestChanges.mutate(inputValue.trim(), { onSuccess });
|
||
break;
|
||
case "reject":
|
||
mutations.staffReject.mutate(inputValue.trim(), { onSuccess });
|
||
break;
|
||
case "operationAccept":
|
||
mutations.reviewOperation.mutate({ decision: "ACCEPT" }, { onSuccess });
|
||
break;
|
||
case "operationRequestChanges":
|
||
mutations.reviewOperation.mutate(
|
||
{ decision: "REQUEST_CHANGES", note: inputValue.trim() },
|
||
{ onSuccess },
|
||
);
|
||
break;
|
||
case "operationAdjustPrice": {
|
||
const amount = Number(inputValue.trim());
|
||
if (!Number.isFinite(amount) || amount < 0) return;
|
||
mutations.reviewOperation.mutate(
|
||
{ decision: "ADJUST_PRICE", amount },
|
||
{ onSuccess },
|
||
);
|
||
break;
|
||
}
|
||
case "approve": {
|
||
const step = getNextPendingApprovalStep(mergedContext.approvalSteps);
|
||
if (!step) return;
|
||
mutations.approveStep.mutate(
|
||
{ stepId: step.id, requiredRole: step.requiredRole },
|
||
{ onSuccess },
|
||
);
|
||
break;
|
||
}
|
||
case "rejectApproval": {
|
||
const step = getNextPendingApprovalStep(mergedContext.approvalSteps);
|
||
if (!step) return;
|
||
mutations.rejectStep.mutate(
|
||
{ stepId: step.id, reason: inputValue.trim() },
|
||
{ onSuccess },
|
||
);
|
||
break;
|
||
}
|
||
case "viewContract":
|
||
break;
|
||
case "startTransit":
|
||
mutations.startTransit.mutate(undefined, { onSuccess });
|
||
break;
|
||
case "complete":
|
||
mutations.complete.mutate(undefined, { onSuccess });
|
||
break;
|
||
case "cancel":
|
||
mutations.cancel.mutate(inputValue.trim(), { onSuccess });
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}, [
|
||
pendingAction,
|
||
inputValue,
|
||
selectedFile,
|
||
mergedContext.approvalSteps,
|
||
mutations,
|
||
closeDialog,
|
||
]);
|
||
|
||
const confirmDisabled =
|
||
mutations.isPending ||
|
||
(needsApprovalSteps && detailLoading) ||
|
||
(pendingAction?.id === "approve" &&
|
||
!getNextPendingApprovalStep(mergedContext.approvalSteps)) ||
|
||
(pendingAction?.input === "file" && !selectedFile) ||
|
||
(pendingAction?.input === "reason" && !inputValue.trim()) ||
|
||
(pendingAction?.input === "note" && !inputValue.trim()) ||
|
||
(pendingAction?.input === "days" && !isValidValidityDays(inputValue)) ||
|
||
(pendingAction?.input === "amount" && !isValidAmount(inputValue));
|
||
|
||
return {
|
||
actions,
|
||
pendingAction,
|
||
inputValue,
|
||
setInputValue,
|
||
selectedFile,
|
||
setSelectedFile,
|
||
dialogOpen,
|
||
setDialogOpen: (open: boolean) => {
|
||
if (!open) closeDialog();
|
||
else setDialogOpen(true);
|
||
},
|
||
openAction,
|
||
closeDialog,
|
||
runAction,
|
||
mutations,
|
||
confirmDisabled,
|
||
detailLoading,
|
||
mergedContext,
|
||
};
|
||
}
|