@voyantjs/bookings-ui 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/components/booking-activity-timeline.d.ts +5 -0
- package/dist/components/booking-activity-timeline.d.ts.map +1 -0
- package/dist/components/booking-activity-timeline.js +83 -0
- package/dist/components/booking-cancellation-dialog.d.ts +18 -0
- package/dist/components/booking-cancellation-dialog.d.ts.map +1 -0
- package/dist/components/booking-cancellation-dialog.js +80 -0
- package/dist/components/booking-create-dialog.d.ts +21 -0
- package/dist/components/booking-create-dialog.d.ts.map +1 -0
- package/dist/components/booking-create-dialog.js +313 -0
- package/dist/components/booking-dialog.d.ts +23 -0
- package/dist/components/booking-dialog.d.ts.map +1 -0
- package/dist/components/booking-dialog.js +108 -0
- package/dist/components/booking-document-dialog.d.ts +8 -0
- package/dist/components/booking-document-dialog.d.ts.map +1 -0
- package/dist/components/booking-document-dialog.js +67 -0
- package/dist/components/booking-document-list.d.ts +5 -0
- package/dist/components/booking-document-list.d.ts.map +1 -0
- package/dist/components/booking-document-list.js +38 -0
- package/dist/components/booking-group-link-dialog.d.ts +10 -0
- package/dist/components/booking-group-link-dialog.d.ts.map +1 -0
- package/dist/components/booking-group-link-dialog.js +68 -0
- package/dist/components/booking-group-section.d.ts +17 -0
- package/dist/components/booking-group-section.d.ts.map +1 -0
- package/dist/components/booking-group-section.js +31 -0
- package/dist/components/booking-guarantee-dialog.d.ts +10 -0
- package/dist/components/booking-guarantee-dialog.d.ts.map +1 -0
- package/dist/components/booking-guarantee-dialog.js +101 -0
- package/dist/components/booking-guarantee-list.d.ts +5 -0
- package/dist/components/booking-guarantee-list.d.ts.map +1 -0
- package/dist/components/booking-guarantee-list.js +45 -0
- package/dist/components/booking-item-dialog.d.ts +10 -0
- package/dist/components/booking-item-dialog.d.ts.map +1 -0
- package/dist/components/booking-item-dialog.js +119 -0
- package/dist/components/booking-item-list.d.ts +5 -0
- package/dist/components/booking-item-list.d.ts.map +1 -0
- package/dist/components/booking-item-list.js +50 -0
- package/dist/components/booking-item-travelers.d.ts +6 -0
- package/dist/components/booking-item-travelers.d.ts.map +1 -0
- package/dist/components/booking-item-travelers.js +50 -0
- package/dist/components/booking-list.d.ts +7 -0
- package/dist/components/booking-list.d.ts.map +1 -0
- package/dist/components/booking-list.js +47 -0
- package/dist/components/booking-notes.d.ts +5 -0
- package/dist/components/booking-notes.d.ts.map +1 -0
- package/dist/components/booking-notes.js +16 -0
- package/dist/components/booking-payment-schedule-dialog.d.ts +10 -0
- package/dist/components/booking-payment-schedule-dialog.d.ts.map +1 -0
- package/dist/components/booking-payment-schedule-dialog.js +77 -0
- package/dist/components/booking-payment-schedule-list.d.ts +5 -0
- package/dist/components/booking-payment-schedule-list.d.ts.map +1 -0
- package/dist/components/booking-payment-schedule-list.js +43 -0
- package/dist/components/booking-payments-summary.d.ts +5 -0
- package/dist/components/booking-payments-summary.d.ts.map +1 -0
- package/dist/components/booking-payments-summary.js +19 -0
- package/dist/components/file-dropzone.d.ts +25 -0
- package/dist/components/file-dropzone.d.ts.map +1 -0
- package/dist/components/file-dropzone.js +92 -0
- package/dist/components/passengers-section.d.ts +72 -0
- package/dist/components/passengers-section.d.ts.map +1 -0
- package/dist/components/passengers-section.js +74 -0
- package/dist/components/payment-schedule-section.d.ts +62 -0
- package/dist/components/payment-schedule-section.d.ts.map +1 -0
- package/dist/components/payment-schedule-section.js +88 -0
- package/dist/components/person-picker-section.d.ts +53 -0
- package/dist/components/person-picker-section.d.ts.map +1 -0
- package/dist/components/person-picker-section.js +71 -0
- package/dist/components/price-breakdown-section.d.ts +48 -0
- package/dist/components/price-breakdown-section.d.ts.map +1 -0
- package/dist/components/price-breakdown-section.js +165 -0
- package/dist/components/product-picker-section.d.ts +27 -0
- package/dist/components/product-picker-section.d.ts.map +1 -0
- package/dist/components/product-picker-section.js +41 -0
- package/dist/components/rooms-stepper-section.d.ts +45 -0
- package/dist/components/rooms-stepper-section.d.ts.map +1 -0
- package/dist/components/rooms-stepper-section.js +60 -0
- package/dist/components/shared-room-section.d.ts +37 -0
- package/dist/components/shared-room-section.d.ts.map +1 -0
- package/dist/components/shared-room-section.js +40 -0
- package/dist/components/status-change-dialog.d.ts +10 -0
- package/dist/components/status-change-dialog.d.ts.map +1 -0
- package/dist/components/status-change-dialog.js +41 -0
- package/dist/components/supplier-status-dialog.d.ts +10 -0
- package/dist/components/supplier-status-dialog.d.ts.map +1 -0
- package/dist/components/supplier-status-dialog.js +77 -0
- package/dist/components/supplier-status-list.d.ts +5 -0
- package/dist/components/supplier-status-list.d.ts.map +1 -0
- package/dist/components/supplier-status-list.js +33 -0
- package/dist/components/traveler-dialog.d.ts +10 -0
- package/dist/components/traveler-dialog.d.ts.map +1 -0
- package/dist/components/traveler-dialog.js +64 -0
- package/dist/components/traveler-list.d.ts +5 -0
- package/dist/components/traveler-list.d.ts.map +1 -0
- package/dist/components/traveler-list.js +32 -0
- package/dist/components/voucher-picker-section.d.ts +50 -0
- package/dist/components/voucher-picker-section.d.ts.map +1 -0
- package/dist/components/voucher-picker-section.js +94 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/package.json +76 -0
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @voyantjs/bookings-ui
|
|
2
|
+
|
|
3
|
+
Importable React UI components for Voyant bookings. Bundler-consumed (Vite, Next.js, webpack, etc.).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @voyantjs/bookings-ui @voyantjs/bookings-react @voyantjs/voyant-ui @tanstack/react-query react react-dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@voyantjs/voyant-ui` provides the design-system primitives. `@voyantjs/bookings-react` provides the data-layer hooks. Both are required peers.
|
|
12
|
+
|
|
13
|
+
All components accept a `className` prop and merge it with `cn()`. Wrap or compose to extend; use the registry copy-paste path (`npx shadcn add @voyant/...`) for components you want to fork outright.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-activity-timeline.d.ts","sourceRoot":"","sources":["../../src/components/booking-activity-timeline.tsx"],"names":[],"mappings":"AA0BA,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,MAAM,CAAA;CAClB;AA+CD,wBAAgB,uBAAuB,CAAC,EAAE,SAAS,EAAE,EAAE,4BAA4B,2CAqFlF"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useBookingActivity, useBookingTravelerDocuments } from "@voyantjs/bookings-react";
|
|
4
|
+
import { usePublicBookingPayments } from "@voyantjs/finance-react";
|
|
5
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, } from "@voyantjs/voyant-ui/components";
|
|
6
|
+
import { Activity, Clock, CreditCard, ExternalLink, FileText, Pencil, Plus, RefreshCw, UserPlus, } from "lucide-react";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
const activityIcons = {
|
|
9
|
+
booking_created: Plus,
|
|
10
|
+
booking_reserved: Plus,
|
|
11
|
+
booking_converted: RefreshCw,
|
|
12
|
+
booking_confirmed: Clock,
|
|
13
|
+
hold_extended: Clock,
|
|
14
|
+
hold_expired: Clock,
|
|
15
|
+
status_change: Clock,
|
|
16
|
+
item_update: Pencil,
|
|
17
|
+
allocation_released: Clock,
|
|
18
|
+
fulfillment_issued: FileText,
|
|
19
|
+
fulfillment_updated: FileText,
|
|
20
|
+
redemption_recorded: FileText,
|
|
21
|
+
supplier_update: RefreshCw,
|
|
22
|
+
passenger_update: UserPlus,
|
|
23
|
+
note_added: Pencil,
|
|
24
|
+
};
|
|
25
|
+
const sourceLabel = {
|
|
26
|
+
activity: "Activity",
|
|
27
|
+
document: "Document",
|
|
28
|
+
payment: "Payment",
|
|
29
|
+
};
|
|
30
|
+
const sourceVariant = {
|
|
31
|
+
activity: "outline",
|
|
32
|
+
document: "secondary",
|
|
33
|
+
payment: "default",
|
|
34
|
+
};
|
|
35
|
+
export function BookingActivityTimeline({ bookingId }) {
|
|
36
|
+
const [filter, setFilter] = React.useState("all");
|
|
37
|
+
const { data: activityData } = useBookingActivity(bookingId);
|
|
38
|
+
const { data: documentsData } = useBookingTravelerDocuments(bookingId);
|
|
39
|
+
const { data: paymentsData } = usePublicBookingPayments(bookingId);
|
|
40
|
+
const events = React.useMemo(() => {
|
|
41
|
+
const merged = [];
|
|
42
|
+
for (const entry of activityData?.data ?? []) {
|
|
43
|
+
merged.push({
|
|
44
|
+
id: `activity:${entry.id}`,
|
|
45
|
+
source: "activity",
|
|
46
|
+
title: entry.description,
|
|
47
|
+
actorId: entry.actorId,
|
|
48
|
+
timestamp: entry.createdAt,
|
|
49
|
+
icon: activityIcons[entry.activityType] ?? Activity,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
for (const doc of documentsData?.data ?? []) {
|
|
53
|
+
merged.push({
|
|
54
|
+
id: `document:${doc.id}`,
|
|
55
|
+
source: "document",
|
|
56
|
+
title: `${doc.type.replace(/_/g, " ")} uploaded`,
|
|
57
|
+
description: doc.fileName,
|
|
58
|
+
timestamp: doc.createdAt,
|
|
59
|
+
icon: FileText,
|
|
60
|
+
link: { href: doc.fileUrl, label: "View file" },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
for (const payment of paymentsData?.data?.payments ?? []) {
|
|
64
|
+
merged.push({
|
|
65
|
+
id: `payment:${payment.id}`,
|
|
66
|
+
source: "payment",
|
|
67
|
+
title: `Payment ${payment.status} — ${(payment.amountCents / 100).toFixed(2)} ${payment.currency}`,
|
|
68
|
+
description: `Invoice ${payment.invoiceNumber} · ${payment.paymentMethod.replace(/_/g, " ")}`,
|
|
69
|
+
timestamp: payment.paymentDate,
|
|
70
|
+
icon: CreditCard,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
merged.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
74
|
+
return merged;
|
|
75
|
+
}, [activityData, documentsData, paymentsData]);
|
|
76
|
+
const visible = filter === "all" ? events : events.filter((e) => e.source === filter);
|
|
77
|
+
const filterChips = ["all", "activity", "document", "payment"];
|
|
78
|
+
return (_jsxs(Card, { "data-slot": "booking-activity-timeline", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(Activity, { className: "h-4 w-4" }), "Activity Timeline"] }), _jsx("div", { className: "flex items-center gap-1", children: filterChips.map((chip) => (_jsx(Button, { variant: filter === chip ? "default" : "ghost", size: "sm", className: "h-7 capitalize", onClick: () => setFilter(chip), children: chip === "all" ? "All" : sourceLabel[chip] }, chip))) })] }), _jsx(CardContent, { children: visible.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: "No events yet." })) : (_jsx("div", { className: "flex flex-col gap-3", children: visible.map((event) => (_jsx(TimelineEventItem, { event: event }, event.id))) })) })] }));
|
|
79
|
+
}
|
|
80
|
+
function TimelineEventItem({ event }) {
|
|
81
|
+
const Icon = event.icon;
|
|
82
|
+
return (_jsxs("div", { className: "flex items-start gap-3 rounded-md border p-3", children: [_jsx(Icon, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("p", { className: "text-sm font-medium capitalize", children: event.title }), _jsx(Badge, { variant: sourceVariant[event.source], className: "text-xs", children: sourceLabel[event.source] })] }), event.description && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: event.description })), _jsxs("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [event.actorId && event.actorId !== "system" ? `By ${event.actorId} · ` : "", new Date(event.timestamp).toLocaleString()] }), event.link && (_jsxs("a", { href: event.link.href, target: "_blank", rel: "noopener noreferrer", className: "mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground hover:underline", children: [event.link.label, _jsx(ExternalLink, { className: "h-3 w-3" })] }))] })] }));
|
|
83
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type BookingRecord } from "@voyantjs/bookings-react";
|
|
2
|
+
export interface BookingCancellationDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
booking: BookingRecord;
|
|
6
|
+
/**
|
|
7
|
+
* Product ID used to resolve the applicable cancellation policy.
|
|
8
|
+
*
|
|
9
|
+
* Leave unset (or pass `undefined`) to auto-resolve from the booking's items
|
|
10
|
+
* — this is what you want for single-product bookings. Pass an explicit
|
|
11
|
+
* string or `null` to override (e.g. for multi-product bookings or to force
|
|
12
|
+
* the default non-product-scoped policy).
|
|
13
|
+
*/
|
|
14
|
+
productId?: string | null;
|
|
15
|
+
onSuccess?: () => void;
|
|
16
|
+
}
|
|
17
|
+
export declare function BookingCancellationDialog({ open, onOpenChange, booking, productId, onSuccess, }: BookingCancellationDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=booking-cancellation-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-cancellation-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-cancellation-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAGnB,MAAM,0BAA0B,CAAA;AA4CjC,MAAM,WAAW,8BAA8B;IAC7C,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,OAAO,EAAE,aAAa,CAAA;IACtB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,wBAAgB,yBAAyB,CAAC,EACxC,IAAI,EACJ,YAAY,EACZ,OAAO,EACP,SAAS,EACT,SAAS,GACV,EAAE,8BAA8B,2CAwMhC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useBookingCancelMutation, useBookingPrimaryProduct, } from "@voyantjs/bookings-react";
|
|
4
|
+
import { useEvaluateCancellation, useResolvePolicy } from "@voyantjs/legal-react";
|
|
5
|
+
import { Badge, Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Label, Textarea, } from "@voyantjs/voyant-ui/components";
|
|
6
|
+
import { AlertTriangle, Loader2 } from "lucide-react";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
function formatAmount(cents, currency) {
|
|
9
|
+
return `${(cents / 100).toFixed(2)} ${currency}`;
|
|
10
|
+
}
|
|
11
|
+
function formatPercent(basisPoints) {
|
|
12
|
+
return `${(basisPoints / 100).toFixed(0)}%`;
|
|
13
|
+
}
|
|
14
|
+
function daysBetween(from, to) {
|
|
15
|
+
const diffMs = to.getTime() - from.getTime();
|
|
16
|
+
return Math.max(0, Math.floor(diffMs / (1000 * 60 * 60 * 24)));
|
|
17
|
+
}
|
|
18
|
+
const refundTypeLabel = {
|
|
19
|
+
cash: "Cash refund",
|
|
20
|
+
credit: "Credit",
|
|
21
|
+
cash_or_credit: "Cash or credit",
|
|
22
|
+
none: "No refund",
|
|
23
|
+
};
|
|
24
|
+
const refundTypeVariant = {
|
|
25
|
+
cash: "default",
|
|
26
|
+
credit: "secondary",
|
|
27
|
+
cash_or_credit: "secondary",
|
|
28
|
+
none: "destructive",
|
|
29
|
+
};
|
|
30
|
+
export function BookingCancellationDialog({ open, onOpenChange, booking, productId, onSuccess, }) {
|
|
31
|
+
const [reason, setReason] = React.useState("");
|
|
32
|
+
const daysBeforeDeparture = React.useMemo(() => {
|
|
33
|
+
if (!booking.startDate)
|
|
34
|
+
return 0;
|
|
35
|
+
return daysBetween(new Date(), new Date(booking.startDate));
|
|
36
|
+
}, [booking.startDate]);
|
|
37
|
+
// When the caller didn't specify a productId, derive one from the booking's
|
|
38
|
+
// items so the consumer doesn't have to wire up `useBookingItems` just for
|
|
39
|
+
// this. Explicit `null` is respected as an override.
|
|
40
|
+
const shouldAutoResolveProduct = productId === undefined;
|
|
41
|
+
const autoResolved = useBookingPrimaryProduct(booking.id, {
|
|
42
|
+
enabled: open && shouldAutoResolveProduct,
|
|
43
|
+
});
|
|
44
|
+
const effectiveProductId = shouldAutoResolveProduct ? autoResolved.productId : productId;
|
|
45
|
+
const { data: resolved, isLoading: resolveLoading } = useResolvePolicy({ kind: "cancellation", productId: effectiveProductId ?? undefined }, { enabled: open });
|
|
46
|
+
const policy = resolved?.data;
|
|
47
|
+
const evalInput = React.useMemo(() => {
|
|
48
|
+
if (booking.sellAmountCents == null)
|
|
49
|
+
return null;
|
|
50
|
+
return {
|
|
51
|
+
daysBeforeDeparture,
|
|
52
|
+
totalCents: booking.sellAmountCents,
|
|
53
|
+
currency: booking.sellCurrency,
|
|
54
|
+
};
|
|
55
|
+
}, [daysBeforeDeparture, booking.sellAmountCents, booking.sellCurrency]);
|
|
56
|
+
const { data: evaluationData, isFetching: evaluationLoading } = useEvaluateCancellation(policy?.policy.id ?? null, evalInput, { enabled: open && Boolean(policy) });
|
|
57
|
+
const evaluation = evaluationData?.data ?? null;
|
|
58
|
+
const cancelMutation = useBookingCancelMutation(booking.id);
|
|
59
|
+
React.useEffect(() => {
|
|
60
|
+
if (!open) {
|
|
61
|
+
setReason("");
|
|
62
|
+
}
|
|
63
|
+
}, [open]);
|
|
64
|
+
const handleConfirm = async () => {
|
|
65
|
+
if (!reason.trim())
|
|
66
|
+
return;
|
|
67
|
+
await cancelMutation.mutateAsync({ note: reason.trim() });
|
|
68
|
+
onOpenChange(false);
|
|
69
|
+
onSuccess?.();
|
|
70
|
+
};
|
|
71
|
+
const total = booking.sellAmountCents;
|
|
72
|
+
const refund = evaluation?.refundCents ?? 0;
|
|
73
|
+
const penalty = total != null ? Math.max(0, total - refund) : 0;
|
|
74
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(AlertTriangle, { className: "h-4 w-4 text-destructive" }), "Cancel Booking"] }) }), _jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4 rounded-md border bg-muted/30 p-3 text-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Booking" }), _jsx("div", { className: "font-mono text-xs", children: booking.bookingNumber })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Start date" }), _jsx("div", { children: booking.startDate ?? "TBD" })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Total" }), _jsx("div", { className: "font-mono", children: total != null ? formatAmount(total, booking.sellCurrency) : "—" })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Days before departure" }), _jsx("div", { children: daysBeforeDeparture })] })] }), resolveLoading ? (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), "Resolving cancellation policy..."] })) : !policy ? (_jsx("div", { className: "rounded-md border border-dashed p-3 text-sm text-muted-foreground", children: "No cancellation policy configured for this booking. Proceeding will cancel without a refund preview." })) : (_jsxs("div", { className: "space-y-2 rounded-md border p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Applicable policy" }), _jsx("div", { className: "text-sm font-medium", children: policy.policy.name })] }), evaluation && (_jsx(Badge, { variant: refundTypeVariant[evaluation.refundType] ?? "secondary", children: refundTypeLabel[evaluation.refundType] ?? evaluation.refundType }))] }), evaluationLoading ? (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), "Calculating refund..."] })) : evaluation && total != null ? (_jsxs("div", { className: "grid grid-cols-3 gap-3 border-t pt-3 text-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Refund" }), _jsx("div", { className: "font-mono font-medium", children: formatAmount(evaluation.refundCents, booking.sellCurrency) }), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["(", formatPercent(evaluation.refundPercent), ")"] })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Penalty" }), _jsx("div", { className: "font-mono font-medium text-destructive", children: formatAmount(penalty, booking.sellCurrency) })] }), _jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Rule" }), _jsx("div", { className: "text-xs", children: evaluation.appliedRule?.label ??
|
|
75
|
+
(evaluation.appliedRule?.daysBeforeDeparture != null
|
|
76
|
+
? `≥ ${evaluation.appliedRule.daysBeforeDeparture} days`
|
|
77
|
+
: "—") })] })] })) : total == null ? (_jsx("p", { className: "border-t pt-3 text-sm text-muted-foreground", children: "Booking has no total amount \u2014 refund cannot be calculated." })) : null] })), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs(Label, { children: ["Reason ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx(Textarea, { value: reason, onChange: (e) => setReason(e.target.value), placeholder: "Why is this booking being cancelled?", required: true })] }), cancelMutation.error && (_jsx("p", { className: "text-xs text-destructive", children: cancelMutation.error instanceof Error
|
|
78
|
+
? cancelMutation.error.message
|
|
79
|
+
: "Cancellation failed" }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), disabled: cancelMutation.isPending, children: "Close" }), _jsxs(Button, { type: "button", variant: "destructive", size: "sm", onClick: handleConfirm, disabled: !reason.trim() || cancelMutation.isPending, children: [cancelMutation.isPending && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "Confirm Cancellation"] })] })] }) }));
|
|
80
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type BookingRecord } from "@voyantjs/bookings-react";
|
|
2
|
+
export interface BookingCreateDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
onCreated?: (booking: BookingRecord) => void;
|
|
6
|
+
/** When provided, pre-selects this product and hides the product picker. */
|
|
7
|
+
defaultProductId?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Operator booking-create dialog. Composes the booking-create picker
|
|
11
|
+
* sections — product, departure, rooms, person, shared-room, passengers,
|
|
12
|
+
* price breakdown, voucher, payment schedule — and submits via the atomic
|
|
13
|
+
* `POST /v1/bookings/quick-create` endpoint so partial failures can't
|
|
14
|
+
* leave orphan state.
|
|
15
|
+
*
|
|
16
|
+
* Normally consumed via `BookingDialog` which delegates here when no
|
|
17
|
+
* `booking` prop is passed. Apps that need a bespoke flow can install the
|
|
18
|
+
* sections individually and assemble their own dialog instead of forking.
|
|
19
|
+
*/
|
|
20
|
+
export declare function BookingCreateDialog({ open, onOpenChange, onCreated, defaultProductId, }: BookingCreateDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
//# sourceMappingURL=booking-create-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-create-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-create-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,aAAa,EAOnB,MAAM,0BAA0B,CAAA;AAqJjC,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,gBAAgB,GACjB,EAAE,wBAAwB,2CAoW1B"}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useSlots, useSlotUnitAvailability } from "@voyantjs/availability-react";
|
|
4
|
+
import { useBookingQuickCreateMutation, useBookingStatusByIdMutation, } from "@voyantjs/bookings-react";
|
|
5
|
+
import { usePersonMutation } from "@voyantjs/crm-react";
|
|
6
|
+
import { Button, Checkbox, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/voyant-ui/components";
|
|
7
|
+
import { Loader2 } from "lucide-react";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import { emptyPassengerListValue, PassengersSection, } from "./passengers-section";
|
|
10
|
+
import { emptyPaymentScheduleValue, PaymentScheduleSection, } from "./payment-schedule-section";
|
|
11
|
+
import { emptyPersonPickerValue, PersonPickerSection, } from "./person-picker-section";
|
|
12
|
+
import { PriceBreakdownSection } from "./price-breakdown-section";
|
|
13
|
+
import { ProductPickerSection } from "./product-picker-section";
|
|
14
|
+
import { emptyRoomsStepperValue, RoomsStepperSection, } from "./rooms-stepper-section";
|
|
15
|
+
import { emptySharedRoomValue, SharedRoomSection, } from "./shared-room-section";
|
|
16
|
+
import { emptyVoucherPickerValue, VoucherPickerSection, } from "./voucher-picker-section";
|
|
17
|
+
function generateBookingNumber() {
|
|
18
|
+
const now = new Date();
|
|
19
|
+
const y = now.getFullYear().toString().slice(-2);
|
|
20
|
+
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
21
|
+
const seq = String(Math.floor(Math.random() * 9000) + 1000);
|
|
22
|
+
return `BK-${y}${m}-${seq}`;
|
|
23
|
+
}
|
|
24
|
+
function paymentScheduleToRows(value, currency, totalAmountCents) {
|
|
25
|
+
if (value.mode === "unpaid")
|
|
26
|
+
return [];
|
|
27
|
+
if (value.mode === "full") {
|
|
28
|
+
if (!value.fullDueDate || totalAmountCents === null)
|
|
29
|
+
return [];
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
scheduleType: "balance",
|
|
33
|
+
status: "due",
|
|
34
|
+
dueDate: value.fullDueDate,
|
|
35
|
+
currency,
|
|
36
|
+
amountCents: totalAmountCents,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
if (value.mode === "advance") {
|
|
41
|
+
if (!value.advanceDueDate || value.advanceAmountCents == null)
|
|
42
|
+
return [];
|
|
43
|
+
const rows = [
|
|
44
|
+
{
|
|
45
|
+
scheduleType: "deposit",
|
|
46
|
+
status: "due",
|
|
47
|
+
dueDate: value.advanceDueDate,
|
|
48
|
+
currency,
|
|
49
|
+
amountCents: value.advanceAmountCents,
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
if (totalAmountCents !== null && totalAmountCents > value.advanceAmountCents) {
|
|
53
|
+
rows.push({
|
|
54
|
+
scheduleType: "balance",
|
|
55
|
+
status: "pending",
|
|
56
|
+
dueDate: value.advanceDueDate,
|
|
57
|
+
currency,
|
|
58
|
+
amountCents: totalAmountCents - value.advanceAmountCents,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return rows;
|
|
62
|
+
}
|
|
63
|
+
// split
|
|
64
|
+
const rows = [];
|
|
65
|
+
if (value.splitFirstDueDate && value.splitFirstAmountCents != null) {
|
|
66
|
+
rows.push({
|
|
67
|
+
scheduleType: "installment",
|
|
68
|
+
status: "due",
|
|
69
|
+
dueDate: value.splitFirstDueDate,
|
|
70
|
+
currency,
|
|
71
|
+
amountCents: value.splitFirstAmountCents,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (value.splitSecondDueDate && value.splitSecondAmountCents != null) {
|
|
75
|
+
rows.push({
|
|
76
|
+
scheduleType: "installment",
|
|
77
|
+
status: "pending",
|
|
78
|
+
dueDate: value.splitSecondDueDate,
|
|
79
|
+
currency,
|
|
80
|
+
amountCents: value.splitSecondAmountCents,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return rows;
|
|
84
|
+
}
|
|
85
|
+
function passengersToRows(value) {
|
|
86
|
+
return value.passengers.map((p) => ({
|
|
87
|
+
firstName: p.firstName.trim(),
|
|
88
|
+
lastName: p.lastName.trim(),
|
|
89
|
+
email: p.email.trim() || null,
|
|
90
|
+
participantType: "traveler",
|
|
91
|
+
travelerCategory: p.role === "child"
|
|
92
|
+
? "child"
|
|
93
|
+
: p.role === "infant"
|
|
94
|
+
? "infant"
|
|
95
|
+
: p.role === "adult"
|
|
96
|
+
? "adult"
|
|
97
|
+
: null,
|
|
98
|
+
isPrimary: p.role === "lead",
|
|
99
|
+
roomUnitId: p.roomUnitId,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Operator booking-create dialog. Composes the booking-create picker
|
|
104
|
+
* sections — product, departure, rooms, person, shared-room, passengers,
|
|
105
|
+
* price breakdown, voucher, payment schedule — and submits via the atomic
|
|
106
|
+
* `POST /v1/bookings/quick-create` endpoint so partial failures can't
|
|
107
|
+
* leave orphan state.
|
|
108
|
+
*
|
|
109
|
+
* Normally consumed via `BookingDialog` which delegates here when no
|
|
110
|
+
* `booking` prop is passed. Apps that need a bespoke flow can install the
|
|
111
|
+
* sections individually and assemble their own dialog instead of forking.
|
|
112
|
+
*/
|
|
113
|
+
export function BookingCreateDialog({ open, onOpenChange, onCreated, defaultProductId, }) {
|
|
114
|
+
const [product, setProduct] = React.useState({
|
|
115
|
+
productId: defaultProductId ?? "",
|
|
116
|
+
optionId: null,
|
|
117
|
+
});
|
|
118
|
+
const [slotId, setSlotId] = React.useState(null);
|
|
119
|
+
const [rooms, setRooms] = React.useState(emptyRoomsStepperValue);
|
|
120
|
+
const [person, setPerson] = React.useState(emptyPersonPickerValue);
|
|
121
|
+
const [sharedRoom, setSharedRoom] = React.useState(emptySharedRoomValue);
|
|
122
|
+
const [passengers, setPassengers] = React.useState(emptyPassengerListValue);
|
|
123
|
+
const [voucher, setVoucher] = React.useState(emptyVoucherPickerValue);
|
|
124
|
+
const [paymentSchedule, setPaymentSchedule] = React.useState(emptyPaymentScheduleValue);
|
|
125
|
+
const [notes, setNotes] = React.useState("");
|
|
126
|
+
/**
|
|
127
|
+
* Optional post-create transition: set status to `confirmed` right after
|
|
128
|
+
* create succeeds. When the parent app has the notifications module's
|
|
129
|
+
* `autoConfirmAndDispatch` enabled, this fires the doc bundle + traveler
|
|
130
|
+
* email via the `booking.confirmed` subscriber. When it isn't, the
|
|
131
|
+
* booking simply lands in `confirmed` instead of `draft`.
|
|
132
|
+
*/
|
|
133
|
+
const [confirmAfterCreate, setConfirmAfterCreate] = React.useState(false);
|
|
134
|
+
const [error, setError] = React.useState(null);
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
if (!open) {
|
|
137
|
+
setProduct({ productId: defaultProductId ?? "", optionId: null });
|
|
138
|
+
setSlotId(null);
|
|
139
|
+
setRooms(emptyRoomsStepperValue);
|
|
140
|
+
setPerson(emptyPersonPickerValue);
|
|
141
|
+
setSharedRoom(emptySharedRoomValue);
|
|
142
|
+
setPassengers(emptyPassengerListValue);
|
|
143
|
+
setVoucher(emptyVoucherPickerValue);
|
|
144
|
+
setPaymentSchedule(emptyPaymentScheduleValue);
|
|
145
|
+
setNotes("");
|
|
146
|
+
setConfirmAfterCreate(false);
|
|
147
|
+
setError(null);
|
|
148
|
+
}
|
|
149
|
+
else if (defaultProductId) {
|
|
150
|
+
setProduct((prev) => prev.productId === defaultProductId
|
|
151
|
+
? prev
|
|
152
|
+
: { productId: defaultProductId, optionId: null });
|
|
153
|
+
}
|
|
154
|
+
}, [open, defaultProductId]);
|
|
155
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: intentionally only resets when product/option changes
|
|
156
|
+
React.useEffect(() => {
|
|
157
|
+
setSlotId(null);
|
|
158
|
+
setRooms(emptyRoomsStepperValue);
|
|
159
|
+
}, [product.productId, product.optionId]);
|
|
160
|
+
const { data: slotsData } = useSlots({
|
|
161
|
+
productId: product.productId || undefined,
|
|
162
|
+
status: "open",
|
|
163
|
+
limit: 100,
|
|
164
|
+
enabled: open && Boolean(product.productId),
|
|
165
|
+
});
|
|
166
|
+
const slots = React.useMemo(() => {
|
|
167
|
+
const nowIso = new Date().toISOString();
|
|
168
|
+
return (slotsData?.data ?? [])
|
|
169
|
+
.filter((slot) => slot.startsAt >= nowIso)
|
|
170
|
+
.filter((slot) => {
|
|
171
|
+
if (!product.optionId)
|
|
172
|
+
return true;
|
|
173
|
+
return slot.optionId === null || slot.optionId === product.optionId;
|
|
174
|
+
})
|
|
175
|
+
.sort((a, b) => a.startsAt.localeCompare(b.startsAt));
|
|
176
|
+
}, [slotsData, product.optionId]);
|
|
177
|
+
const formatSlotLabel = React.useCallback((slot) => {
|
|
178
|
+
const date = new Date(slot.startsAt).toLocaleDateString(undefined, {
|
|
179
|
+
year: "numeric",
|
|
180
|
+
month: "short",
|
|
181
|
+
day: "numeric",
|
|
182
|
+
});
|
|
183
|
+
const remaining = !slot.unlimited && typeof slot.remainingPax === "number" ? ` · ${slot.remainingPax} left` : "";
|
|
184
|
+
return `${date}${remaining}`;
|
|
185
|
+
}, []);
|
|
186
|
+
const slotUnitAvailability = useSlotUnitAvailability({
|
|
187
|
+
slotId: slotId ?? undefined,
|
|
188
|
+
enabled: open && Boolean(slotId),
|
|
189
|
+
});
|
|
190
|
+
const roomUnitOptions = React.useMemo(() => {
|
|
191
|
+
const units = slotUnitAvailability.data?.data ?? [];
|
|
192
|
+
if (units.length === 0)
|
|
193
|
+
return [];
|
|
194
|
+
return units
|
|
195
|
+
.filter((unit) => (rooms.quantities[unit.optionUnitId] ?? 0) > 0)
|
|
196
|
+
.map((unit) => {
|
|
197
|
+
const qty = rooms.quantities[unit.optionUnitId] ?? 0;
|
|
198
|
+
const occupancyMax = 1;
|
|
199
|
+
const seats = qty * occupancyMax;
|
|
200
|
+
const assigned = passengers.passengers.filter((p) => p.roomUnitId === unit.optionUnitId).length;
|
|
201
|
+
return {
|
|
202
|
+
unitId: unit.optionUnitId,
|
|
203
|
+
unitName: unit.unitName,
|
|
204
|
+
remainingCapacity: Math.max(0, seats - assigned),
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
}, [slotUnitAvailability.data, rooms.quantities, passengers.passengers]);
|
|
208
|
+
// Currency placeholder — used for voucher + payment schedule display.
|
|
209
|
+
// Consumers hooking in real product data should override this by wrapping
|
|
210
|
+
// the component or swapping in their own currency-aware hook.
|
|
211
|
+
const currency = "EUR";
|
|
212
|
+
const { create: createPerson } = usePersonMutation();
|
|
213
|
+
const quickCreateMutation = useBookingQuickCreateMutation();
|
|
214
|
+
const statusMutation = useBookingStatusByIdMutation();
|
|
215
|
+
const handleSubmit = async () => {
|
|
216
|
+
setError(null);
|
|
217
|
+
if (!product.productId) {
|
|
218
|
+
setError("Select a product");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
let resolvedPersonId = null;
|
|
222
|
+
try {
|
|
223
|
+
if (person.mode === "existing") {
|
|
224
|
+
if (!person.personId) {
|
|
225
|
+
setError("Select a person or switch to create mode");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
resolvedPersonId = person.personId;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
if (!person.newPerson.firstName.trim() || !person.newPerson.lastName.trim()) {
|
|
232
|
+
setError("First and last name are required");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const created = await createPerson.mutateAsync({
|
|
236
|
+
firstName: person.newPerson.firstName.trim(),
|
|
237
|
+
lastName: person.newPerson.lastName.trim(),
|
|
238
|
+
email: person.newPerson.email.trim() || null,
|
|
239
|
+
phone: person.newPerson.phone.trim() || null,
|
|
240
|
+
});
|
|
241
|
+
resolvedPersonId = created.id;
|
|
242
|
+
}
|
|
243
|
+
if (sharedRoom.enabled && sharedRoom.mode === "join" && !sharedRoom.groupId) {
|
|
244
|
+
setError("Select a shared-room group to join");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const bookingNumber = generateBookingNumber();
|
|
248
|
+
const paymentSchedules = paymentScheduleToRows(paymentSchedule, currency, null);
|
|
249
|
+
const travelers = passengersToRows(passengers);
|
|
250
|
+
const voucherRedemption = voucher.picked && voucher.picked.remainingAmountCents != null
|
|
251
|
+
? {
|
|
252
|
+
voucherId: voucher.picked.id,
|
|
253
|
+
amountCents: voucher.picked.remainingAmountCents,
|
|
254
|
+
}
|
|
255
|
+
: undefined;
|
|
256
|
+
const groupMembership = sharedRoom.enabled
|
|
257
|
+
? sharedRoom.mode === "create"
|
|
258
|
+
? {
|
|
259
|
+
action: "create",
|
|
260
|
+
kind: "shared_room",
|
|
261
|
+
label: `Shared room — ${bookingNumber}`,
|
|
262
|
+
optionUnitId: product.optionId,
|
|
263
|
+
makeBookingPrimary: true,
|
|
264
|
+
}
|
|
265
|
+
: sharedRoom.groupId
|
|
266
|
+
? { action: "join", groupId: sharedRoom.groupId, role: "shared" }
|
|
267
|
+
: undefined
|
|
268
|
+
: undefined;
|
|
269
|
+
const { booking } = await quickCreateMutation.mutateAsync({
|
|
270
|
+
productId: product.productId,
|
|
271
|
+
bookingNumber,
|
|
272
|
+
optionId: product.optionId,
|
|
273
|
+
slotId,
|
|
274
|
+
personId: resolvedPersonId,
|
|
275
|
+
organizationId: person.organizationId,
|
|
276
|
+
internalNotes: notes.trim() || null,
|
|
277
|
+
travelers: travelers.length > 0 ? travelers : undefined,
|
|
278
|
+
paymentSchedules: paymentSchedules.length > 0 ? paymentSchedules : undefined,
|
|
279
|
+
voucherRedemption,
|
|
280
|
+
groupMembership,
|
|
281
|
+
});
|
|
282
|
+
// Optional post-create confirm. If the app has autoConfirmAndDispatch
|
|
283
|
+
// wired on the notifications module, the status transition triggers
|
|
284
|
+
// the doc bundle + traveler email subscriber. A failed status change
|
|
285
|
+
// doesn't roll back the booking — it exists, operator can confirm
|
|
286
|
+
// manually later.
|
|
287
|
+
let finalBooking = booking;
|
|
288
|
+
if (confirmAfterCreate) {
|
|
289
|
+
try {
|
|
290
|
+
finalBooking = await statusMutation.mutateAsync({
|
|
291
|
+
bookingId: booking.id,
|
|
292
|
+
currentStatus: booking.status,
|
|
293
|
+
status: "confirmed",
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
catch (statusErr) {
|
|
297
|
+
setError(statusErr instanceof Error
|
|
298
|
+
? `Booking created but confirm failed: ${statusErr.message}`
|
|
299
|
+
: "Booking created but confirm failed");
|
|
300
|
+
onCreated?.(booking);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
onOpenChange(false);
|
|
305
|
+
onCreated?.(finalBooking);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
setError(err instanceof Error ? err.message : "Failed to create booking");
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
const isSubmitting = quickCreateMutation.isPending || createPerson.isPending || statusMutation.isPending;
|
|
312
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Quick Book" }) }), _jsxs(DialogBody, { className: "grid gap-4", children: [_jsx(ProductPickerSection, { value: product, onChange: setProduct, enabled: open, lockProduct: Boolean(defaultProductId) }), product.productId ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { children: "Departure" }), _jsxs(Select, { value: slotId ?? "__none__", onValueChange: (v) => setSlotId(v === "__none__" ? null : (v ?? null)), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Select a departure..." }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "__none__", children: "No specific departure" }), slots.length === 0 ? (_jsx(SelectItem, { value: "__empty__", disabled: true, children: "No open departures for this product" })) : (slots.map((slot) => (_jsx(SelectItem, { value: slot.id, children: formatSlotLabel(slot) }, slot.id))))] })] })] })) : null, slotId ? (_jsx(RoomsStepperSection, { value: rooms, onChange: setRooms, slotId: slotId, enabled: open })) : null, _jsx(PersonPickerSection, { value: person, onChange: setPerson, enabled: open }), _jsx(SharedRoomSection, { value: sharedRoom, onChange: setSharedRoom, productId: product.productId || undefined, enabled: open }), product.productId ? (_jsx(PassengersSection, { value: passengers, onChange: setPassengers, roomUnits: roomUnitOptions.length > 0 ? roomUnitOptions : undefined })) : null, product.productId ? (_jsx(PriceBreakdownSection, { productId: product.productId, optionId: product.optionId, unitQuantities: rooms.quantities })) : null, _jsx(VoucherPickerSection, { value: voucher, onChange: setVoucher, currency: currency }), _jsx(PaymentScheduleSection, { value: paymentSchedule, onChange: setPaymentSchedule, currency: currency }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Internal Notes" }), _jsx(Textarea, { value: notes, onChange: (e) => setNotes(e.target.value), placeholder: "Quick context for this booking..." })] }), _jsxs("div", { className: "flex items-start gap-2 rounded-md border p-3", children: [_jsx(Checkbox, { id: "quickbook-confirm-after-create", checked: confirmAfterCreate, onCheckedChange: (v) => setConfirmAfterCreate(v === true), className: "mt-0.5" }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { htmlFor: "quickbook-confirm-after-create", className: "cursor-pointer text-sm", children: "Confirm & notify traveler after creating" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Transitions to confirmed after create. When the notifications module's auto-dispatch is on, this fires the doc bundle + traveler email via the booking.confirmed subscriber." })] })] }), error && _jsx("p", { className: "text-xs text-destructive", children: error })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), disabled: isSubmitting, children: "Cancel" }), _jsxs(Button, { type: "button", size: "sm", onClick: handleSubmit, disabled: isSubmitting || !product.productId, children: [isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "Create Draft Booking"] })] })] }) }));
|
|
313
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type BookingRecord } from "@voyantjs/bookings-react";
|
|
2
|
+
export interface BookingDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
booking?: BookingRecord;
|
|
6
|
+
onSuccess?: (booking: BookingRecord) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Pre-seeds the product picker in create mode. Useful when opened from
|
|
9
|
+
* a product detail page. Ignored when editing an existing booking.
|
|
10
|
+
*/
|
|
11
|
+
defaultProductId?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Single booking dialog that handles both create and edit:
|
|
15
|
+
* - Create (no `booking` prop): renders the rich product → option → person
|
|
16
|
+
* picker flow via `BookingCreateDialog`, so the draft booking inherits
|
|
17
|
+
* pricing, dates, and currency from the catalogue instead of being
|
|
18
|
+
* hand-entered.
|
|
19
|
+
* - Edit (with `booking` prop): renders the flat form below that patches
|
|
20
|
+
* the existing row's metadata (status, amounts, dates, notes).
|
|
21
|
+
*/
|
|
22
|
+
export declare function BookingDialog({ open, onOpenChange, booking, onSuccess, defaultProductId, }: BookingDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
//# sourceMappingURL=booking-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAsB,MAAM,0BAA0B,CAAA;AA2CjF,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAUD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,YAAY,EACZ,OAAO,EACP,SAAS,EACT,gBAAgB,GACjB,EAAE,kBAAkB,2CAoBpB"}
|