Files
edr-platform/apps/edr-freight-web/backoffice/src/components/bookings/useBookingActionDialog.ts
Marshal 0f7cac2b68 add booking request functionality for GENERAL customs contracts
- Create migration for booking_requests table with necessary fields and indexes.
- Implement BookingRequestRepository for database operations related to booking requests.
- Develop BookingRequestService to handle business logic for submitting, accepting, rejecting, and canceling booking requests.
- Create DTOs for creating booking requests and reviewing them.
- Define BookingRequest entity to map to the booking_requests table.
- Add UI components for managing shipment requests, including detail and list pages.
- Implement OperationDatePicker component for selecting available shipment days.
2026-06-29 09:30:44 +00:00

161 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, 1365. */
function isValidValidityDays(value: string): boolean {
const days = Number(value.trim());
return Number.isInteger(days) && days >= 1 && days <= 365;
}
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 "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));
return {
actions,
pendingAction,
inputValue,
setInputValue,
selectedFile,
setSelectedFile,
dialogOpen,
setDialogOpen: (open: boolean) => {
if (!open) closeDialog();
else setDialogOpen(true);
},
openAction,
closeDialog,
runAction,
mutations,
confirmDisabled,
detailLoading,
mergedContext,
};
}