booking flow,summtion, approval, contract, mock payemnt and integration to back office, and also add permissions

This commit is contained in:
marshal
2026-06-05 10:38:31 +03:00
parent 810bbc1168
commit d226d7ef22
94 changed files with 3509 additions and 1042 deletions

View File

@@ -4,12 +4,14 @@ import {
ExternalLink,
Loader2,
MoreHorizontal,
Upload,
} from "lucide-react";
import { BookingConfirmDialog } from "./BookingConfirmDialog";
import { useBookingActionDialog } from "./useBookingActionDialog";
import { useAuth } from "@/auth/useAuth";
import {
getNextPendingApprovalStep,
isContractNavAction,
listRowHasActions,
type BookingActionContext,
} from "@/features/bookings/booking-actions.config";
@@ -30,18 +32,23 @@ interface BookingActionsMenuProps {
/** Compact table cell vs. larger detail toolbar */
variant?: "table" | "toolbar";
className?: string;
/** Suppresses table row navigation after menu/dialog close (click-through). */
onSuppressRowClick?: () => void;
}
export function BookingActionsMenu({
row,
variant = "table",
className,
onSuppressRowClick,
}: BookingActionsMenuProps) {
const navigate = useNavigate();
const { user } = useAuth();
const context: BookingActionContext = {
status: row.status,
paymentCurrency: row.paymentCurrency,
reference: row.reference,
approvalSteps: row.approvalSteps,
};
const flow = useBookingActionDialog(row.id, context);
@@ -50,9 +57,7 @@ export function BookingActionsMenu({
const goToContract = () =>
navigate(`/dashboard/booking-requests/${row.id}/contract`);
const showUsdPaymentHint =
row.status === "FULLY_EXECUTED" && row.paymentCurrency === "USD";
const hasMenu = listRowHasActions(row) || showUsdPaymentHint;
const hasMenu = listRowHasActions(row, user);
const primary = actions.find((a) => a.primary) ?? actions[0];
@@ -73,8 +78,9 @@ export function BookingActionsMenu({
return (
<>
<div
data-stop-row-click
className={cn(
"flex items-center justify-end gap-1",
"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,
)}
@@ -87,7 +93,7 @@ export function BookingActionsMenu({
className="hidden h-8 gap-1.5 px-2.5 shadow-sm lg:inline-flex"
disabled={mutations.isPending}
onClick={() =>
primary.id === "viewContract"
isContractNavAction(primary.id)
? goToContract()
: flow.openAction(primary)
}
@@ -119,7 +125,7 @@ export function BookingActionsMenu({
)}
disabled={mutations.isPending}
onClick={() =>
action.id === "viewContract"
isContractNavAction(action.id)
? goToContract()
: flow.openAction(action)
}
@@ -164,36 +170,29 @@ export function BookingActionsMenu({
"gap-2 cursor-pointer",
action.variant === "destructive" && "text-red-700 focus:text-red-700",
)}
onClick={() =>
action.id === "viewContract"
? goToContract()
: flow.openAction(action)
}
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>
);
})}
{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 />
)}
{actions.length > 0 && <DropdownMenuSeparator />}
<DropdownMenuItem
className="gap-2 cursor-pointer"
onClick={() =>
navigate(`/dashboard/booking-requests/${row.id}`)
}
onSelect={(event) => {
event.preventDefault();
onSuppressRowClick?.();
navigate(`/dashboard/booking-requests/${row.id}`);
}}
>
<ExternalLink className="size-4 opacity-70" />
Open full details
@@ -205,12 +204,20 @@ export function BookingActionsMenu({
<BookingConfirmDialog
open={flow.dialogOpen}
onOpenChange={flow.setDialogOpen}
onOpenChange={(open) => {
if (!open) onSuppressRowClick?.();
flow.setDialogOpen(open);
}}
action={pendingAction}
reference={flow.mergedContext.reference}
inputValue={flow.inputValue}
onInputChange={flow.setInputValue}
onConfirm={flow.runAction}
selectedFile={flow.selectedFile}
onFileChange={flow.setSelectedFile}
onConfirm={() => {
onSuppressRowClick?.();
flow.runAction();
}}
isPending={mutations.isPending || flow.detailLoading}
confirmDisabled={flow.confirmDisabled}
extra={
@@ -220,10 +227,10 @@ export function BookingActionsMenu({
Loading approval steps
</p>
) : pendingAction?.id === "approve" &&
!flow.mergedContext.approvalSteps?.length ? (
!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 found. Accept the submission on the detail
page first.
No pending approval step. Refresh the page after staff accept, or
reject the booking.
</p>
) : null
}