mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 14:08:11 +00:00
220 lines
6.5 KiB
TypeScript
220 lines
6.5 KiB
TypeScript
import { useNavigate } from "react-router-dom";
|
|
import { ChevronRight, ExternalLink, MoreHorizontal } from "lucide-react";
|
|
import { Button, Menu, ActionIcon, Group, Text } from "@mantine/core";
|
|
|
|
import { BookingConfirmDialog } from "./BookingConfirmDialog";
|
|
import { useBookingActionDialog } from "./useBookingActionDialog";
|
|
import { useAuth } from "@/auth/useAuth";
|
|
import {
|
|
getNextPendingApprovalStep,
|
|
isAllocateAction,
|
|
isClearanceNavAction,
|
|
isContractNavAction,
|
|
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,
|
|
approvalSteps: row.approvalSteps,
|
|
schedulingStatus: row.schedulingStatus,
|
|
customsClearingEnabled: row.customsClearingEnabled,
|
|
};
|
|
|
|
const flow = useBookingActionDialog(row.id, context);
|
|
const { actions, pendingAction, mutations } = flow;
|
|
|
|
const goToContract = () =>
|
|
navigate(`/dashboard/booking-requests/${row.id}/contract`);
|
|
|
|
const goToClearanceTab = () =>
|
|
navigate(`/dashboard/booking-requests/${row.id}?tab=clearance`);
|
|
|
|
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 {
|
|
flow.openAction(action);
|
|
}
|
|
};
|
|
|
|
const hasMenu = listRowHasActions(row, user);
|
|
|
|
if (!hasMenu && variant === "table") {
|
|
return (
|
|
<ActionIcon
|
|
variant="subtle"
|
|
color="gray"
|
|
onClick={() => navigate(`/dashboard/booking-requests/${row.id}`)}
|
|
aria-label="View booking"
|
|
>
|
|
<ChevronRight size={16} />
|
|
</ActionIcon>
|
|
);
|
|
}
|
|
|
|
// Toolbar: lay every action out as a button row.
|
|
if (variant === "toolbar" && actions.length > 0) {
|
|
return (
|
|
<>
|
|
<Group gap="sm" w="100%">
|
|
{actions.map((action) => {
|
|
const Icon = action.icon;
|
|
const destructive = action.variant === "destructive";
|
|
return (
|
|
<Button
|
|
key={action.id}
|
|
size="sm"
|
|
variant={action.primary && !destructive ? "filled" : "default"}
|
|
color={destructive ? "red" : action.primary ? "edr-green" : "gray"}
|
|
leftSection={<Icon size={16} />}
|
|
disabled={mutations.isPending}
|
|
onClick={() => handleAction(action)}
|
|
>
|
|
{action.label}
|
|
</Button>
|
|
);
|
|
})}
|
|
</Group>
|
|
<ActionDialog flow={flow} pendingAction={pendingAction} onSuppressRowClick={onSuppressRowClick} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Group
|
|
gap={4}
|
|
justify="flex-end"
|
|
wrap="nowrap"
|
|
data-stop-row-click
|
|
onClick={(e) => e.stopPropagation()}
|
|
onKeyDown={(e) => e.stopPropagation()}
|
|
>
|
|
<Menu position="bottom-end" width={220} withinPortal>
|
|
<Menu.Target>
|
|
<ActionIcon
|
|
variant={variant === "table" ? "subtle" : "default"}
|
|
color="gray"
|
|
loading={mutations.isPending}
|
|
aria-label="Booking actions"
|
|
>
|
|
<MoreHorizontal size={16} />
|
|
</ActionIcon>
|
|
</Menu.Target>
|
|
<Menu.Dropdown>
|
|
<Menu.Label>
|
|
<Text size="xs" ff="monospace" c="dimmed">
|
|
{row.reference}
|
|
</Text>
|
|
</Menu.Label>
|
|
{actions.map((action) => {
|
|
const Icon = action.icon;
|
|
return (
|
|
<Menu.Item
|
|
key={action.id}
|
|
color={action.variant === "destructive" ? "red" : undefined}
|
|
leftSection={<Icon size={15} />}
|
|
onClick={() => handleAction(action)}
|
|
>
|
|
{action.label}
|
|
</Menu.Item>
|
|
);
|
|
})}
|
|
{actions.length > 0 && <Menu.Divider />}
|
|
<Menu.Item
|
|
leftSection={<ExternalLink size={15} />}
|
|
onClick={() => {
|
|
onSuppressRowClick?.();
|
|
navigate(`/dashboard/booking-requests/${row.id}`);
|
|
}}
|
|
>
|
|
Open full details
|
|
</Menu.Item>
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
|
|
<ActionDialog flow={flow} pendingAction={pendingAction} onSuppressRowClick={onSuppressRowClick} />
|
|
</Group>
|
|
);
|
|
}
|
|
|
|
function ActionDialog({
|
|
flow,
|
|
pendingAction,
|
|
onSuppressRowClick,
|
|
}: {
|
|
flow: ReturnType<typeof useBookingActionDialog>;
|
|
pendingAction: ReturnType<typeof useBookingActionDialog>["pendingAction"];
|
|
onSuppressRowClick?: () => void;
|
|
}) {
|
|
return (
|
|
<BookingConfirmDialog
|
|
open={flow.dialogOpen}
|
|
onOpenChange={(open) => {
|
|
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}
|
|
extra={
|
|
flow.detailLoading ? (
|
|
<Text size="sm" c="dimmed">
|
|
Loading approval steps…
|
|
</Text>
|
|
) : pendingAction?.id === "approve" &&
|
|
!getNextPendingApprovalStep(flow.mergedContext.approvalSteps) ? (
|
|
<Text
|
|
size="sm"
|
|
c="orange.9"
|
|
p="xs"
|
|
style={{
|
|
borderRadius: 8,
|
|
border: "1px solid var(--mantine-color-orange-2)",
|
|
background: "var(--mantine-color-orange-0)",
|
|
}}
|
|
>
|
|
No pending approval step. Refresh the page after staff accept, or reject the
|
|
booking.
|
|
</Text>
|
|
) : null
|
|
}
|
|
/>
|
|
);
|
|
}
|