import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { ExternalLink, MoreHorizontal, Receipt } from "lucide-react"; import { Button, Menu, ActionIcon, Group, Text } from "@mantine/core"; import { BookingConfirmDialog } from "./BookingConfirmDialog"; import { OperationRescheduleModal } from "./OperationRescheduleModal"; import { useBookingActionDialog } from "./useBookingActionDialog"; import { useAuth } from "@/auth/useAuth"; import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from "@/lib/permissions"; import { isAllocateAction, isClearanceNavAction, isContractNavAction, isRescheduleAction, listRowHasActions, type BookingActionContext, } from "@/features/bookings/booking-actions.config"; import type { BookingListRow } from "@/types/booking"; interface BookingActionsMenuProps { row: BookingListRow; /** Compact table cell vs. larger detail toolbar */ variant?: "table" | "toolbar"; className?: string; /** Suppresses table row navigation after menu/dialog close (click-through). */ onSuppressRowClick?: () => void; onAllocateBooking?: () => void; } export function BookingActionsMenu({ row, variant = "table", onSuppressRowClick, onAllocateBooking, }: BookingActionsMenuProps) { const navigate = useNavigate(); const { user } = useAuth(); const context: BookingActionContext = { status: row.status, paymentCurrency: row.paymentCurrency, reference: row.reference, schedulingStatus: row.schedulingStatus, customsClearingEnabled: row.customsClearingEnabled, consolidationPartnerId: row.consolidationPartnerId, }; const flow = useBookingActionDialog(row.id, context); const { actions, pendingAction, mutations } = flow; // Day / train reschedule has its own modal (date + export train picker). const [rescheduleOpen, setRescheduleOpen] = useState(false); const goToContract = () => navigate(`/dashboard/booking-requests/${row.id}/contract`); const goToClearanceTab = () => navigate(`/dashboard/booking-requests/${row.id}?tab=clearance`); const goToAdditionalCharges = () => navigate(`/dashboard/booking-requests/${row.id}?tab=additional-charges`); const canSeeAdditionalCharges = hasFreightPermission( user, FREIGHT_PERMS.additionalCharges.view, ); const handleAction = (action: (typeof actions)[number]) => { onSuppressRowClick?.(); if (isContractNavAction(action.id)) { goToContract(); } else if (isClearanceNavAction(action.id)) { goToClearanceTab(); } else if (isAllocateAction(action.id)) { onAllocateBooking?.(); } else if (isRescheduleAction(action.id)) { setRescheduleOpen(true); } else { flow.openAction(action); } }; const hasMenu = listRowHasActions(row, user); // Row click already opens the detail page — no chevron affordance needed. if (!hasMenu && variant === "table") { return null; } // Toolbar: lay every action out as a button row. if (variant === "toolbar" && actions.length > 0) { return ( <> {actions.map((action) => { const Icon = action.icon; const destructive = action.variant === "destructive"; return ( ); })} { onSuppressRowClick?.(); setRescheduleOpen(false); }} /> ); } return ( e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} > {row.reference} {actions.map((action) => { const Icon = action.icon; return ( } onClick={() => handleAction(action)} > {action.label} ); })} {actions.length > 0 && } {canSeeAdditionalCharges && ( } onClick={() => { onSuppressRowClick?.(); goToAdditionalCharges(); }} > Additional charges )} } onClick={() => { onSuppressRowClick?.(); navigate(`/dashboard/booking-requests/${row.id}`); }} > Open full details { onSuppressRowClick?.(); setRescheduleOpen(false); }} /> ); } function ActionDialog({ flow, pendingAction, onSuppressRowClick, consolidationPartnerId, consolidationPartnerReference, }: { flow: ReturnType; pendingAction: ReturnType["pendingAction"]; onSuppressRowClick?: () => void; consolidationPartnerId?: string | null; consolidationPartnerReference?: string | null; }) { return ( { if (!open) onSuppressRowClick?.(); flow.setDialogOpen(open); }} action={pendingAction} reference={flow.mergedContext.reference} inputValue={flow.inputValue} onInputChange={flow.setInputValue} selectedFile={flow.selectedFile} onFileChange={flow.setSelectedFile} onConfirm={() => { onSuppressRowClick?.(); flow.runAction(); }} isPending={flow.mutations.isPending || flow.detailLoading} confirmDisabled={flow.confirmDisabled} // Only the four pairable decisions land on both halves; the rest stay // per booking, so the warning must not appear for them. pairedWithReference={ consolidationPartnerId && pendingAction && ["accept", "cancel", "operationAccept", "requestChanges"].includes( pendingAction.id, ) ? (consolidationPartnerReference ?? "its wagon partner") : null } /> ); }