mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-02 15:03:39 +00:00
135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
import { Loader2 } from "lucide-react";
|
|
|
|
import type { BookingActionDef } from "@/features/bookings/booking-actions.config";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
Button,
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
Textarea,
|
|
} from "@edr/ui-common";
|
|
|
|
interface BookingConfirmDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
action: BookingActionDef | null;
|
|
reference?: string;
|
|
inputValue: string;
|
|
onInputChange: (value: string) => void;
|
|
onConfirm: () => void;
|
|
isPending: boolean;
|
|
confirmDisabled?: boolean;
|
|
extra?: React.ReactNode;
|
|
}
|
|
|
|
export function BookingConfirmDialog({
|
|
open,
|
|
onOpenChange,
|
|
action,
|
|
reference,
|
|
inputValue,
|
|
onInputChange,
|
|
onConfirm,
|
|
isPending,
|
|
confirmDisabled = false,
|
|
extra,
|
|
}: BookingConfirmDialogProps) {
|
|
if (!action || !action.confirmTitle) return null;
|
|
|
|
const Icon = action.icon;
|
|
const needsInput = Boolean(action.input);
|
|
const inputMissing = needsInput && !inputValue.trim();
|
|
const isDestructive = action.variant === "destructive";
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="gap-0 overflow-hidden p-0 sm:max-w-md">
|
|
<div
|
|
className={cn(
|
|
"border-b px-6 py-5",
|
|
isDestructive
|
|
? "bg-gradient-to-br from-red-500/10 via-background to-background"
|
|
: "bg-gradient-to-br from-primary/8 via-background to-background",
|
|
)}
|
|
>
|
|
<DialogHeader className="gap-3 text-left">
|
|
<div className="flex items-start gap-3">
|
|
<div
|
|
className={cn(
|
|
"flex size-11 shrink-0 items-center justify-center rounded-xl shadow-sm",
|
|
isDestructive
|
|
? "bg-red-500/15 text-red-700 dark:text-red-300"
|
|
: "bg-primary/15 text-primary",
|
|
)}
|
|
>
|
|
<Icon className="size-5" />
|
|
</div>
|
|
<div className="min-w-0 space-y-1 pt-0.5">
|
|
<DialogTitle className="text-base leading-snug">
|
|
{action.confirmTitle}
|
|
</DialogTitle>
|
|
{reference && (
|
|
<p className="font-mono text-xs font-semibold text-muted-foreground">
|
|
{reference}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<DialogDescription className="text-left text-sm leading-relaxed">
|
|
{action.confirmDescription}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
</div>
|
|
|
|
<div className="space-y-4 px-6 py-5">
|
|
{needsInput && (
|
|
<div className="space-y-2">
|
|
<label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
{action.inputLabel}
|
|
<span className="text-red-600"> *</span>
|
|
</label>
|
|
<Textarea
|
|
value={inputValue}
|
|
onChange={(e) => onInputChange(e.target.value)}
|
|
placeholder={action.inputPlaceholder}
|
|
rows={4}
|
|
className="min-h-[100px] resize-y"
|
|
/>
|
|
</div>
|
|
)}
|
|
{extra}
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 border-t bg-muted/20 px-6 py-4 sm:justify-end">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
disabled={isPending}
|
|
onClick={() => onOpenChange(false)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant={isDestructive ? "destructive" : "default"}
|
|
disabled={isPending || inputMissing || confirmDisabled}
|
|
className="min-w-[7rem] gap-2"
|
|
onClick={onConfirm}
|
|
>
|
|
{isPending ? (
|
|
<Loader2 className="size-4 animate-spin" />
|
|
) : (
|
|
<Icon className="size-4" />
|
|
)}
|
|
{action.shortLabel}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|