mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 17:45:42 +00:00
ui improvemnt
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
ChevronRight,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
} from "lucide-react";
|
||||
import { ChevronRight, ExternalLink, MoreHorizontal } from "lucide-react";
|
||||
import { Button, Menu, ActionIcon, Group, Text } from "@mantine/core";
|
||||
|
||||
import { BookingConfirmDialog } from "./BookingConfirmDialog";
|
||||
import { useBookingActionDialog } from "./useBookingActionDialog";
|
||||
@@ -16,16 +12,6 @@ import {
|
||||
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;
|
||||
@@ -39,7 +25,6 @@ interface BookingActionsMenuProps {
|
||||
export function BookingActionsMenu({
|
||||
row,
|
||||
variant = "table",
|
||||
className,
|
||||
onSuppressRowClick,
|
||||
}: BookingActionsMenuProps) {
|
||||
const navigate = useNavigate();
|
||||
@@ -57,184 +42,179 @@ export function BookingActionsMenu({
|
||||
const goToContract = () =>
|
||||
navigate(`/dashboard/booking-requests/${row.id}/contract`);
|
||||
|
||||
const hasMenu = listRowHasActions(row, user);
|
||||
const handleAction = (action: (typeof actions)[number]) => {
|
||||
onSuppressRowClick?.();
|
||||
if (isContractNavAction(action.id)) {
|
||||
goToContract();
|
||||
} else {
|
||||
flow.openAction(action);
|
||||
}
|
||||
};
|
||||
|
||||
const hasMenu = listRowHasActions(row, user);
|
||||
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"
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={() => navigate(`/dashboard/booking-requests/${row.id}`)}
|
||||
aria-label="View booking"
|
||||
>
|
||||
<ChevronRight className="size-4" />
|
||||
</Button>
|
||||
<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 ? "green" : "gray"}
|
||||
leftSection={<Icon size={16} />}
|
||||
disabled={mutations.isPending}
|
||||
onClick={() => handleAction(action)}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
<ActionDialog flow={flow} pendingAction={pendingAction} onSuppressRowClick={onSuppressRowClick} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
data-stop-row-click
|
||||
className={cn(
|
||||
"flex w-full min-h-[2.5rem] 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={() =>
|
||||
isContractNavAction(primary.id)
|
||||
? goToContract()
|
||||
: flow.openAction(primary)
|
||||
}
|
||||
<Group
|
||||
gap={4}
|
||||
justify="flex-end"
|
||||
wrap="nowrap"
|
||||
data-stop-row-click
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{variant === "table" && primary && (
|
||||
<Button
|
||||
size="compact-sm"
|
||||
color="green"
|
||||
visibleFrom="lg"
|
||||
leftSection={<primary.icon size={14} />}
|
||||
disabled={mutations.isPending}
|
||||
onClick={() => handleAction(primary)}
|
||||
>
|
||||
{primary.shortLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Menu position="bottom-end" width={220} withinPortal>
|
||||
<Menu.Target>
|
||||
<ActionIcon
|
||||
variant={variant === "table" ? "subtle" : "default"}
|
||||
color="gray"
|
||||
loading={mutations.isPending}
|
||||
aria-label="Booking actions"
|
||||
>
|
||||
<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={() =>
|
||||
isContractNavAction(action.id)
|
||||
? 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">
|
||||
<MoreHorizontal size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>
|
||||
<Text size="xs" ff="monospace" c="dimmed">
|
||||
{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",
|
||||
)}
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
onSuppressRowClick?.();
|
||||
if (isContractNavAction(action.id)) {
|
||||
goToContract();
|
||||
} else {
|
||||
flow.openAction(action);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon className="size-4 opacity-70" />
|
||||
<span>{action.label}</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
{actions.length > 0 && <DropdownMenuSeparator />}
|
||||
<DropdownMenuItem
|
||||
className="gap-2 cursor-pointer"
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
onSuppressRowClick?.();
|
||||
navigate(`/dashboard/booking-requests/${row.id}`);
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="size-4 opacity-70" />
|
||||
Open full details
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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={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" &&
|
||||
!getNextPendingApprovalStep(flow.mergedContext.approvalSteps) ? (
|
||||
<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. Refresh the page after staff accept, or
|
||||
reject the booking.
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</>
|
||||
<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
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user