mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 03:40:56 +00:00
234 lines
7.5 KiB
TypeScript
234 lines
7.5 KiB
TypeScript
import { useNavigate } from "react-router-dom";
|
|
import {
|
|
ChevronRight,
|
|
ExternalLink,
|
|
Loader2,
|
|
MoreHorizontal,
|
|
Upload,
|
|
} from "lucide-react";
|
|
|
|
import { BookingConfirmDialog } from "./BookingConfirmDialog";
|
|
import { useBookingActionDialog } from "./useBookingActionDialog";
|
|
import {
|
|
listRowHasActions,
|
|
type BookingActionContext,
|
|
} from "@/features/bookings/booking-actions.config";
|
|
import type { BookingListRow } from "@/types/booking";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
Button,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@edr/ui-common";
|
|
|
|
interface BookingActionsMenuProps {
|
|
row: BookingListRow;
|
|
/** Compact table cell vs. larger detail toolbar */
|
|
variant?: "table" | "toolbar";
|
|
className?: string;
|
|
}
|
|
|
|
export function BookingActionsMenu({
|
|
row,
|
|
variant = "table",
|
|
className,
|
|
}: BookingActionsMenuProps) {
|
|
const navigate = useNavigate();
|
|
const context: BookingActionContext = {
|
|
status: row.status,
|
|
paymentCurrency: row.paymentCurrency,
|
|
reference: row.reference,
|
|
};
|
|
|
|
const flow = useBookingActionDialog(row.id, context);
|
|
const { actions, pendingAction, mutations } = flow;
|
|
|
|
const goToContract = () =>
|
|
navigate(`/dashboard/booking-requests/${row.id}/contract`);
|
|
|
|
const showUsdPaymentHint =
|
|
row.status === "FULLY_EXECUTED" && row.paymentCurrency === "USD";
|
|
const hasMenu = listRowHasActions(row) || showUsdPaymentHint;
|
|
|
|
const primary = actions.find((a) => a.primary) ?? actions[0];
|
|
|
|
if (!hasMenu && variant === "table") {
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-8 text-muted-foreground hover:text-primary"
|
|
onClick={() => navigate(`/dashboard/booking-requests/${row.id}`)}
|
|
aria-label="View booking"
|
|
>
|
|
<ChevronRight className="size-4" />
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className={cn(
|
|
"flex items-center justify-end gap-1",
|
|
variant === "table" && "opacity-80 transition-opacity group-hover/tr:opacity-100",
|
|
className,
|
|
)}
|
|
onClick={(e) => e.stopPropagation()}
|
|
onKeyDown={(e) => e.stopPropagation()}
|
|
>
|
|
{variant === "table" && primary && (
|
|
<Button
|
|
size="sm"
|
|
className="hidden h-8 gap-1.5 px-2.5 shadow-sm lg:inline-flex"
|
|
disabled={mutations.isPending}
|
|
onClick={() =>
|
|
primary.id === "viewContract"
|
|
? goToContract()
|
|
: flow.openAction(primary)
|
|
}
|
|
>
|
|
<primary.icon className="size-3.5" />
|
|
{primary.shortLabel}
|
|
</Button>
|
|
)}
|
|
|
|
{variant === "toolbar" && actions.length > 0 ? (
|
|
<div className="flex w-full flex-wrap gap-2">
|
|
{actions.map((action) => {
|
|
const Icon = action.icon;
|
|
return (
|
|
<Button
|
|
key={action.id}
|
|
size="sm"
|
|
variant={
|
|
action.variant === "destructive"
|
|
? "outline"
|
|
: action.primary
|
|
? "default"
|
|
: "outline"
|
|
}
|
|
className={cn(
|
|
"gap-2 shadow-sm",
|
|
action.variant === "destructive" &&
|
|
"border-red-200 text-red-700 hover:bg-red-50 dark:hover:bg-red-950/30",
|
|
)}
|
|
disabled={mutations.isPending}
|
|
onClick={() =>
|
|
action.id === "viewContract"
|
|
? goToContract()
|
|
: flow.openAction(action)
|
|
}
|
|
>
|
|
<Icon className="size-4" />
|
|
{action.label}
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant={variant === "table" ? "ghost" : "outline"}
|
|
size={variant === "table" ? "icon" : "sm"}
|
|
className={cn(
|
|
variant === "table" ? "size-8" : "gap-2",
|
|
"shrink-0",
|
|
)}
|
|
disabled={mutations.isPending}
|
|
aria-label="Booking actions"
|
|
>
|
|
{mutations.isPending ? (
|
|
<Loader2 className="size-4 animate-spin" />
|
|
) : (
|
|
<MoreHorizontal className="size-4" />
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
<DropdownMenuLabel className="font-mono text-xs text-muted-foreground">
|
|
{row.reference}
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
{actions.map((action) => {
|
|
const Icon = action.icon;
|
|
return (
|
|
<DropdownMenuItem
|
|
key={action.id}
|
|
className={cn(
|
|
"gap-2 cursor-pointer",
|
|
action.variant === "destructive" && "text-red-700 focus:text-red-700",
|
|
)}
|
|
onClick={() =>
|
|
action.id === "viewContract"
|
|
? goToContract()
|
|
: flow.openAction(action)
|
|
}
|
|
>
|
|
<Icon className="size-4 opacity-70" />
|
|
<span>{action.label}</span>
|
|
</DropdownMenuItem>
|
|
);
|
|
})}
|
|
{showUsdPaymentHint && (
|
|
<DropdownMenuItem
|
|
className="gap-2 cursor-pointer"
|
|
onClick={() =>
|
|
navigate(`/dashboard/booking-requests/${row.id}`)
|
|
}
|
|
>
|
|
<Upload className="size-4 opacity-70" />
|
|
Upload payment proof…
|
|
</DropdownMenuItem>
|
|
)}
|
|
{(actions.length > 0 || showUsdPaymentHint) && (
|
|
<DropdownMenuSeparator />
|
|
)}
|
|
<DropdownMenuItem
|
|
className="gap-2 cursor-pointer"
|
|
onClick={() =>
|
|
navigate(`/dashboard/booking-requests/${row.id}`)
|
|
}
|
|
>
|
|
<ExternalLink className="size-4 opacity-70" />
|
|
Open full details
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
|
|
<BookingConfirmDialog
|
|
open={flow.dialogOpen}
|
|
onOpenChange={flow.setDialogOpen}
|
|
action={pendingAction}
|
|
reference={flow.mergedContext.reference}
|
|
inputValue={flow.inputValue}
|
|
onInputChange={flow.setInputValue}
|
|
onConfirm={flow.runAction}
|
|
isPending={mutations.isPending || flow.detailLoading}
|
|
confirmDisabled={flow.confirmDisabled}
|
|
extra={
|
|
flow.detailLoading ? (
|
|
<p className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Loader2 className="size-4 animate-spin" />
|
|
Loading approval steps…
|
|
</p>
|
|
) : pendingAction?.id === "approve" &&
|
|
!flow.mergedContext.approvalSteps?.length ? (
|
|
<p className="rounded-lg border border-amber-200/80 bg-amber-50/50 px-3 py-2 text-sm text-amber-900 dark:bg-amber-950/30 dark:text-amber-200">
|
|
No pending approval step found. Accept the submission on the detail
|
|
page first.
|
|
</p>
|
|
) : null
|
|
}
|
|
/>
|
|
</>
|
|
);
|
|
}
|