@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useBookingItemMutation } 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 { DatePicker } 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
|
+
const itemTypes = [
|
|
13
|
+
"unit",
|
|
14
|
+
"extra",
|
|
15
|
+
"service",
|
|
16
|
+
"fee",
|
|
17
|
+
"tax",
|
|
18
|
+
"discount",
|
|
19
|
+
"adjustment",
|
|
20
|
+
"accommodation",
|
|
21
|
+
"transport",
|
|
22
|
+
"other",
|
|
23
|
+
];
|
|
24
|
+
const itemStatuses = ["draft", "on_hold", "confirmed", "cancelled", "expired", "fulfilled"];
|
|
25
|
+
const bookingItemFormSchema = z.object({
|
|
26
|
+
title: z.string().min(1, "Title is required"),
|
|
27
|
+
itemType: z.enum(itemTypes).default("unit"),
|
|
28
|
+
status: z.enum(itemStatuses).default("draft"),
|
|
29
|
+
quantity: z.coerce.number().int().positive().default(1),
|
|
30
|
+
sellCurrency: z.string().min(3).max(3).default("EUR"),
|
|
31
|
+
unitSellAmountCents: z.coerce.number().int().optional().nullable(),
|
|
32
|
+
totalSellAmountCents: z.coerce.number().int().optional().nullable(),
|
|
33
|
+
costCurrency: z.string().min(3).max(3).optional().nullable(),
|
|
34
|
+
unitCostAmountCents: z.coerce.number().int().optional().nullable(),
|
|
35
|
+
totalCostAmountCents: z.coerce.number().int().optional().nullable(),
|
|
36
|
+
serviceDate: z.string().optional().nullable(),
|
|
37
|
+
description: z.string().optional().nullable(),
|
|
38
|
+
notes: z.string().optional().nullable(),
|
|
39
|
+
});
|
|
40
|
+
export function BookingItemDialog({ open, onOpenChange, bookingId, item, onSuccess, }) {
|
|
41
|
+
const isEditing = Boolean(item);
|
|
42
|
+
const { create, update } = useBookingItemMutation(bookingId);
|
|
43
|
+
const form = useForm({
|
|
44
|
+
resolver: zodResolver(bookingItemFormSchema),
|
|
45
|
+
defaultValues: {
|
|
46
|
+
title: "",
|
|
47
|
+
itemType: "unit",
|
|
48
|
+
status: "draft",
|
|
49
|
+
quantity: 1,
|
|
50
|
+
sellCurrency: "EUR",
|
|
51
|
+
unitSellAmountCents: null,
|
|
52
|
+
totalSellAmountCents: null,
|
|
53
|
+
costCurrency: null,
|
|
54
|
+
unitCostAmountCents: null,
|
|
55
|
+
totalCostAmountCents: null,
|
|
56
|
+
serviceDate: "",
|
|
57
|
+
description: "",
|
|
58
|
+
notes: "",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (open && item) {
|
|
63
|
+
form.reset({
|
|
64
|
+
title: item.title,
|
|
65
|
+
itemType: item.itemType,
|
|
66
|
+
status: item.status,
|
|
67
|
+
quantity: item.quantity,
|
|
68
|
+
sellCurrency: item.sellCurrency,
|
|
69
|
+
unitSellAmountCents: item.unitSellAmountCents,
|
|
70
|
+
totalSellAmountCents: item.totalSellAmountCents,
|
|
71
|
+
costCurrency: item.costCurrency,
|
|
72
|
+
unitCostAmountCents: item.unitCostAmountCents,
|
|
73
|
+
totalCostAmountCents: item.totalCostAmountCents,
|
|
74
|
+
serviceDate: item.serviceDate ?? "",
|
|
75
|
+
description: item.description ?? "",
|
|
76
|
+
notes: item.notes ?? "",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else if (open) {
|
|
80
|
+
form.reset();
|
|
81
|
+
}
|
|
82
|
+
}, [form, open, item]);
|
|
83
|
+
const onSubmit = async (values) => {
|
|
84
|
+
const payload = {
|
|
85
|
+
title: values.title,
|
|
86
|
+
itemType: values.itemType,
|
|
87
|
+
status: values.status,
|
|
88
|
+
quantity: values.quantity,
|
|
89
|
+
sellCurrency: values.sellCurrency,
|
|
90
|
+
unitSellAmountCents: values.unitSellAmountCents || null,
|
|
91
|
+
totalSellAmountCents: values.totalSellAmountCents || null,
|
|
92
|
+
costCurrency: values.costCurrency || null,
|
|
93
|
+
unitCostAmountCents: values.unitCostAmountCents || null,
|
|
94
|
+
totalCostAmountCents: values.totalCostAmountCents || null,
|
|
95
|
+
serviceDate: values.serviceDate || null,
|
|
96
|
+
description: values.description || null,
|
|
97
|
+
notes: values.notes || null,
|
|
98
|
+
};
|
|
99
|
+
if (isEditing) {
|
|
100
|
+
await update.mutateAsync({ id: item.id, input: payload });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
await create.mutateAsync(payload);
|
|
104
|
+
}
|
|
105
|
+
onOpenChange(false);
|
|
106
|
+
onSuccess?.();
|
|
107
|
+
};
|
|
108
|
+
const isSubmitting = create.isPending || update.isPending;
|
|
109
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Item" : "Add Item" }) }), _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: "flex flex-col gap-2", children: [_jsx(Label, { children: "Title" }), _jsx(Input, { ...form.register("title"), placeholder: "Room night, transfer, tour..." }), form.formState.errors.title && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.title.message }))] }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Type" }), _jsxs(Select, { items: itemTypes.map((t) => ({ label: t.replace("_", " "), value: t })), value: form.watch("itemType"), onValueChange: (v) => form.setValue("itemType", v), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: itemTypes.map((t) => (_jsx(SelectItem, { value: t, children: t.replace("_", " ") }, t))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Status" }), _jsxs(Select, { items: itemStatuses.map((s) => ({ label: s.replace("_", " "), value: s })), value: form.watch("status"), onValueChange: (v) => form.setValue("status", v), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: itemStatuses.map((s) => (_jsx(SelectItem, { value: s, children: s.replace("_", " ") }, s))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Quantity" }), _jsx(Input, { ...form.register("quantity"), type: "number", min: 1 })] })] }), _jsxs("div", { className: "grid grid-cols-3 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", {
|
|
110
|
+
shouldValidate: true,
|
|
111
|
+
shouldDirty: true,
|
|
112
|
+
}) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Unit Sell (cents)" }), _jsx(Input, { ...form.register("unitSellAmountCents"), type: "number", placeholder: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Total Sell (cents)" }), _jsx(Input, { ...form.register("totalSellAmountCents"), type: "number", placeholder: "0" })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Cost Currency" }), _jsx(CurrencyCombobox, { value: form.watch("costCurrency") || null, onChange: (next) => form.setValue("costCurrency", next ?? "EUR", {
|
|
113
|
+
shouldValidate: true,
|
|
114
|
+
shouldDirty: true,
|
|
115
|
+
}) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Unit Cost (cents)" }), _jsx(Input, { ...form.register("unitCostAmountCents"), type: "number", placeholder: "0" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Total Cost (cents)" }), _jsx(Input, { ...form.register("totalCostAmountCents"), type: "number", placeholder: "0" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Service Date" }), _jsx(DatePicker, { value: form.watch("serviceDate") || null, onChange: (next) => form.setValue("serviceDate", next ?? "", {
|
|
116
|
+
shouldValidate: true,
|
|
117
|
+
shouldDirty: true,
|
|
118
|
+
}), placeholder: "Select service date", className: "w-full" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Description" }), _jsx(Textarea, { ...form.register("description"), placeholder: "Item description..." })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Notes" }), _jsx(Textarea, { ...form.register("notes"), placeholder: "Internal 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 Item"] })] })] })] }) }));
|
|
119
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-item-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-item-list.tsx"],"names":[],"mappings":"AAmCA,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,EAAE,oBAAoB,2CAyIlE"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useBookingItemMutation, useBookingItems, } from "@voyantjs/bookings-react";
|
|
4
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, } from "@voyantjs/voyant-ui/components";
|
|
5
|
+
import { ChevronDown, ChevronRight, Package, Pencil, Plus, Trash2 } from "lucide-react";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { BookingItemDialog } from "./booking-item-dialog";
|
|
8
|
+
import { BookingItemTravelers } from "./booking-item-travelers";
|
|
9
|
+
const statusVariant = {
|
|
10
|
+
draft: "outline",
|
|
11
|
+
on_hold: "secondary",
|
|
12
|
+
confirmed: "default",
|
|
13
|
+
cancelled: "destructive",
|
|
14
|
+
expired: "secondary",
|
|
15
|
+
fulfilled: "default",
|
|
16
|
+
};
|
|
17
|
+
function formatAmount(cents, currency) {
|
|
18
|
+
if (cents == null)
|
|
19
|
+
return "-";
|
|
20
|
+
return `${(cents / 100).toFixed(2)} ${currency}`;
|
|
21
|
+
}
|
|
22
|
+
export function BookingItemList({ bookingId }) {
|
|
23
|
+
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
24
|
+
const [editing, setEditing] = React.useState(undefined);
|
|
25
|
+
const [expandedItemId, setExpandedItemId] = React.useState(null);
|
|
26
|
+
const { data } = useBookingItems(bookingId);
|
|
27
|
+
const { remove } = useBookingItemMutation(bookingId);
|
|
28
|
+
const items = data?.data ?? [];
|
|
29
|
+
return (_jsxs(Card, { "data-slot": "booking-item-list", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(Package, { className: "h-4 w-4" }), "Items"] }), _jsxs(Button, { size: "sm", onClick: () => {
|
|
30
|
+
setEditing(undefined);
|
|
31
|
+
setDialogOpen(true);
|
|
32
|
+
}, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Add Item"] })] }), _jsx(CardContent, { children: items.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: "No items 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: "w-8 p-2" }), _jsx("th", { className: "p-2 text-left font-medium", children: "Title" }), _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: "Qty" }), _jsx("th", { className: "p-2 text-right font-medium", children: "Total" }), _jsx("th", { className: "p-2 text-left font-medium", children: "Service Date" }), _jsx("th", { className: "w-20 p-2" })] }) }), _jsx("tbody", { children: items.map((item) => {
|
|
33
|
+
const isExpanded = expandedItemId === item.id;
|
|
34
|
+
return (_jsxs(React.Fragment, { children: [_jsxs("tr", { className: "border-b", children: [_jsx("td", { className: "p-2", children: _jsx("button", { type: "button", onClick: () => setExpandedItemId(isExpanded ? null : item.id), className: "text-muted-foreground hover:text-foreground", children: isExpanded ? (_jsx(ChevronDown, { className: "h-3.5 w-3.5" })) : (_jsx(ChevronRight, { className: "h-3.5 w-3.5" })) }) }), _jsx("td", { className: "p-2 font-medium", children: item.title }), _jsx("td", { className: "p-2 capitalize", children: item.itemType.replace("_", " ") }), _jsx("td", { className: "p-2", children: _jsx(Badge, { variant: statusVariant[item.status] ?? "secondary", className: "capitalize", children: item.status.replace("_", " ") }) }), _jsx("td", { className: "p-2 text-right font-mono", children: item.quantity }), _jsx("td", { className: "p-2 text-right font-mono", children: formatAmount(item.totalSellAmountCents, item.sellCurrency) }), _jsx("td", { className: "p-2", children: item.serviceDate ?? "-" }), _jsx("td", { className: "p-2", children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => {
|
|
35
|
+
setEditing(item);
|
|
36
|
+
setDialogOpen(true);
|
|
37
|
+
}, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => {
|
|
38
|
+
if (confirm("Delete this item?")) {
|
|
39
|
+
remove.mutate(item.id);
|
|
40
|
+
}
|
|
41
|
+
}, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }) })] }), isExpanded && (_jsx("tr", { className: "border-b last:border-b-0", children: _jsx("td", { colSpan: 8, className: "p-2", children: _jsx(BookingItemTravelers, { bookingId: bookingId, itemId: item.id }) }) }))] }, item.id));
|
|
42
|
+
}) })] }) })) }), _jsx(BookingItemDialog, { open: dialogOpen, onOpenChange: (nextOpen) => {
|
|
43
|
+
setDialogOpen(nextOpen);
|
|
44
|
+
if (!nextOpen) {
|
|
45
|
+
setEditing(undefined);
|
|
46
|
+
}
|
|
47
|
+
}, bookingId: bookingId, item: editing, onSuccess: () => {
|
|
48
|
+
setEditing(undefined);
|
|
49
|
+
} })] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface BookingItemTravelersProps {
|
|
2
|
+
bookingId: string;
|
|
3
|
+
itemId: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function BookingItemTravelers({ bookingId, itemId }: BookingItemTravelersProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=booking-item-travelers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-item-travelers.d.ts","sourceRoot":"","sources":["../../src/components/booking-item-travelers.tsx"],"names":[],"mappings":"AA8BA,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,yBAAyB,2CAuIpF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useBookingItemTravelerMutation, useBookingItemTravelers, useTravelers, } from "@voyantjs/bookings-react";
|
|
4
|
+
import { Badge, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/voyant-ui/components";
|
|
5
|
+
import { Plus, Trash2, UserCheck } from "lucide-react";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
const roles = [
|
|
8
|
+
"traveler",
|
|
9
|
+
"occupant",
|
|
10
|
+
"primary_contact",
|
|
11
|
+
"service_assignee",
|
|
12
|
+
"beneficiary",
|
|
13
|
+
"other",
|
|
14
|
+
];
|
|
15
|
+
export function BookingItemTravelers({ bookingId, itemId }) {
|
|
16
|
+
const { data: travelerLinksData } = useBookingItemTravelers(bookingId, itemId);
|
|
17
|
+
const { data: travelersData } = useTravelers(bookingId);
|
|
18
|
+
const { add, remove } = useBookingItemTravelerMutation(bookingId, itemId);
|
|
19
|
+
const [selectedTravelerId, setSelectedTravelerId] = React.useState("");
|
|
20
|
+
const [selectedRole, setSelectedRole] = React.useState("traveler");
|
|
21
|
+
const assignedTravelers = travelerLinksData?.data ?? [];
|
|
22
|
+
const travelers = travelersData?.data ?? [];
|
|
23
|
+
const assignedIds = new Set(assignedTravelers.map((link) => link.travelerId));
|
|
24
|
+
const availableTravelers = travelers.filter((traveler) => !assignedIds.has(traveler.id));
|
|
25
|
+
const travelerMap = new Map();
|
|
26
|
+
for (const traveler of travelers) {
|
|
27
|
+
travelerMap.set(traveler.id, traveler);
|
|
28
|
+
}
|
|
29
|
+
const handleAssign = () => {
|
|
30
|
+
if (!selectedTravelerId)
|
|
31
|
+
return;
|
|
32
|
+
add.mutate({ travelerId: selectedTravelerId, role: selectedRole }, {
|
|
33
|
+
onSuccess: () => {
|
|
34
|
+
setSelectedTravelerId("");
|
|
35
|
+
setSelectedRole("traveler");
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
return (_jsxs("div", { className: "space-y-3 rounded-md border bg-muted/30 p-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs font-medium text-muted-foreground", children: [_jsx(UserCheck, { className: "h-3.5 w-3.5" }), "Assigned Travelers"] }), assignedTravelers.length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground", children: "No travelers assigned to this item." })) : (_jsx("div", { className: "space-y-1", children: assignedTravelers.map((link) => {
|
|
40
|
+
const traveler = travelerMap.get(link.travelerId);
|
|
41
|
+
return (_jsxs("div", { className: "flex items-center justify-between rounded px-2 py-1 text-sm", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { children: traveler ? `${traveler.firstName} ${traveler.lastName}` : link.travelerId }), _jsx(Badge, { variant: "outline", className: "text-xs capitalize", children: link.role.replace("_", " ") }), link.isPrimary && (_jsx(Badge, { variant: "default", className: "text-xs", children: "Primary" }))] }), _jsx("button", { type: "button", onClick: () => {
|
|
42
|
+
if (confirm("Remove this traveler from the item?")) {
|
|
43
|
+
remove.mutate(link.id);
|
|
44
|
+
}
|
|
45
|
+
}, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }, link.id));
|
|
46
|
+
}) })), availableTravelers.length > 0 && (_jsxs("div", { className: "flex items-end gap-2 border-t pt-3", children: [_jsx("div", { className: "flex-1", children: _jsxs(Select, { items: availableTravelers.map((traveler) => ({
|
|
47
|
+
label: `${traveler.firstName} ${traveler.lastName}`,
|
|
48
|
+
value: traveler.id,
|
|
49
|
+
})), value: selectedTravelerId, onValueChange: (v) => setSelectedTravelerId(v ?? ""), children: [_jsx(SelectTrigger, { className: "w-full h-8 text-xs", children: _jsx(SelectValue, { placeholder: "Select traveler..." }) }), _jsx(SelectContent, { children: availableTravelers.map((traveler) => (_jsxs(SelectItem, { value: traveler.id, children: [traveler.firstName, " ", traveler.lastName] }, traveler.id))) })] }) }), _jsx("div", { className: "w-36", children: _jsxs(Select, { items: roles.map((r) => ({ label: r.replace("_", " "), value: r })), value: selectedRole, onValueChange: (v) => setSelectedRole(v ?? "traveler"), children: [_jsx(SelectTrigger, { className: "w-full h-8 text-xs", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: roles.map((r) => (_jsx(SelectItem, { value: r, children: r.replace("_", " ") }, r))) })] }) }), _jsxs(Button, { size: "sm", variant: "outline", className: "h-8", onClick: handleAssign, disabled: !selectedTravelerId || add.isPending, children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), "Assign"] })] }))] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type BookingRecord } from "@voyantjs/bookings-react";
|
|
2
|
+
export interface BookingListProps {
|
|
3
|
+
pageSize?: number;
|
|
4
|
+
onSelectBooking?: (booking: BookingRecord) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function BookingList({ pageSize, onSelectBooking }?: BookingListProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=booking-list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-list.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,aAAa,EAInB,MAAM,0BAA0B,CAAA;AAiBjC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;CACnD;AAOD,wBAAgB,WAAW,CAAC,EAAE,QAAa,EAAE,eAAe,EAAE,GAAE,gBAAqB,2CAkJpF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { bookingStatusBadgeVariant, formatBookingStatus, useBookings, } from "@voyantjs/bookings-react";
|
|
4
|
+
import { Badge } from "@voyantjs/voyant-ui/components/badge";
|
|
5
|
+
import { Button } from "@voyantjs/voyant-ui/components/button";
|
|
6
|
+
import { Input } from "@voyantjs/voyant-ui/components/input";
|
|
7
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/voyant-ui/components/table";
|
|
8
|
+
import { Loader2, Plus, Search } from "lucide-react";
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
import { BookingDialog } from "./booking-dialog";
|
|
11
|
+
function formatAmount(cents, currency) {
|
|
12
|
+
if (cents == null)
|
|
13
|
+
return "—";
|
|
14
|
+
return `${(cents / 100).toFixed(2)} ${currency}`;
|
|
15
|
+
}
|
|
16
|
+
export function BookingList({ pageSize = 25, onSelectBooking } = {}) {
|
|
17
|
+
const [search, setSearch] = React.useState("");
|
|
18
|
+
const [offset, setOffset] = React.useState(0);
|
|
19
|
+
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
20
|
+
const [editing, setEditing] = React.useState(undefined);
|
|
21
|
+
const { data, isPending, isError } = useBookings({
|
|
22
|
+
search: search || undefined,
|
|
23
|
+
limit: pageSize,
|
|
24
|
+
offset,
|
|
25
|
+
});
|
|
26
|
+
const bookings = data?.data ?? [];
|
|
27
|
+
const total = data?.total ?? 0;
|
|
28
|
+
const page = Math.floor(offset / pageSize) + 1;
|
|
29
|
+
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
30
|
+
const handleSelect = (booking) => {
|
|
31
|
+
if (onSelectBooking) {
|
|
32
|
+
onSelectBooking(booking);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setEditing(booking);
|
|
36
|
+
setDialogOpen(true);
|
|
37
|
+
};
|
|
38
|
+
return (_jsxs("div", { "data-slot": "booking-list", className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { className: "relative w-full max-w-sm", children: [_jsx(Search, { className: "absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: "Search bookings\u2026", value: search, onChange: (event) => {
|
|
39
|
+
setSearch(event.target.value);
|
|
40
|
+
setOffset(0);
|
|
41
|
+
}, className: "pl-9" })] }), _jsx("div", { className: "flex items-center gap-2", children: _jsxs(Button, { onClick: () => {
|
|
42
|
+
setEditing(undefined);
|
|
43
|
+
setDialogOpen(true);
|
|
44
|
+
}, children: [_jsx(Plus, { className: "mr-2 size-4" }), "New booking"] }) })] }), _jsx("div", { className: "rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Booking #" }), _jsx(TableHead, { children: "Status" }), _jsx(TableHead, { children: "Sell Amount" }), _jsx(TableHead, { children: "Pax" }), _jsx(TableHead, { children: "Start Date" })] }) }), _jsx(TableBody, { children: isPending ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "h-24 text-center", children: _jsx(Loader2, { className: "mx-auto size-4 animate-spin text-muted-foreground" }) }) })) : isError ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "h-24 text-center text-sm text-destructive", children: "Failed to load bookings." }) })) : bookings.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "h-24 text-center text-sm text-muted-foreground", children: "No bookings found." }) })) : (bookings.map((booking) => (_jsxs(TableRow, { onClick: () => handleSelect(booking), className: "cursor-pointer", children: [_jsx(TableCell, { className: "font-medium", children: booking.bookingNumber }), _jsx(TableCell, { children: _jsx(Badge, { variant: bookingStatusBadgeVariant[booking.status], children: formatBookingStatus(booking.status) }) }), _jsx(TableCell, { children: formatAmount(booking.sellAmountCents, booking.sellCurrency) }), _jsx(TableCell, { children: booking.pax ?? "—" }), _jsx(TableCell, { children: booking.startDate ?? "—" })] }, booking.id)))) })] }) }), _jsxs("div", { className: "flex items-center justify-between text-sm text-muted-foreground", children: [_jsxs("span", { children: ["Showing ", bookings.length, " of ", total] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", disabled: offset === 0, onClick: () => setOffset((prev) => Math.max(0, prev - pageSize)), children: "Previous" }), _jsxs("span", { children: ["Page ", page, " / ", pageCount] }), _jsx(Button, { variant: "outline", size: "sm", disabled: offset + pageSize >= total, onClick: () => setOffset((prev) => prev + pageSize), children: "Next" })] })] }), _jsx(BookingDialog, { open: dialogOpen, onOpenChange: setDialogOpen, booking: editing, onSuccess: (booking) => {
|
|
45
|
+
onSelectBooking?.(booking);
|
|
46
|
+
} })] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-notes.d.ts","sourceRoot":"","sources":["../../src/components/booking-notes.tsx"],"names":[],"mappings":"AAcA,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,YAAY,CAAC,EAAE,SAAS,EAAE,EAAE,iBAAiB,2CA+C5D"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useBookingNoteMutation, useBookingNotes } from "@voyantjs/bookings-react";
|
|
4
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, Textarea, } from "@voyantjs/voyant-ui/components";
|
|
5
|
+
import { Loader2 } from "lucide-react";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
export function BookingNotes({ bookingId }) {
|
|
8
|
+
const [content, setContent] = React.useState("");
|
|
9
|
+
const { data } = useBookingNotes(bookingId);
|
|
10
|
+
const mutation = useBookingNoteMutation(bookingId);
|
|
11
|
+
const notes = data?.data ?? [];
|
|
12
|
+
return (_jsxs(Card, { "data-slot": "booking-notes", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Notes" }) }), _jsxs(CardContent, { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex gap-2", children: [_jsx(Textarea, { placeholder: "Add a note...", value: content, onChange: (event) => setContent(event.target.value), className: "min-h-[80px]" }), _jsx(Button, { className: "self-end", disabled: !content.trim() || mutation.isPending, onClick: async () => {
|
|
13
|
+
await mutation.mutateAsync({ content: content.trim() });
|
|
14
|
+
setContent("");
|
|
15
|
+
}, children: mutation.isPending ? _jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : "Add" })] }), notes.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: "No notes yet." })) : (notes.map((note) => (_jsxs("div", { className: "rounded-md border p-3", children: [_jsx("p", { className: "whitespace-pre-wrap text-sm", children: note.content }), _jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: new Date(note.createdAt).toLocaleString() })] }, note.id))))] })] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type BookingPaymentScheduleRecord } from "@voyantjs/finance-react";
|
|
2
|
+
export interface BookingPaymentScheduleDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
bookingId: string;
|
|
6
|
+
schedule?: BookingPaymentScheduleRecord;
|
|
7
|
+
onSuccess?: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function BookingPaymentScheduleDialog({ open, onOpenChange, bookingId, schedule, onSuccess, }: BookingPaymentScheduleDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=booking-payment-schedule-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-payment-schedule-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-payment-schedule-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,4BAA4B,EAElC,MAAM,yBAAyB,CAAA;AAyChC,MAAM,WAAW,iCAAiC;IAChD,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,4BAA4B,CAAA;IACvC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,wBAAgB,4BAA4B,CAAC,EAC3C,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,SAAS,GACV,EAAE,iCAAiC,2CA4KnC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useBookingPaymentScheduleMutation, } 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 { DatePicker } 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
|
+
const scheduleTypes = ["deposit", "installment", "balance", "hold", "other"];
|
|
13
|
+
const scheduleStatuses = ["pending", "due", "paid", "waived", "cancelled", "expired"];
|
|
14
|
+
const scheduleFormSchema = z.object({
|
|
15
|
+
scheduleType: z.enum(scheduleTypes).default("balance"),
|
|
16
|
+
status: z.enum(scheduleStatuses).default("pending"),
|
|
17
|
+
dueDate: z.string().min(1, "Due date is required"),
|
|
18
|
+
currency: z.string().min(3).max(3).default("EUR"),
|
|
19
|
+
amountCents: z.coerce.number().int().min(0, "Amount is required"),
|
|
20
|
+
notes: z.string().optional().nullable(),
|
|
21
|
+
});
|
|
22
|
+
export function BookingPaymentScheduleDialog({ open, onOpenChange, bookingId, schedule, onSuccess, }) {
|
|
23
|
+
const isEditing = Boolean(schedule);
|
|
24
|
+
const { create, update } = useBookingPaymentScheduleMutation(bookingId);
|
|
25
|
+
const form = useForm({
|
|
26
|
+
resolver: zodResolver(scheduleFormSchema),
|
|
27
|
+
defaultValues: {
|
|
28
|
+
scheduleType: "balance",
|
|
29
|
+
status: "pending",
|
|
30
|
+
dueDate: "",
|
|
31
|
+
currency: "EUR",
|
|
32
|
+
amountCents: 0,
|
|
33
|
+
notes: "",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (open && schedule) {
|
|
38
|
+
form.reset({
|
|
39
|
+
scheduleType: schedule.scheduleType,
|
|
40
|
+
status: schedule.status,
|
|
41
|
+
dueDate: schedule.dueDate,
|
|
42
|
+
currency: schedule.currency,
|
|
43
|
+
amountCents: schedule.amountCents,
|
|
44
|
+
notes: schedule.notes ?? "",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
else if (open) {
|
|
48
|
+
form.reset();
|
|
49
|
+
}
|
|
50
|
+
}, [form, open, schedule]);
|
|
51
|
+
const onSubmit = async (values) => {
|
|
52
|
+
const payload = {
|
|
53
|
+
scheduleType: values.scheduleType,
|
|
54
|
+
status: values.status,
|
|
55
|
+
dueDate: values.dueDate,
|
|
56
|
+
currency: values.currency,
|
|
57
|
+
amountCents: values.amountCents,
|
|
58
|
+
notes: values.notes || null,
|
|
59
|
+
};
|
|
60
|
+
if (isEditing) {
|
|
61
|
+
await update.mutateAsync({ id: schedule.id, input: payload });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
await create.mutateAsync(payload);
|
|
65
|
+
}
|
|
66
|
+
onOpenChange(false);
|
|
67
|
+
onSuccess?.();
|
|
68
|
+
};
|
|
69
|
+
const isSubmitting = create.isPending || update.isPending;
|
|
70
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Payment Schedule" : "Add Payment Schedule" }) }), _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: scheduleTypes.map((t) => ({ label: t.replace("_", " "), value: t })), value: form.watch("scheduleType"), onValueChange: (v) => form.setValue("scheduleType", (v ?? "balance")), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: scheduleTypes.map((t) => (_jsx(SelectItem, { value: t, children: t.replace("_", " ") }, t))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Status" }), _jsxs(Select, { items: scheduleStatuses.map((s) => ({ label: s.replace("_", " "), 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: scheduleStatuses.map((s) => (_jsx(SelectItem, { value: s, children: s.replace("_", " ") }, s))) })] })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Due Date" }), _jsx(DatePicker, { value: form.watch("dueDate") || null, onChange: (next) => form.setValue("dueDate", next ?? "", {
|
|
71
|
+
shouldValidate: true,
|
|
72
|
+
shouldDirty: true,
|
|
73
|
+
}), placeholder: "Select due date", className: "w-full" }), form.formState.errors.dueDate && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.dueDate.message }))] }), _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", {
|
|
74
|
+
shouldValidate: true,
|
|
75
|
+
shouldDirty: true,
|
|
76
|
+
}) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Amount (cents)" }), _jsx(Input, { ...form.register("amountCents"), type: "number", min: 0 }), form.formState.errors.amountCents && (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.amountCents.message }))] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Notes" }), _jsx(Textarea, { ...form.register("notes"), placeholder: "Payment 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 Schedule"] })] })] })] }) }));
|
|
77
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export interface BookingPaymentScheduleListProps {
|
|
2
|
+
bookingId: string;
|
|
3
|
+
}
|
|
4
|
+
export declare function BookingPaymentScheduleList({ bookingId }: BookingPaymentScheduleListProps): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
//# sourceMappingURL=booking-payment-schedule-list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-payment-schedule-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-payment-schedule-list.tsx"],"names":[],"mappings":"AAiCA,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,0BAA0B,CAAC,EAAE,SAAS,EAAE,EAAE,+BAA+B,2CAgHxF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useBookingPaymentScheduleMutation, useBookingPaymentSchedules, } from "@voyantjs/finance-react";
|
|
4
|
+
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, } from "@voyantjs/voyant-ui/components";
|
|
5
|
+
import { CalendarClock, Pencil, Plus, Trash2 } from "lucide-react";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { BookingPaymentScheduleDialog } from "./booking-payment-schedule-dialog";
|
|
8
|
+
const statusVariant = {
|
|
9
|
+
pending: "outline",
|
|
10
|
+
due: "secondary",
|
|
11
|
+
paid: "default",
|
|
12
|
+
waived: "secondary",
|
|
13
|
+
cancelled: "destructive",
|
|
14
|
+
expired: "secondary",
|
|
15
|
+
};
|
|
16
|
+
function formatAmount(cents, currency) {
|
|
17
|
+
return `${(cents / 100).toFixed(2)} ${currency}`;
|
|
18
|
+
}
|
|
19
|
+
export function BookingPaymentScheduleList({ bookingId }) {
|
|
20
|
+
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
21
|
+
const [editing, setEditing] = React.useState(undefined);
|
|
22
|
+
const { data } = useBookingPaymentSchedules(bookingId);
|
|
23
|
+
const { remove } = useBookingPaymentScheduleMutation(bookingId);
|
|
24
|
+
const schedules = data?.data ?? [];
|
|
25
|
+
return (_jsxs(Card, { "data-slot": "booking-payment-schedule-list", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between", children: [_jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(CalendarClock, { className: "h-4 w-4" }), "Payment Schedule"] }), _jsxs(Button, { size: "sm", onClick: () => {
|
|
26
|
+
setEditing(undefined);
|
|
27
|
+
setDialogOpen(true);
|
|
28
|
+
}, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Add Schedule"] })] }), _jsx(CardContent, { children: schedules.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: "No payment schedules 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-left font-medium", children: "Due Date" }), _jsx("th", { className: "p-2 text-right font-medium", children: "Amount" }), _jsx("th", { className: "p-2 text-left font-medium", children: "Notes" }), _jsx("th", { className: "w-20 p-2" })] }) }), _jsx("tbody", { children: schedules.map((schedule) => (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsx("td", { className: "p-2 capitalize", children: schedule.scheduleType.replace("_", " ") }), _jsx("td", { className: "p-2", children: _jsx(Badge, { variant: statusVariant[schedule.status] ?? "secondary", className: "capitalize", children: schedule.status.replace("_", " ") }) }), _jsx("td", { className: "p-2", children: schedule.dueDate }), _jsx("td", { className: "p-2 text-right font-mono", children: formatAmount(schedule.amountCents, schedule.currency) }), _jsx("td", { className: "max-w-[200px] truncate p-2 text-muted-foreground", children: schedule.notes ?? "-" }), _jsx("td", { className: "p-2", children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => {
|
|
29
|
+
setEditing(schedule);
|
|
30
|
+
setDialogOpen(true);
|
|
31
|
+
}, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => {
|
|
32
|
+
if (confirm("Delete this payment schedule?")) {
|
|
33
|
+
remove.mutate(schedule.id);
|
|
34
|
+
}
|
|
35
|
+
}, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }) })] }, schedule.id))) })] }) })) }), _jsx(BookingPaymentScheduleDialog, { open: dialogOpen, onOpenChange: (nextOpen) => {
|
|
36
|
+
setDialogOpen(nextOpen);
|
|
37
|
+
if (!nextOpen) {
|
|
38
|
+
setEditing(undefined);
|
|
39
|
+
}
|
|
40
|
+
}, bookingId: bookingId, schedule: editing, onSuccess: () => {
|
|
41
|
+
setEditing(undefined);
|
|
42
|
+
} })] }));
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-payments-summary.d.ts","sourceRoot":"","sources":["../../src/components/booking-payments-summary.tsx"],"names":[],"mappings":"AAiBA,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,sBAAsB,CAAC,EAAE,SAAS,EAAE,EAAE,2BAA2B,2CA0DhF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { usePublicBookingPayments } from "@voyantjs/finance-react";
|
|
4
|
+
import { Badge, Card, CardContent, CardHeader, CardTitle } from "@voyantjs/voyant-ui/components";
|
|
5
|
+
import { CreditCard } from "lucide-react";
|
|
6
|
+
const statusVariant = {
|
|
7
|
+
pending: "outline",
|
|
8
|
+
completed: "default",
|
|
9
|
+
failed: "destructive",
|
|
10
|
+
refunded: "secondary",
|
|
11
|
+
};
|
|
12
|
+
function formatAmount(cents, currency) {
|
|
13
|
+
return `${(cents / 100).toFixed(2)} ${currency}`;
|
|
14
|
+
}
|
|
15
|
+
export function BookingPaymentsSummary({ bookingId }) {
|
|
16
|
+
const { data } = usePublicBookingPayments(bookingId);
|
|
17
|
+
const payments = data?.data?.payments ?? [];
|
|
18
|
+
return (_jsxs(Card, { "data-slot": "booking-payments-summary", children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { className: "flex items-center gap-2", children: [_jsx(CreditCard, { className: "h-4 w-4" }), "Payments"] }) }), _jsx(CardContent, { children: payments.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: "No payments recorded." })) : (_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: "Invoice" }), _jsx("th", { className: "p-2 text-left font-medium", children: "Method" }), _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: "Date" }), _jsx("th", { className: "p-2 text-left font-medium", children: "Reference" })] }) }), _jsx("tbody", { children: payments.map((payment) => (_jsxs("tr", { className: "border-b last:border-b-0", children: [_jsx("td", { className: "p-2 font-mono text-xs", children: payment.invoiceNumber }), _jsx("td", { className: "p-2 capitalize", children: payment.paymentMethod.replace(/_/g, " ") }), _jsx("td", { className: "p-2", children: _jsx(Badge, { variant: statusVariant[payment.status] ?? "secondary", className: "capitalize", children: payment.status }) }), _jsx("td", { className: "p-2 text-right font-mono", children: formatAmount(payment.amountCents, payment.currency) }), _jsx("td", { className: "p-2", children: new Date(payment.paymentDate).toLocaleDateString() }), _jsx("td", { className: "max-w-[150px] truncate p-2 font-mono text-xs", children: payment.referenceNumber ?? "-" })] }, payment.id))) })] }) })) })] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface UploadedFile {
|
|
2
|
+
key: string;
|
|
3
|
+
url: string;
|
|
4
|
+
mimeType: string;
|
|
5
|
+
size: number;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FileDropzoneProps {
|
|
9
|
+
/** URL of the upload endpoint (defaults to /api/v1/uploads). */
|
|
10
|
+
uploadUrl?: string;
|
|
11
|
+
/** MIME types or extensions to accept (same format as <input accept>). */
|
|
12
|
+
accept?: string;
|
|
13
|
+
/** Maximum file size in bytes. */
|
|
14
|
+
maxSize?: number;
|
|
15
|
+
/** Called after a successful upload. */
|
|
16
|
+
onUploaded: (file: UploadedFile) => void;
|
|
17
|
+
/** Called when an error occurs (validation, upload failure). */
|
|
18
|
+
onError?: (message: string) => void;
|
|
19
|
+
/** Helper text shown in the idle state. */
|
|
20
|
+
helperText?: string;
|
|
21
|
+
/** Disable interaction. */
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function FileDropzone({ uploadUrl, accept, maxSize, onUploaded, onError, helperText, disabled, }: FileDropzoneProps): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
//# sourceMappingURL=file-dropzone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-dropzone.d.ts","sourceRoot":"","sources":["../../src/components/file-dropzone.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kCAAkC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,wCAAwC;IACxC,UAAU,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAA;IACxC,gEAAgE;IAChE,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAQD,wBAAgB,YAAY,CAAC,EAC3B,SAA6B,EAC7B,MAAM,EACN,OAAO,EACP,UAAU,EACV,OAAO,EACP,UAA4D,EAC5D,QAAQ,GACT,EAAE,iBAAiB,2CAgJnB"}
|