@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.
Files changed (101) hide show
  1. package/README.md +13 -0
  2. package/dist/components/booking-activity-timeline.d.ts +5 -0
  3. package/dist/components/booking-activity-timeline.d.ts.map +1 -0
  4. package/dist/components/booking-activity-timeline.js +83 -0
  5. package/dist/components/booking-cancellation-dialog.d.ts +18 -0
  6. package/dist/components/booking-cancellation-dialog.d.ts.map +1 -0
  7. package/dist/components/booking-cancellation-dialog.js +80 -0
  8. package/dist/components/booking-create-dialog.d.ts +21 -0
  9. package/dist/components/booking-create-dialog.d.ts.map +1 -0
  10. package/dist/components/booking-create-dialog.js +313 -0
  11. package/dist/components/booking-dialog.d.ts +23 -0
  12. package/dist/components/booking-dialog.d.ts.map +1 -0
  13. package/dist/components/booking-dialog.js +108 -0
  14. package/dist/components/booking-document-dialog.d.ts +8 -0
  15. package/dist/components/booking-document-dialog.d.ts.map +1 -0
  16. package/dist/components/booking-document-dialog.js +67 -0
  17. package/dist/components/booking-document-list.d.ts +5 -0
  18. package/dist/components/booking-document-list.d.ts.map +1 -0
  19. package/dist/components/booking-document-list.js +38 -0
  20. package/dist/components/booking-group-link-dialog.d.ts +10 -0
  21. package/dist/components/booking-group-link-dialog.d.ts.map +1 -0
  22. package/dist/components/booking-group-link-dialog.js +68 -0
  23. package/dist/components/booking-group-section.d.ts +17 -0
  24. package/dist/components/booking-group-section.d.ts.map +1 -0
  25. package/dist/components/booking-group-section.js +31 -0
  26. package/dist/components/booking-guarantee-dialog.d.ts +10 -0
  27. package/dist/components/booking-guarantee-dialog.d.ts.map +1 -0
  28. package/dist/components/booking-guarantee-dialog.js +101 -0
  29. package/dist/components/booking-guarantee-list.d.ts +5 -0
  30. package/dist/components/booking-guarantee-list.d.ts.map +1 -0
  31. package/dist/components/booking-guarantee-list.js +45 -0
  32. package/dist/components/booking-item-dialog.d.ts +10 -0
  33. package/dist/components/booking-item-dialog.d.ts.map +1 -0
  34. package/dist/components/booking-item-dialog.js +119 -0
  35. package/dist/components/booking-item-list.d.ts +5 -0
  36. package/dist/components/booking-item-list.d.ts.map +1 -0
  37. package/dist/components/booking-item-list.js +50 -0
  38. package/dist/components/booking-item-travelers.d.ts +6 -0
  39. package/dist/components/booking-item-travelers.d.ts.map +1 -0
  40. package/dist/components/booking-item-travelers.js +50 -0
  41. package/dist/components/booking-list.d.ts +7 -0
  42. package/dist/components/booking-list.d.ts.map +1 -0
  43. package/dist/components/booking-list.js +47 -0
  44. package/dist/components/booking-notes.d.ts +5 -0
  45. package/dist/components/booking-notes.d.ts.map +1 -0
  46. package/dist/components/booking-notes.js +16 -0
  47. package/dist/components/booking-payment-schedule-dialog.d.ts +10 -0
  48. package/dist/components/booking-payment-schedule-dialog.d.ts.map +1 -0
  49. package/dist/components/booking-payment-schedule-dialog.js +77 -0
  50. package/dist/components/booking-payment-schedule-list.d.ts +5 -0
  51. package/dist/components/booking-payment-schedule-list.d.ts.map +1 -0
  52. package/dist/components/booking-payment-schedule-list.js +43 -0
  53. package/dist/components/booking-payments-summary.d.ts +5 -0
  54. package/dist/components/booking-payments-summary.d.ts.map +1 -0
  55. package/dist/components/booking-payments-summary.js +19 -0
  56. package/dist/components/file-dropzone.d.ts +25 -0
  57. package/dist/components/file-dropzone.d.ts.map +1 -0
  58. package/dist/components/file-dropzone.js +92 -0
  59. package/dist/components/passengers-section.d.ts +72 -0
  60. package/dist/components/passengers-section.d.ts.map +1 -0
  61. package/dist/components/passengers-section.js +74 -0
  62. package/dist/components/payment-schedule-section.d.ts +62 -0
  63. package/dist/components/payment-schedule-section.d.ts.map +1 -0
  64. package/dist/components/payment-schedule-section.js +88 -0
  65. package/dist/components/person-picker-section.d.ts +53 -0
  66. package/dist/components/person-picker-section.d.ts.map +1 -0
  67. package/dist/components/person-picker-section.js +71 -0
  68. package/dist/components/price-breakdown-section.d.ts +48 -0
  69. package/dist/components/price-breakdown-section.d.ts.map +1 -0
  70. package/dist/components/price-breakdown-section.js +165 -0
  71. package/dist/components/product-picker-section.d.ts +27 -0
  72. package/dist/components/product-picker-section.d.ts.map +1 -0
  73. package/dist/components/product-picker-section.js +41 -0
  74. package/dist/components/rooms-stepper-section.d.ts +45 -0
  75. package/dist/components/rooms-stepper-section.d.ts.map +1 -0
  76. package/dist/components/rooms-stepper-section.js +60 -0
  77. package/dist/components/shared-room-section.d.ts +37 -0
  78. package/dist/components/shared-room-section.d.ts.map +1 -0
  79. package/dist/components/shared-room-section.js +40 -0
  80. package/dist/components/status-change-dialog.d.ts +10 -0
  81. package/dist/components/status-change-dialog.d.ts.map +1 -0
  82. package/dist/components/status-change-dialog.js +41 -0
  83. package/dist/components/supplier-status-dialog.d.ts +10 -0
  84. package/dist/components/supplier-status-dialog.d.ts.map +1 -0
  85. package/dist/components/supplier-status-dialog.js +77 -0
  86. package/dist/components/supplier-status-list.d.ts +5 -0
  87. package/dist/components/supplier-status-list.d.ts.map +1 -0
  88. package/dist/components/supplier-status-list.js +33 -0
  89. package/dist/components/traveler-dialog.d.ts +10 -0
  90. package/dist/components/traveler-dialog.d.ts.map +1 -0
  91. package/dist/components/traveler-dialog.js +64 -0
  92. package/dist/components/traveler-list.d.ts +5 -0
  93. package/dist/components/traveler-list.d.ts.map +1 -0
  94. package/dist/components/traveler-list.js +32 -0
  95. package/dist/components/voucher-picker-section.d.ts +50 -0
  96. package/dist/components/voucher-picker-section.d.ts.map +1 -0
  97. package/dist/components/voucher-picker-section.js +94 -0
  98. package/dist/index.d.ts +33 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +32 -0
  101. package/package.json +76 -0
@@ -0,0 +1,108 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useBookingMutation } from "@voyantjs/bookings-react";
4
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/voyant-ui/components";
5
+ import { CurrencyCombobox } from "@voyantjs/voyant-ui/components/currency-combobox";
6
+ import { DateRangePicker } from "@voyantjs/voyant-ui/components/date-picker";
7
+ import { zodResolver } from "@voyantjs/voyant-ui/lib/zod-resolver";
8
+ import { Loader2 } from "lucide-react";
9
+ import { useEffect } from "react";
10
+ import { useForm } from "react-hook-form";
11
+ import { z } from "zod/v4";
12
+ import { BookingCreateDialog } from "./booking-create-dialog";
13
+ const bookingFormSchema = z.object({
14
+ bookingNumber: z.string().min(1, "Booking number is required"),
15
+ status: z.enum(["draft", "confirmed", "in_progress", "completed", "cancelled"]),
16
+ sellCurrency: z.string().min(3).max(3, "Use 3-letter ISO code"),
17
+ sellAmountCents: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
18
+ costAmountCents: z.coerce.number().int().min(0).optional().or(z.literal("")).nullable(),
19
+ startDate: z.string().optional().nullable(),
20
+ endDate: z.string().optional().nullable(),
21
+ pax: z.coerce.number().int().positive().optional().or(z.literal("")).nullable(),
22
+ internalNotes: z.string().optional().nullable(),
23
+ });
24
+ const BOOKING_STATUSES = [
25
+ { value: "draft", label: "Draft" },
26
+ { value: "confirmed", label: "Confirmed" },
27
+ { value: "in_progress", label: "In Progress" },
28
+ { value: "completed", label: "Completed" },
29
+ { value: "cancelled", label: "Cancelled" },
30
+ ];
31
+ /**
32
+ * Single booking dialog that handles both create and edit:
33
+ * - Create (no `booking` prop): renders the rich product → option → person
34
+ * picker flow via `BookingCreateDialog`, so the draft booking inherits
35
+ * pricing, dates, and currency from the catalogue instead of being
36
+ * hand-entered.
37
+ * - Edit (with `booking` prop): renders the flat form below that patches
38
+ * the existing row's metadata (status, amounts, dates, notes).
39
+ */
40
+ export function BookingDialog({ open, onOpenChange, booking, onSuccess, defaultProductId, }) {
41
+ if (!booking) {
42
+ return (_jsx(BookingCreateDialog, { open: open, onOpenChange: onOpenChange, defaultProductId: defaultProductId, onCreated: onSuccess }));
43
+ }
44
+ return (_jsx(BookingEditDialog, { open: open, onOpenChange: onOpenChange, booking: booking, onSuccess: onSuccess }));
45
+ }
46
+ function BookingEditDialog({ open, onOpenChange, booking, onSuccess }) {
47
+ const { update } = useBookingMutation();
48
+ const form = useForm({
49
+ resolver: zodResolver(bookingFormSchema),
50
+ defaultValues: {
51
+ bookingNumber: "",
52
+ status: "draft",
53
+ sellCurrency: "EUR",
54
+ sellAmountCents: "",
55
+ costAmountCents: "",
56
+ startDate: "",
57
+ endDate: "",
58
+ pax: "",
59
+ internalNotes: "",
60
+ },
61
+ });
62
+ useEffect(() => {
63
+ if (!open)
64
+ return;
65
+ form.reset({
66
+ bookingNumber: booking.bookingNumber,
67
+ status: booking.status === "on_hold" || booking.status === "expired" ? "draft" : booking.status,
68
+ sellCurrency: booking.sellCurrency,
69
+ sellAmountCents: booking.sellAmountCents ?? "",
70
+ costAmountCents: booking.costAmountCents ?? "",
71
+ startDate: booking.startDate ?? "",
72
+ endDate: booking.endDate ?? "",
73
+ pax: booking.pax ?? "",
74
+ internalNotes: booking.internalNotes ?? "",
75
+ });
76
+ }, [booking, form, open]);
77
+ const onSubmit = async (values) => {
78
+ const payload = {
79
+ bookingNumber: values.bookingNumber,
80
+ status: values.status,
81
+ sellCurrency: values.sellCurrency,
82
+ sellAmountCents: values.sellAmountCents && typeof values.sellAmountCents === "number"
83
+ ? values.sellAmountCents
84
+ : null,
85
+ costAmountCents: values.costAmountCents && typeof values.costAmountCents === "number"
86
+ ? values.costAmountCents
87
+ : null,
88
+ startDate: values.startDate || null,
89
+ endDate: values.endDate || null,
90
+ pax: values.pax && typeof values.pax === "number" ? values.pax : null,
91
+ internalNotes: values.internalNotes || null,
92
+ };
93
+ const saved = await update.mutateAsync({ id: booking.id, input: payload });
94
+ onOpenChange(false);
95
+ onSuccess?.(saved);
96
+ };
97
+ const isSubmitting = update.isPending;
98
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Edit Booking" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "flex flex-1 flex-col overflow-hidden", children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Booking Number" }), _jsx(Input, { ...form.register("bookingNumber"), placeholder: "BK-2501-1234" }), form.formState.errors.bookingNumber && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.bookingNumber.message }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Status" }), _jsxs(Select, { value: form.watch("status"), onValueChange: (value) => form.setValue("status", value), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: BOOKING_STATUSES.map((status) => (_jsx(SelectItem, { value: status.value, children: status.label }, status.value))) })] })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Sell Currency" }), _jsx(CurrencyCombobox, { value: form.watch("sellCurrency") || null, onChange: (next) => form.setValue("sellCurrency", next ?? "EUR", {
99
+ shouldValidate: true,
100
+ shouldDirty: true,
101
+ }) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Travel Dates" }), _jsx(DateRangePicker, { value: {
102
+ from: form.watch("startDate") || null,
103
+ to: form.watch("endDate") || null,
104
+ }, onChange: (nextValue) => {
105
+ form.setValue("startDate", nextValue?.from ?? "", { shouldDirty: true });
106
+ form.setValue("endDate", nextValue?.to ?? "", { shouldDirty: true });
107
+ }, placeholder: "Pick travel dates", className: "w-full" })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Sell Amount (cents)" }), _jsx(Input, { ...form.register("sellAmountCents"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Cost Amount (cents)" }), _jsx(Input, { ...form.register("costAmountCents"), type: "number", min: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Travelers (Pax)" }), _jsx(Input, { ...form.register("pax"), type: "number", min: "1", placeholder: "2" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Internal Notes" }), _jsx(Textarea, { ...form.register("internalNotes"), placeholder: "Private operations notes..." })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", size: "sm", disabled: isSubmitting, children: [isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "Save Changes"] })] })] })] }) }));
108
+ }
@@ -0,0 +1,8 @@
1
+ export interface BookingDocumentDialogProps {
2
+ open: boolean;
3
+ onOpenChange: (open: boolean) => void;
4
+ bookingId: string;
5
+ onSuccess?: () => void;
6
+ }
7
+ export declare function BookingDocumentDialog({ open, onOpenChange, bookingId, onSuccess, }: BookingDocumentDialogProps): import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=booking-document-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-document-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-document-dialog.tsx"],"names":[],"mappings":"AA4CA,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,wBAAgB,qBAAqB,CAAC,EACpC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,GACV,EAAE,0BAA0B,2CAmJ5B"}
@@ -0,0 +1,67 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useBookingTravelerDocumentMutation, useTravelers } from "@voyantjs/bookings-react";
4
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/voyant-ui/components";
5
+ import { DatePicker } from "@voyantjs/voyant-ui/components/date-picker";
6
+ import { zodResolver } from "@voyantjs/voyant-ui/lib/zod-resolver";
7
+ import { Loader2 } from "lucide-react";
8
+ import { useEffect } from "react";
9
+ import { useForm } from "react-hook-form";
10
+ import { z } from "zod/v4";
11
+ import { FileDropzone } from "./file-dropzone";
12
+ const documentTypes = ["visa", "insurance", "health", "passport_copy", "other"];
13
+ const UNASSIGNED = "__unassigned__";
14
+ const documentFormSchema = z.object({
15
+ type: z.enum(documentTypes).default("other"),
16
+ fileName: z.string().min(1, "File name is required").max(500),
17
+ fileUrl: z.string().url("Must be a valid URL"),
18
+ travelerId: z.string().optional().nullable(),
19
+ expiresAt: z.string().optional().nullable(),
20
+ notes: z.string().optional().nullable(),
21
+ });
22
+ export function BookingDocumentDialog({ open, onOpenChange, bookingId, onSuccess, }) {
23
+ const { create } = useBookingTravelerDocumentMutation(bookingId);
24
+ const { data: travelersData } = useTravelers(bookingId);
25
+ const travelers = travelersData?.data ?? [];
26
+ const form = useForm({
27
+ resolver: zodResolver(documentFormSchema),
28
+ defaultValues: {
29
+ type: "other",
30
+ fileName: "",
31
+ fileUrl: "",
32
+ travelerId: UNASSIGNED,
33
+ expiresAt: "",
34
+ notes: "",
35
+ },
36
+ });
37
+ useEffect(() => {
38
+ if (open) {
39
+ form.reset();
40
+ }
41
+ }, [form, open]);
42
+ const onSubmit = async (values) => {
43
+ await create.mutateAsync({
44
+ type: values.type,
45
+ fileName: values.fileName,
46
+ fileUrl: values.fileUrl,
47
+ travelerId: values.travelerId && values.travelerId !== UNASSIGNED ? values.travelerId : null,
48
+ expiresAt: values.expiresAt || null,
49
+ notes: values.notes || null,
50
+ });
51
+ onOpenChange(false);
52
+ onSuccess?.();
53
+ };
54
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Add Document" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "flex flex-1 flex-col overflow-hidden", children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Type" }), _jsxs(Select, { items: documentTypes.map((t) => ({ label: t.replace(/_/g, " "), value: t })), value: form.watch("type"), onValueChange: (v) => form.setValue("type", (v ?? "other")), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: documentTypes.map((t) => (_jsx(SelectItem, { value: t, children: t.replace(/_/g, " ") }, t))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Traveler (optional)" }), _jsxs(Select, { items: [
55
+ { label: "Booking-wide", value: UNASSIGNED },
56
+ ...travelers.map((traveler) => ({
57
+ label: `${traveler.firstName} ${traveler.lastName}`,
58
+ value: traveler.id,
59
+ })),
60
+ ], value: form.watch("travelerId") ?? UNASSIGNED, onValueChange: (v) => form.setValue("travelerId", v ?? UNASSIGNED), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: UNASSIGNED, children: "Booking-wide" }), travelers.map((traveler) => (_jsxs(SelectItem, { value: traveler.id, children: [traveler.firstName, " ", traveler.lastName] }, traveler.id)))] })] })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "File" }), _jsx(FileDropzone, { accept: "application/pdf,image/*", maxSize: 10 * 1024 * 1024, onUploaded: (upload) => {
61
+ form.setValue("fileUrl", upload.url, { shouldValidate: true });
62
+ form.setValue("fileName", upload.name, { shouldValidate: true });
63
+ }, helperText: "Drop passport, visa, or insurance document (PDF or image)" }), form.formState.errors.fileUrl && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.fileUrl.message }))] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Expires At (optional)" }), _jsx(DatePicker, { value: form.watch("expiresAt") || null, onChange: (next) => form.setValue("expiresAt", next ?? "", {
64
+ shouldValidate: true,
65
+ shouldDirty: true,
66
+ }), placeholder: "Select expiry date", className: "w-full" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Notes" }), _jsx(Textarea, { ...form.register("notes"), placeholder: "Additional notes..." })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", size: "sm", disabled: create.isPending, children: [create.isPending && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "Add Document"] })] })] })] }) }));
67
+ }
@@ -0,0 +1,5 @@
1
+ export interface BookingDocumentListProps {
2
+ bookingId: string;
3
+ }
4
+ export declare function BookingDocumentList({ bookingId }: BookingDocumentListProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=booking-document-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-document-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-document-list.tsx"],"names":[],"mappings":"AA6BA,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,mBAAmB,CAAC,EAAE,SAAS,EAAE,EAAE,wBAAwB,2CAqG1E"}
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useBookingTravelerDocumentMutation, useBookingTravelerDocuments, useTravelers, } from "@voyantjs/bookings-react";
4
+ import { Badge, Button, Card, CardContent, CardHeader, CardTitle, } from "@voyantjs/voyant-ui/components";
5
+ import { ExternalLink, FileText, Plus, Trash2 } from "lucide-react";
6
+ import * as React from "react";
7
+ import { BookingDocumentDialog } from "./booking-document-dialog";
8
+ const typeVariant = {
9
+ visa: "default",
10
+ insurance: "secondary",
11
+ health: "secondary",
12
+ passport_copy: "outline",
13
+ other: "outline",
14
+ };
15
+ export function BookingDocumentList({ bookingId }) {
16
+ const [dialogOpen, setDialogOpen] = React.useState(false);
17
+ const { data } = useBookingTravelerDocuments(bookingId);
18
+ const { data: travelersData } = useTravelers(bookingId);
19
+ const { remove } = useBookingTravelerDocumentMutation(bookingId);
20
+ const documents = data?.data ?? [];
21
+ const travelers = travelersData?.data ?? [];
22
+ const travelerMap = new Map();
23
+ for (const traveler of travelers) {
24
+ travelerMap.set(traveler.id, traveler);
25
+ }
26
+ return (_jsxs(Card, { "data-slot": "booking-document-list", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(FileText, { className: "h-4 w-4" }), "Documents"] }), _jsxs(Button, { size: "sm", onClick: () => setDialogOpen(true), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Add Document"] })] }), _jsx(CardContent, { children: documents.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: "No documents yet." })) : (_jsx("div", { className: "rounded border bg-background", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "p-2 text-left font-medium", children: "Type" }), _jsx("th", { className: "p-2 text-left font-medium", children: "File" }), _jsx("th", { className: "p-2 text-left font-medium", children: "Traveler" }), _jsx("th", { className: "p-2 text-left font-medium", children: "Expires" }), _jsx("th", { className: "p-2 text-left font-medium", children: "Notes" }), _jsx("th", { className: "w-20 p-2" })] }) }), _jsx("tbody", { children: documents.map((doc) => {
27
+ const traveler = doc.travelerId ? travelerMap.get(doc.travelerId) : undefined;
28
+ return (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsx("td", { className: "p-2", children: _jsx(Badge, { variant: typeVariant[doc.type] ?? "outline", className: "capitalize", children: doc.type.replace(/_/g, " ") }) }), _jsx("td", { className: "p-2", children: _jsxs("a", { href: doc.fileUrl, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1 hover:underline", children: [doc.fileName, _jsx(ExternalLink, { className: "h-3 w-3 text-muted-foreground" })] }) }), _jsx("td", { className: "p-2", children: traveler
29
+ ? `${traveler.firstName} ${traveler.lastName}`
30
+ : doc.travelerId
31
+ ? doc.travelerId
32
+ : "—" }), _jsx("td", { className: "p-2", children: doc.expiresAt ? new Date(doc.expiresAt).toLocaleDateString() : "-" }), _jsx("td", { className: "max-w-[200px] truncate p-2 text-muted-foreground", children: doc.notes ?? "-" }), _jsx("td", { className: "p-2", children: _jsx("button", { type: "button", onClick: () => {
33
+ if (confirm("Delete this document?")) {
34
+ remove.mutate(doc.id);
35
+ }
36
+ }, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }) })] }, doc.id));
37
+ }) })] }) })) }), _jsx(BookingDocumentDialog, { open: dialogOpen, onOpenChange: setDialogOpen, bookingId: bookingId })] }));
38
+ }
@@ -0,0 +1,10 @@
1
+ export interface BookingGroupLinkDialogProps {
2
+ open: boolean;
3
+ onOpenChange: (open: boolean) => void;
4
+ bookingId: string;
5
+ productId?: string | null;
6
+ optionUnitId?: string | null;
7
+ onLinked?: (groupId: string) => void;
8
+ }
9
+ export declare function BookingGroupLinkDialog({ open, onOpenChange, bookingId, productId, optionUnitId, onLinked, }: BookingGroupLinkDialogProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=booking-group-link-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-group-link-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-group-link-dialog.tsx"],"names":[],"mappings":"AA0BA,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACrC;AAMD,wBAAgB,sBAAsB,CAAC,EACrC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,EACT,YAAY,EACZ,QAAQ,GACT,EAAE,2BAA2B,2CAuK7B"}
@@ -0,0 +1,68 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useBookingGroupMemberMutation, useBookingGroupMutation, useBookingGroups, } from "@voyantjs/bookings-react";
4
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/voyant-ui/components";
5
+ import { Loader2 } from "lucide-react";
6
+ import * as React from "react";
7
+ const JOIN_PLACEHOLDER = "__none__";
8
+ export function BookingGroupLinkDialog({ open, onOpenChange, bookingId, productId, optionUnitId, onLinked, }) {
9
+ const [mode, setMode] = React.useState("join");
10
+ const [selectedGroupId, setSelectedGroupId] = React.useState("");
11
+ const [newGroupLabel, setNewGroupLabel] = React.useState("");
12
+ const [error, setError] = React.useState(null);
13
+ React.useEffect(() => {
14
+ if (!open) {
15
+ setMode("join");
16
+ setSelectedGroupId("");
17
+ setNewGroupLabel("");
18
+ setError(null);
19
+ }
20
+ }, [open]);
21
+ const { data } = useBookingGroups({
22
+ productId: productId ?? undefined,
23
+ optionUnitId: optionUnitId ?? undefined,
24
+ limit: 50,
25
+ enabled: open,
26
+ });
27
+ const groups = data?.data ?? [];
28
+ const { create: createGroup } = useBookingGroupMutation();
29
+ const { add: addMember } = useBookingGroupMemberMutation();
30
+ const handleSubmit = async () => {
31
+ setError(null);
32
+ try {
33
+ let targetGroupId = selectedGroupId;
34
+ let role = "shared";
35
+ if (mode === "create") {
36
+ const label = newGroupLabel.trim() || `Shared room — ${new Date().toLocaleDateString()}`;
37
+ const group = await createGroup.mutateAsync({
38
+ kind: "shared_room",
39
+ label,
40
+ productId: productId ?? null,
41
+ optionUnitId: optionUnitId ?? null,
42
+ primaryBookingId: bookingId,
43
+ });
44
+ targetGroupId = group.id;
45
+ role = "primary";
46
+ }
47
+ if (!targetGroupId || targetGroupId === JOIN_PLACEHOLDER) {
48
+ setError("Select a group to join");
49
+ return;
50
+ }
51
+ await addMember.mutateAsync({
52
+ groupId: targetGroupId,
53
+ bookingId,
54
+ role,
55
+ });
56
+ onOpenChange(false);
57
+ onLinked?.(targetGroupId);
58
+ }
59
+ catch (err) {
60
+ const message = err instanceof Error ? err.message : "Failed to link booking";
61
+ setError(message);
62
+ }
63
+ };
64
+ const isSubmitting = createGroup.isPending || addMember.isPending;
65
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Link Booking to Shared Room" }) }), _jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", size: "sm", variant: mode === "join" ? "default" : "ghost", onClick: () => setMode("join"), children: "Join existing" }), _jsx(Button, { type: "button", size: "sm", variant: mode === "create" ? "default" : "ghost", onClick: () => setMode("create"), children: "Create new" })] }), mode === "join" ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Existing groups" }), _jsxs(Select, { items: groups.length === 0
66
+ ? [{ label: "No existing groups", value: JOIN_PLACEHOLDER }]
67
+ : groups.map((g) => ({ label: g.label, value: g.id })), value: selectedGroupId || JOIN_PLACEHOLDER, onValueChange: (v) => setSelectedGroupId(v === JOIN_PLACEHOLDER ? "" : (v ?? "")), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: "Select a group..." }) }), _jsx(SelectContent, { children: groups.length === 0 ? (_jsx(SelectItem, { value: JOIN_PLACEHOLDER, disabled: true, children: "No existing groups" })) : (groups.map((g) => (_jsx(SelectItem, { value: g.id, children: g.label }, g.id)))) })] }), productId && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Filtered to groups for the booking's product." }))] })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Group label" }), _jsx(Input, { value: newGroupLabel, onChange: (e) => setNewGroupLabel(e.target.value), placeholder: "e.g. Smith + Jones, Room 204" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "This booking will be marked as the primary member." })] })), 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 || (mode === "join" && !selectedGroupId), children: [isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), mode === "create" ? "Create & Link" : "Link to Group"] })] })] }) }));
68
+ }
@@ -0,0 +1,17 @@
1
+ export interface BookingGroupSectionProps {
2
+ bookingId: string;
3
+ /**
4
+ * Product ID used to scope shared-room group context. Leave unset to
5
+ * auto-resolve from the booking's items; pass an explicit string or `null`
6
+ * to override.
7
+ */
8
+ productId?: string | null;
9
+ /**
10
+ * Option unit ID used to scope shared-room group context. Leave unset to
11
+ * auto-resolve from the booking's items; pass an explicit string or `null`
12
+ * to override.
13
+ */
14
+ optionUnitId?: string | null;
15
+ }
16
+ export declare function BookingGroupSection({ bookingId, productId, optionUnitId, }: BookingGroupSectionProps): import("react/jsx-runtime").JSX.Element;
17
+ //# sourceMappingURL=booking-group-section.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-group-section.d.ts","sourceRoot":"","sources":["../../src/components/booking-group-section.tsx"],"names":[],"mappings":"AAqBA,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAED,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,SAAS,EACT,YAAY,GACb,EAAE,wBAAwB,2CAsH1B"}
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useBookingGroup, useBookingGroupForBooking, useBookingGroupMemberMutation, useBookingPrimaryProduct, } from "@voyantjs/bookings-react";
4
+ import { Badge, Button, Card, CardContent, CardHeader, CardTitle, } from "@voyantjs/voyant-ui/components";
5
+ import { Link2, Unlink, Users } from "lucide-react";
6
+ import * as React from "react";
7
+ import { BookingGroupLinkDialog } from "./booking-group-link-dialog";
8
+ export function BookingGroupSection({ bookingId, productId, optionUnitId, }) {
9
+ const [linkDialogOpen, setLinkDialogOpen] = React.useState(false);
10
+ // Auto-resolve product/option-unit from items when the caller hasn't
11
+ // supplied them. Explicit `null` is respected as an override.
12
+ const shouldAutoResolve = productId === undefined || optionUnitId === undefined;
13
+ const autoResolved = useBookingPrimaryProduct(bookingId, { enabled: shouldAutoResolve });
14
+ const effectiveProductId = productId === undefined ? autoResolved.productId : productId;
15
+ const effectiveOptionUnitId = optionUnitId === undefined ? autoResolved.optionUnitId : optionUnitId;
16
+ const { data: groupForBookingData } = useBookingGroupForBooking(bookingId);
17
+ const group = groupForBookingData?.data ?? null;
18
+ const groupId = group?.id ?? null;
19
+ const { data: groupDetail } = useBookingGroup(groupId, { enabled: Boolean(groupId) });
20
+ const members = groupDetail?.data?.members ?? [];
21
+ const { remove: removeMember } = useBookingGroupMemberMutation();
22
+ const handleRemove = async () => {
23
+ if (!groupId)
24
+ return;
25
+ if (!confirm("Remove this booking from the shared-room group?"))
26
+ return;
27
+ await removeMember.mutateAsync({ groupId, bookingId });
28
+ };
29
+ const siblings = members.filter((m) => m.bookingId !== bookingId);
30
+ return (_jsxs(Card, { "data-slot": "booking-group-section", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(Users, { className: "h-4 w-4" }), "Shared Room"] }), group ? (_jsxs(Button, { size: "sm", variant: "outline", onClick: handleRemove, disabled: removeMember.isPending, children: [_jsx(Unlink, { className: "mr-2 h-4 w-4" }), "Remove from group"] })) : (_jsxs(Button, { size: "sm", onClick: () => setLinkDialogOpen(true), children: [_jsx(Link2, { className: "mr-2 h-4 w-4" }), "Link to shared room"] }))] }), _jsx(CardContent, { children: !group ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: "This booking is not linked to a shared-room group." })) : (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between rounded-md border bg-muted/30 p-3 text-sm", children: [_jsxs("div", { children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Group" }), _jsx("div", { className: "font-medium", children: group.label })] }), _jsx(Badge, { variant: "outline", className: "capitalize", children: group.kind.replace(/_/g, " ") })] }), _jsxs("div", { children: [_jsxs("div", { className: "mb-2 text-xs font-medium text-muted-foreground", children: ["Sibling bookings (", siblings.length, ")"] }), siblings.length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground", children: "No other bookings linked yet. Share the group id with another booking to link them." })) : (_jsx("ul", { className: "space-y-1", children: siblings.map((m) => (_jsxs("li", { className: "flex items-center justify-between rounded px-2 py-1 text-sm", children: [_jsx("span", { className: "font-mono text-xs", children: m.booking?.bookingNumber ?? m.bookingId }), _jsxs("div", { className: "flex items-center gap-2", children: [m.role === "primary" && (_jsx(Badge, { variant: "default", className: "text-xs", children: "Primary" })), m.booking?.status && (_jsx("span", { className: "text-xs text-muted-foreground capitalize", children: m.booking.status.replace(/_/g, " ") }))] })] }, m.id))) }))] })] })) }), _jsx(BookingGroupLinkDialog, { open: linkDialogOpen, onOpenChange: setLinkDialogOpen, bookingId: bookingId, productId: effectiveProductId, optionUnitId: effectiveOptionUnitId })] }));
31
+ }
@@ -0,0 +1,10 @@
1
+ import { type BookingGuaranteeRecord } from "@voyantjs/finance-react";
2
+ export interface BookingGuaranteeDialogProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ bookingId: string;
6
+ guarantee?: BookingGuaranteeRecord;
7
+ onSuccess?: () => void;
8
+ }
9
+ export declare function BookingGuaranteeDialog({ open, onOpenChange, bookingId, guarantee, onSuccess, }: BookingGuaranteeDialogProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=booking-guarantee-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-guarantee-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-guarantee-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,sBAAsB,EAA+B,MAAM,yBAAyB,CAAA;AA4DlG,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,sBAAsB,CAAA;IAClC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,wBAAgB,sBAAsB,CAAC,EACrC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,EACT,SAAS,GACV,EAAE,2BAA2B,2CAqL7B"}
@@ -0,0 +1,101 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useBookingGuaranteeMutation } from "@voyantjs/finance-react";
4
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea, } from "@voyantjs/voyant-ui/components";
5
+ import { CurrencyCombobox } from "@voyantjs/voyant-ui/components/currency-combobox";
6
+ import { DateTimePicker } from "@voyantjs/voyant-ui/components/date-time-picker";
7
+ import { zodResolver } from "@voyantjs/voyant-ui/lib/zod-resolver";
8
+ import { Loader2 } from "lucide-react";
9
+ import { useEffect } from "react";
10
+ import { useForm } from "react-hook-form";
11
+ import { z } from "zod/v4";
12
+ const guaranteeTypes = [
13
+ "deposit",
14
+ "credit_card",
15
+ "preauth",
16
+ "card_on_file",
17
+ "bank_transfer",
18
+ "voucher",
19
+ "agency_letter",
20
+ "other",
21
+ ];
22
+ const guaranteeStatuses = [
23
+ "pending",
24
+ "active",
25
+ "released",
26
+ "failed",
27
+ "cancelled",
28
+ "expired",
29
+ ];
30
+ const guaranteeFormSchema = z.object({
31
+ guaranteeType: z.enum(guaranteeTypes),
32
+ status: z.enum(guaranteeStatuses).default("pending"),
33
+ currency: z.string().min(3).max(3).optional().nullable(),
34
+ amountCents: z.coerce.number().int().min(0).optional().nullable(),
35
+ provider: z.string().optional().nullable(),
36
+ referenceNumber: z.string().optional().nullable(),
37
+ expiresAt: z.string().optional().nullable(),
38
+ notes: z.string().optional().nullable(),
39
+ });
40
+ export function BookingGuaranteeDialog({ open, onOpenChange, bookingId, guarantee, onSuccess, }) {
41
+ const isEditing = Boolean(guarantee);
42
+ const { create, update } = useBookingGuaranteeMutation(bookingId);
43
+ const form = useForm({
44
+ resolver: zodResolver(guaranteeFormSchema),
45
+ defaultValues: {
46
+ guaranteeType: "deposit",
47
+ status: "pending",
48
+ currency: "EUR",
49
+ amountCents: null,
50
+ provider: "",
51
+ referenceNumber: "",
52
+ expiresAt: "",
53
+ notes: "",
54
+ },
55
+ });
56
+ useEffect(() => {
57
+ if (open && guarantee) {
58
+ form.reset({
59
+ guaranteeType: guarantee.guaranteeType,
60
+ status: guarantee.status,
61
+ currency: guarantee.currency,
62
+ amountCents: guarantee.amountCents,
63
+ provider: guarantee.provider ?? "",
64
+ referenceNumber: guarantee.referenceNumber ?? "",
65
+ expiresAt: guarantee.expiresAt ? guarantee.expiresAt.slice(0, 16) : "",
66
+ notes: guarantee.notes ?? "",
67
+ });
68
+ }
69
+ else if (open) {
70
+ form.reset();
71
+ }
72
+ }, [form, open, guarantee]);
73
+ const onSubmit = async (values) => {
74
+ const payload = {
75
+ guaranteeType: values.guaranteeType,
76
+ status: values.status,
77
+ currency: values.currency || null,
78
+ amountCents: values.amountCents ?? null,
79
+ provider: values.provider || null,
80
+ referenceNumber: values.referenceNumber || null,
81
+ expiresAt: values.expiresAt || null,
82
+ notes: values.notes || null,
83
+ };
84
+ if (isEditing) {
85
+ await update.mutateAsync({ id: guarantee.id, input: payload });
86
+ }
87
+ else {
88
+ await create.mutateAsync(payload);
89
+ }
90
+ onOpenChange(false);
91
+ onSuccess?.();
92
+ };
93
+ const isSubmitting = create.isPending || update.isPending;
94
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Guarantee" : "Add Guarantee" }) }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "flex flex-1 flex-col overflow-hidden", children: [_jsxs(DialogBody, { className: "grid gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Type" }), _jsxs(Select, { items: guaranteeTypes.map((t) => ({ label: t.replace(/_/g, " "), value: t })), value: form.watch("guaranteeType"), onValueChange: (v) => form.setValue("guaranteeType", (v ?? "deposit")), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: guaranteeTypes.map((t) => (_jsx(SelectItem, { value: t, children: t.replace(/_/g, " ") }, t))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Status" }), _jsxs(Select, { items: guaranteeStatuses.map((s) => ({ label: s.replace(/_/g, " "), value: s })), value: form.watch("status"), onValueChange: (v) => form.setValue("status", (v ?? "pending")), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: guaranteeStatuses.map((s) => (_jsx(SelectItem, { value: s, children: s.replace(/_/g, " ") }, s))) })] })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Currency" }), _jsx(CurrencyCombobox, { value: form.watch("currency") || null, onChange: (next) => form.setValue("currency", next ?? "EUR", {
95
+ shouldValidate: true,
96
+ shouldDirty: true,
97
+ }) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Amount (cents)" }), _jsx(Input, { ...form.register("amountCents"), type: "number", min: 0 })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Provider" }), _jsx(Input, { ...form.register("provider"), placeholder: "Stripe, bank name..." })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Reference Number" }), _jsx(Input, { ...form.register("referenceNumber"), placeholder: "External reference..." })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Expires At" }), _jsx(DateTimePicker, { value: form.watch("expiresAt") || null, onChange: (next) => form.setValue("expiresAt", next ?? "", {
98
+ shouldValidate: true,
99
+ shouldDirty: true,
100
+ }), placeholder: "Select expiry date & time", className: "w-full" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Notes" }), _jsx(Textarea, { ...form.register("notes"), placeholder: "Guarantee notes..." })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", size: "sm", disabled: isSubmitting, children: [isSubmitting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), isEditing ? "Save Changes" : "Add Guarantee"] })] })] })] }) }));
101
+ }
@@ -0,0 +1,5 @@
1
+ export interface BookingGuaranteeListProps {
2
+ bookingId: string;
3
+ }
4
+ export declare function BookingGuaranteeList({ bookingId }: BookingGuaranteeListProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=booking-guarantee-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-guarantee-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-guarantee-list.tsx"],"names":[],"mappings":"AAkCA,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE,yBAAyB,2CAkH5E"}
@@ -0,0 +1,45 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useBookingGuaranteeMutation, useBookingGuarantees, } from "@voyantjs/finance-react";
4
+ import { Badge, Button, Card, CardContent, CardHeader, CardTitle, } from "@voyantjs/voyant-ui/components";
5
+ import { Pencil, Plus, ShieldCheck, Trash2 } from "lucide-react";
6
+ import * as React from "react";
7
+ import { BookingGuaranteeDialog } from "./booking-guarantee-dialog";
8
+ const statusVariant = {
9
+ pending: "outline",
10
+ active: "default",
11
+ released: "secondary",
12
+ failed: "destructive",
13
+ cancelled: "destructive",
14
+ expired: "secondary",
15
+ };
16
+ function formatAmount(cents, currency) {
17
+ if (cents == null || !currency)
18
+ return "-";
19
+ return `${(cents / 100).toFixed(2)} ${currency}`;
20
+ }
21
+ export function BookingGuaranteeList({ bookingId }) {
22
+ const [dialogOpen, setDialogOpen] = React.useState(false);
23
+ const [editing, setEditing] = React.useState(undefined);
24
+ const { data } = useBookingGuarantees(bookingId);
25
+ const { remove } = useBookingGuaranteeMutation(bookingId);
26
+ const guarantees = data?.data ?? [];
27
+ return (_jsxs(Card, { "data-slot": "booking-guarantee-list", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(ShieldCheck, { className: "h-4 w-4" }), "Guarantees"] }), _jsxs(Button, { size: "sm", onClick: () => {
28
+ setEditing(undefined);
29
+ setDialogOpen(true);
30
+ }, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Add Guarantee"] })] }), _jsx(CardContent, { children: guarantees.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: "No guarantees yet." })) : (_jsx("div", { className: "rounded border bg-background", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b text-muted-foreground", children: [_jsx("th", { className: "p-2 text-left font-medium", children: "Type" }), _jsx("th", { className: "p-2 text-left font-medium", children: "Status" }), _jsx("th", { className: "p-2 text-right font-medium", children: "Amount" }), _jsx("th", { className: "p-2 text-left font-medium", children: "Provider" }), _jsx("th", { className: "p-2 text-left font-medium", children: "Reference" }), _jsx("th", { className: "p-2 text-left font-medium", children: "Expires" }), _jsx("th", { className: "w-20 p-2" })] }) }), _jsx("tbody", { children: guarantees.map((g) => (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsx("td", { className: "p-2 capitalize", children: g.guaranteeType.replace(/_/g, " ") }), _jsx("td", { className: "p-2", children: _jsx(Badge, { variant: statusVariant[g.status] ?? "secondary", className: "capitalize", children: g.status.replace(/_/g, " ") }) }), _jsx("td", { className: "p-2 text-right font-mono", children: formatAmount(g.amountCents, g.currency) }), _jsx("td", { className: "p-2", children: g.provider ?? "-" }), _jsx("td", { className: "max-w-[150px] truncate p-2 font-mono text-xs", children: g.referenceNumber ?? "-" }), _jsx("td", { className: "p-2", children: g.expiresAt ? new Date(g.expiresAt).toLocaleDateString() : "-" }), _jsx("td", { className: "p-2", children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => {
31
+ setEditing(g);
32
+ setDialogOpen(true);
33
+ }, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => {
34
+ if (confirm("Delete this guarantee?")) {
35
+ remove.mutate(g.id);
36
+ }
37
+ }, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }) })] }, g.id))) })] }) })) }), _jsx(BookingGuaranteeDialog, { open: dialogOpen, onOpenChange: (nextOpen) => {
38
+ setDialogOpen(nextOpen);
39
+ if (!nextOpen) {
40
+ setEditing(undefined);
41
+ }
42
+ }, bookingId: bookingId, guarantee: editing, onSuccess: () => {
43
+ setEditing(undefined);
44
+ } })] }));
45
+ }
@@ -0,0 +1,10 @@
1
+ import { type BookingItemRecord } from "@voyantjs/bookings-react";
2
+ export interface BookingItemDialogProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ bookingId: string;
6
+ item?: BookingItemRecord;
7
+ onSuccess?: () => void;
8
+ }
9
+ export declare function BookingItemDialog({ open, onOpenChange, bookingId, item, onSuccess, }: BookingItemDialogProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=booking-item-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"booking-item-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-item-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,iBAAiB,EAA0B,MAAM,0BAA0B,CAAA;AA4DzF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,iBAAiB,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,IAAI,EACJ,SAAS,GACV,EAAE,sBAAsB,2CA8NxB"}