@voyantjs/bookings-ui 0.52.1 → 0.52.3
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/dist/components/booking-billing-dialog.d.ts +16 -0
- package/dist/components/booking-billing-dialog.d.ts.map +1 -0
- package/dist/components/booking-billing-dialog.js +90 -0
- package/dist/components/booking-create-dialog.d.ts.map +1 -1
- package/dist/components/booking-create-dialog.js +512 -151
- package/dist/components/booking-create-page.js +1 -1
- package/dist/components/booking-document-dialog.d.ts.map +1 -1
- package/dist/components/booking-document-dialog.js +16 -14
- package/dist/components/booking-guarantee-dialog.d.ts.map +1 -1
- package/dist/components/booking-guarantee-dialog.js +10 -8
- package/dist/components/booking-item-dialog.d.ts.map +1 -1
- package/dist/components/booking-item-dialog.js +18 -9
- package/dist/components/booking-item-travelers.d.ts.map +1 -1
- package/dist/components/booking-item-travelers.js +9 -7
- package/dist/components/booking-payment-schedule-dialog.d.ts.map +1 -1
- package/dist/components/booking-payment-schedule-dialog.js +10 -8
- package/dist/components/booking-payment-schedule-list.d.ts.map +1 -1
- package/dist/components/booking-payment-schedule-list.js +32 -3
- package/dist/components/{rooms-stepper-section.d.ts → option-units-stepper-section.d.ts} +17 -9
- package/dist/components/option-units-stepper-section.d.ts.map +1 -0
- package/dist/components/option-units-stepper-section.js +172 -0
- package/dist/components/payment-schedule-section.d.ts +1 -1
- package/dist/components/payment-schedule-section.d.ts.map +1 -1
- package/dist/components/payment-schedule-section.js +5 -11
- package/dist/components/person-picker-section.d.ts +4 -0
- package/dist/components/person-picker-section.d.ts.map +1 -1
- package/dist/components/person-picker-section.js +27 -5
- package/dist/components/price-breakdown-section.d.ts +8 -2
- package/dist/components/price-breakdown-section.d.ts.map +1 -1
- package/dist/components/price-breakdown-section.js +17 -5
- package/dist/components/status-change-dialog.d.ts.map +1 -1
- package/dist/components/status-change-dialog.js +6 -5
- package/dist/components/supplier-status-dialog.d.ts.map +1 -1
- package/dist/components/supplier-status-dialog.js +6 -5
- package/dist/components/traveler-list.d.ts.map +1 -1
- package/dist/components/traveler-list.js +12 -1
- package/dist/components/travelers-section.d.ts +62 -3
- package/dist/components/travelers-section.d.ts.map +1 -1
- package/dist/components/travelers-section.js +290 -23
- package/dist/i18n/en.d.ts +63 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +68 -5
- package/dist/i18n/messages.d.ts +63 -0
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/provider.d.ts +126 -0
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +63 -0
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +68 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +26 -24
- package/dist/components/rooms-stepper-section.d.ts.map +0 -1
- package/dist/components/rooms-stepper-section.js +0 -111
|
@@ -7,5 +7,5 @@ import { BookingCreateForm } from "./booking-create-dialog.js";
|
|
|
7
7
|
*/
|
|
8
8
|
export function BookingCreatePage({ onCreated, onCancel, defaultProductId, }) {
|
|
9
9
|
const messages = useBookingsUiMessagesOrDefault();
|
|
10
|
-
return (_jsxs("main", { className: "mx-auto flex w-full max-w-
|
|
10
|
+
return (_jsxs("main", { className: "mx-auto flex w-full max-w-screen-2xl flex-col gap-6 px-4 py-6 sm:px-6 lg:px-8", children: [_jsxs("header", { className: "flex flex-col gap-1", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-normal", children: messages.bookingCreatePage.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: messages.bookingCreatePage.description })] }), _jsx("section", { className: "flex flex-col gap-4", children: _jsx(BookingCreateForm, { onCreated: onCreated, onCancel: onCancel, defaultProductId: defaultProductId }) })] }));
|
|
11
11
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-document-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-document-dialog.tsx"],"names":[],"mappings":"AAkDA,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,
|
|
1
|
+
{"version":3,"file":"booking-document-dialog.d.ts","sourceRoot":"","sources":["../../src/components/booking-document-dialog.tsx"],"names":[],"mappings":"AAkDA,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,2CAyK5B"}
|
|
@@ -5,7 +5,7 @@ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader,
|
|
|
5
5
|
import { DatePicker } from "@voyantjs/ui/components/date-picker";
|
|
6
6
|
import { zodResolver } from "@voyantjs/ui/lib/zod-resolver";
|
|
7
7
|
import { Loader2 } from "lucide-react";
|
|
8
|
-
import { useEffect } from "react";
|
|
8
|
+
import { useEffect, useMemo } from "react";
|
|
9
9
|
import { useForm } from "react-hook-form";
|
|
10
10
|
import { z } from "zod/v4";
|
|
11
11
|
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
@@ -31,6 +31,20 @@ export function BookingDocumentDialog({ open, onOpenChange, bookingId, onSuccess
|
|
|
31
31
|
const travelers = travelersData?.data ?? [];
|
|
32
32
|
const messages = useBookingsUiMessagesOrDefault();
|
|
33
33
|
const documentFormSchema = createDocumentFormSchema(messages);
|
|
34
|
+
const typeItems = useMemo(() => documentTypes.map((t) => ({
|
|
35
|
+
value: t,
|
|
36
|
+
label: messages.bookingDocumentDialog.documentTypeLabels[t],
|
|
37
|
+
})), [messages.bookingDocumentDialog.documentTypeLabels]);
|
|
38
|
+
const travelerItems = useMemo(() => [
|
|
39
|
+
{
|
|
40
|
+
value: UNASSIGNED,
|
|
41
|
+
label: messages.bookingDocumentDialog.placeholders.travelerUnassigned,
|
|
42
|
+
},
|
|
43
|
+
...travelers.map((t) => ({
|
|
44
|
+
value: t.id,
|
|
45
|
+
label: `${t.firstName} ${t.lastName}`,
|
|
46
|
+
})),
|
|
47
|
+
], [travelers, messages.bookingDocumentDialog.placeholders.travelerUnassigned]);
|
|
34
48
|
const form = useForm({
|
|
35
49
|
resolver: zodResolver(documentFormSchema),
|
|
36
50
|
defaultValues: {
|
|
@@ -59,19 +73,7 @@ export function BookingDocumentDialog({ open, onOpenChange, bookingId, onSuccess
|
|
|
59
73
|
onOpenChange(false);
|
|
60
74
|
onSuccess?.();
|
|
61
75
|
};
|
|
62
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: messages.bookingDocumentDialog.title }) }), _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: messages.bookingDocumentDialog.fields.type }), _jsxs(Select, { items: documentTypes.map((t) => ({
|
|
63
|
-
label: messages.bookingDocumentDialog.documentTypeLabels[t],
|
|
64
|
-
value: t,
|
|
65
|
-
})), 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: messages.bookingDocumentDialog.documentTypeLabels[t] }, t))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDocumentDialog.fields.traveler }), _jsxs(Select, { items: [
|
|
66
|
-
{
|
|
67
|
-
label: messages.bookingDocumentDialog.placeholders.travelerUnassigned,
|
|
68
|
-
value: UNASSIGNED,
|
|
69
|
-
},
|
|
70
|
-
...travelers.map((traveler) => ({
|
|
71
|
-
label: `${traveler.firstName} ${traveler.lastName}`,
|
|
72
|
-
value: traveler.id,
|
|
73
|
-
})),
|
|
74
|
-
], 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: messages.bookingDocumentDialog.placeholders.travelerUnassigned }), 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: messages.bookingDocumentDialog.fields.file }), _jsx(FileDropzone, { accept: "application/pdf,image/*", maxSize: 10 * 1024 * 1024, onUploaded: (upload) => {
|
|
76
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: messages.bookingDocumentDialog.title }) }), _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: messages.bookingDocumentDialog.fields.type }), _jsxs(Select, { items: typeItems, 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: messages.bookingDocumentDialog.documentTypeLabels[t] }, t))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingDocumentDialog.fields.traveler }), _jsxs(Select, { items: travelerItems, 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: messages.bookingDocumentDialog.placeholders.travelerUnassigned }), 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: messages.bookingDocumentDialog.fields.file }), _jsx(FileDropzone, { accept: "application/pdf,image/*", maxSize: 10 * 1024 * 1024, onUploaded: (upload) => {
|
|
75
77
|
form.setValue("fileUrl", upload.url, { shouldValidate: true });
|
|
76
78
|
form.setValue("fileName", upload.name, { shouldValidate: true });
|
|
77
79
|
}, helperText: messages.bookingDocumentDialog.placeholders.helperText }), 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: messages.bookingDocumentDialog.fields.expiresAt }), _jsx(DatePicker, { value: form.watch("expiresAt") || null, onChange: (next) => form.setValue("expiresAt", next ?? "", {
|
|
@@ -1 +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;AAiElG,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,
|
|
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;AAiElG,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,2CA+N7B"}
|
|
@@ -7,7 +7,7 @@ import { CurrencyInput } from "@voyantjs/ui/components/currency-input";
|
|
|
7
7
|
import { DateTimePicker } from "@voyantjs/ui/components/date-time-picker";
|
|
8
8
|
import { zodResolver } from "@voyantjs/ui/lib/zod-resolver";
|
|
9
9
|
import { Loader2 } from "lucide-react";
|
|
10
|
-
import { useEffect } from "react";
|
|
10
|
+
import { useEffect, useMemo } from "react";
|
|
11
11
|
import { useForm } from "react-hook-form";
|
|
12
12
|
import { z } from "zod/v4";
|
|
13
13
|
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
@@ -47,6 +47,14 @@ export function BookingGuaranteeDialog({ open, onOpenChange, bookingId, guarante
|
|
|
47
47
|
const { create, update } = useBookingGuaranteeMutation(bookingId);
|
|
48
48
|
const messages = useBookingsUiMessagesOrDefault();
|
|
49
49
|
const guaranteeFormSchema = createGuaranteeFormSchema();
|
|
50
|
+
const typeItems = useMemo(() => guaranteeTypes.map((t) => ({
|
|
51
|
+
value: t,
|
|
52
|
+
label: messages.bookingGuaranteeDialog.guaranteeTypeLabels[t],
|
|
53
|
+
})), [messages.bookingGuaranteeDialog.guaranteeTypeLabels]);
|
|
54
|
+
const statusItems = useMemo(() => guaranteeStatuses.map((s) => ({
|
|
55
|
+
value: s,
|
|
56
|
+
label: messages.bookingGuaranteeDialog.guaranteeStatusLabels[s],
|
|
57
|
+
})), [messages.bookingGuaranteeDialog.guaranteeStatusLabels]);
|
|
50
58
|
const form = useForm({
|
|
51
59
|
resolver: zodResolver(guaranteeFormSchema),
|
|
52
60
|
defaultValues: {
|
|
@@ -100,13 +108,7 @@ export function BookingGuaranteeDialog({ open, onOpenChange, bookingId, guarante
|
|
|
100
108
|
const isSubmitting = create.isPending || update.isPending;
|
|
101
109
|
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing
|
|
102
110
|
? messages.bookingGuaranteeDialog.titles.edit
|
|
103
|
-
: messages.bookingGuaranteeDialog.titles.create }) }), _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: messages.bookingGuaranteeDialog.fields.type }), _jsxs(Select, { items: guaranteeTypes.map((t) => ({
|
|
104
|
-
label: messages.bookingGuaranteeDialog.guaranteeTypeLabels[t],
|
|
105
|
-
value: t,
|
|
106
|
-
})), 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: messages.bookingGuaranteeDialog.guaranteeTypeLabels[t] }, t))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingGuaranteeDialog.fields.status }), _jsxs(Select, { items: guaranteeStatuses.map((s) => ({
|
|
107
|
-
label: messages.bookingGuaranteeDialog.guaranteeStatusLabels[s],
|
|
108
|
-
value: s,
|
|
109
|
-
})), 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: messages.bookingGuaranteeDialog.guaranteeStatusLabels[s] }, s))) })] })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingGuaranteeDialog.fields.currency }), _jsx(CurrencyCombobox, { value: form.watch("currency") || null, onChange: (next) => form.setValue("currency", next ?? DEFAULT_CURRENCY, {
|
|
111
|
+
: messages.bookingGuaranteeDialog.titles.create }) }), _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: messages.bookingGuaranteeDialog.fields.type }), _jsxs(Select, { items: typeItems, 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: messages.bookingGuaranteeDialog.guaranteeTypeLabels[t] }, t))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingGuaranteeDialog.fields.status }), _jsxs(Select, { items: statusItems, 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: messages.bookingGuaranteeDialog.guaranteeStatusLabels[s] }, s))) })] })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingGuaranteeDialog.fields.currency }), _jsx(CurrencyCombobox, { value: form.watch("currency") || null, onChange: (next) => form.setValue("currency", next ?? DEFAULT_CURRENCY, {
|
|
110
112
|
shouldValidate: true,
|
|
111
113
|
shouldDirty: true,
|
|
112
114
|
}) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingGuaranteeDialog.fields.amountCents }), _jsx(CurrencyInput, { value: form.watch("amountCents"), onChange: (next) => form.setValue("amountCents", next, {
|
|
@@ -1 +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;AAiEzF,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,
|
|
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;AAiEzF,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,2CA4SxB"}
|
|
@@ -7,7 +7,7 @@ import { CurrencyInput } from "@voyantjs/ui/components/currency-input";
|
|
|
7
7
|
import { DatePicker } from "@voyantjs/ui/components/date-picker";
|
|
8
8
|
import { zodResolver } from "@voyantjs/ui/lib/zod-resolver";
|
|
9
9
|
import { Loader2 } from "lucide-react";
|
|
10
|
-
import { useEffect } from "react";
|
|
10
|
+
import { useEffect, useMemo } from "react";
|
|
11
11
|
import { useForm } from "react-hook-form";
|
|
12
12
|
import { z } from "zod/v4";
|
|
13
13
|
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
@@ -47,6 +47,14 @@ export function BookingItemDialog({ open, onOpenChange, bookingId, item, onSucce
|
|
|
47
47
|
const { create, update } = useBookingItemMutation(bookingId);
|
|
48
48
|
const messages = useBookingsUiMessagesOrDefault();
|
|
49
49
|
const bookingItemFormSchema = createBookingItemFormSchema(messages);
|
|
50
|
+
const typeItems = useMemo(() => itemTypes.map((t) => ({
|
|
51
|
+
value: t,
|
|
52
|
+
label: messages.bookingItemDialog.itemTypeLabels[t],
|
|
53
|
+
})), [messages.bookingItemDialog.itemTypeLabels]);
|
|
54
|
+
const statusItems = useMemo(() => itemStatuses.map((s) => ({
|
|
55
|
+
value: s,
|
|
56
|
+
label: messages.bookingItemDialog.itemStatusLabels[s],
|
|
57
|
+
})), [messages.bookingItemDialog.itemStatusLabels]);
|
|
50
58
|
const form = useForm({
|
|
51
59
|
resolver: zodResolver(bookingItemFormSchema),
|
|
52
60
|
defaultValues: {
|
|
@@ -65,6 +73,13 @@ export function BookingItemDialog({ open, onOpenChange, bookingId, item, onSucce
|
|
|
65
73
|
notes: "",
|
|
66
74
|
},
|
|
67
75
|
});
|
|
76
|
+
// `form` is intentionally omitted from deps — react-hook-form returns
|
|
77
|
+
// a fresh wrapper object on every render even though the underlying
|
|
78
|
+
// state lives in a ref. Including `form` here would re-run the effect
|
|
79
|
+
// on every render and re-trigger reset → re-render → loop. The methods
|
|
80
|
+
// we call (`reset`) are safe to call from a stale closure since they
|
|
81
|
+
// dispatch into the form's internal store.
|
|
82
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: see comment above
|
|
68
83
|
useEffect(() => {
|
|
69
84
|
if (open && item) {
|
|
70
85
|
form.reset({
|
|
@@ -86,7 +101,7 @@ export function BookingItemDialog({ open, onOpenChange, bookingId, item, onSucce
|
|
|
86
101
|
else if (open) {
|
|
87
102
|
form.reset();
|
|
88
103
|
}
|
|
89
|
-
}, [
|
|
104
|
+
}, [open, item]);
|
|
90
105
|
const onSubmit = async (values) => {
|
|
91
106
|
const payload = {
|
|
92
107
|
title: values.title,
|
|
@@ -115,13 +130,7 @@ export function BookingItemDialog({ open, onOpenChange, bookingId, item, onSucce
|
|
|
115
130
|
const isSubmitting = create.isPending || update.isPending;
|
|
116
131
|
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing
|
|
117
132
|
? messages.bookingItemDialog.titles.edit
|
|
118
|
-
: messages.bookingItemDialog.titles.create }) }), _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: messages.bookingItemDialog.fields.title }), _jsx(Input, { ...form.register("title"), placeholder: messages.bookingItemDialog.placeholders.title }), 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: messages.bookingItemDialog.fields.type }), _jsxs(Select, { items: itemTypes.map((t) => ({
|
|
119
|
-
label: messages.bookingItemDialog.itemTypeLabels[t],
|
|
120
|
-
value: t,
|
|
121
|
-
})), 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: messages.bookingItemDialog.itemTypeLabels[t] }, t))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingItemDialog.fields.status }), _jsxs(Select, { items: itemStatuses.map((s) => ({
|
|
122
|
-
label: messages.bookingItemDialog.itemStatusLabels[s],
|
|
123
|
-
value: s,
|
|
124
|
-
})), 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: messages.bookingItemDialog.itemStatusLabels[s] }, s))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingItemDialog.fields.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: messages.bookingItemDialog.fields.sellCurrency }), _jsx(CurrencyCombobox, { value: form.watch("sellCurrency") || null, onChange: (next) => form.setValue("sellCurrency", next ?? DEFAULT_CURRENCY, {
|
|
133
|
+
: messages.bookingItemDialog.titles.create }) }), _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: messages.bookingItemDialog.fields.title }), _jsx(Input, { ...form.register("title"), placeholder: messages.bookingItemDialog.placeholders.title }), 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: messages.bookingItemDialog.fields.type }), _jsxs(Select, { items: typeItems, 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: messages.bookingItemDialog.itemTypeLabels[t] }, t))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingItemDialog.fields.status }), _jsxs(Select, { items: statusItems, 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: messages.bookingItemDialog.itemStatusLabels[s] }, s))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingItemDialog.fields.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: messages.bookingItemDialog.fields.sellCurrency }), _jsx(CurrencyCombobox, { value: form.watch("sellCurrency") || null, onChange: (next) => form.setValue("sellCurrency", next ?? DEFAULT_CURRENCY, {
|
|
125
134
|
shouldValidate: true,
|
|
126
135
|
shouldDirty: true,
|
|
127
136
|
}) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.bookingItemDialog.fields.unitSellAmountCents }), _jsx(CurrencyInput, { value: form.watch("unitSellAmountCents"), onChange: (next) => form.setValue("unitSellAmountCents", next, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-item-travelers.d.ts","sourceRoot":"","sources":["../../src/components/booking-item-travelers.tsx"],"names":[],"mappings":"AA+BA,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,
|
|
1
|
+
{"version":3,"file":"booking-item-travelers.d.ts","sourceRoot":"","sources":["../../src/components/booking-item-travelers.tsx"],"names":[],"mappings":"AA+BA,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,2CAwJpF"}
|
|
@@ -24,6 +24,14 @@ export function BookingItemTravelers({ bookingId, itemId }) {
|
|
|
24
24
|
const travelers = travelersData?.data ?? [];
|
|
25
25
|
const assignedIds = new Set(assignedTravelers.map((link) => link.travelerId));
|
|
26
26
|
const availableTravelers = travelers.filter((traveler) => !assignedIds.has(traveler.id));
|
|
27
|
+
const travelerItems = React.useMemo(() => availableTravelers.map((t) => ({
|
|
28
|
+
value: t.id,
|
|
29
|
+
label: `${t.firstName} ${t.lastName}`,
|
|
30
|
+
})), [availableTravelers]);
|
|
31
|
+
const roleItems = React.useMemo(() => roles.map((r) => ({
|
|
32
|
+
value: r,
|
|
33
|
+
label: messages.bookingItemTravelers.roleLabels[r],
|
|
34
|
+
})), [messages.bookingItemTravelers.roleLabels]);
|
|
27
35
|
const travelerMap = new Map();
|
|
28
36
|
for (const traveler of travelers) {
|
|
29
37
|
travelerMap.set(traveler.id, traveler);
|
|
@@ -45,11 +53,5 @@ export function BookingItemTravelers({ bookingId, itemId }) {
|
|
|
45
53
|
remove.mutate(link.id);
|
|
46
54
|
}
|
|
47
55
|
}, className: "text-muted-foreground hover:text-destructive", children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) })] }, link.id));
|
|
48
|
-
}) })), 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) => ({
|
|
49
|
-
label: `${traveler.firstName} ${traveler.lastName}`,
|
|
50
|
-
value: traveler.id,
|
|
51
|
-
})), value: selectedTravelerId, onValueChange: (v) => setSelectedTravelerId(v ?? ""), children: [_jsx(SelectTrigger, { className: "w-full h-8 text-xs", children: _jsx(SelectValue, { placeholder: messages.bookingItemTravelers.selectTravelerPlaceholder }) }), _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) => ({
|
|
52
|
-
label: messages.bookingItemTravelers.roleLabels[r],
|
|
53
|
-
value: r,
|
|
54
|
-
})), 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: messages.bookingItemTravelers.roleLabels[r] }, 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" }), messages.bookingItemTravelers.actions.assign] })] }))] }));
|
|
56
|
+
}) })), 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: travelerItems, value: selectedTravelerId, onValueChange: (v) => setSelectedTravelerId(v ?? ""), children: [_jsx(SelectTrigger, { className: "w-full h-8 text-xs", children: _jsx(SelectValue, { placeholder: messages.bookingItemTravelers.selectTravelerPlaceholder }) }), _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: roleItems, 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: messages.bookingItemTravelers.roleLabels[r] }, 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" }), messages.bookingItemTravelers.actions.assign] })] }))] }));
|
|
55
57
|
}
|
|
@@ -1 +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;AAgDhC,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,
|
|
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;AAgDhC,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,2CAgNnC"}
|
|
@@ -7,7 +7,7 @@ import { CurrencyInput } from "@voyantjs/ui/components/currency-input";
|
|
|
7
7
|
import { DatePicker } from "@voyantjs/ui/components/date-picker";
|
|
8
8
|
import { zodResolver } from "@voyantjs/ui/lib/zod-resolver";
|
|
9
9
|
import { Loader2 } from "lucide-react";
|
|
10
|
-
import { useEffect } from "react";
|
|
10
|
+
import { useEffect, useMemo } from "react";
|
|
11
11
|
import { useForm } from "react-hook-form";
|
|
12
12
|
import { z } from "zod/v4";
|
|
13
13
|
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
@@ -32,6 +32,14 @@ export function BookingPaymentScheduleDialog({ open, onOpenChange, bookingId, sc
|
|
|
32
32
|
const { create, update } = useBookingPaymentScheduleMutation(bookingId);
|
|
33
33
|
const messages = useBookingsUiMessagesOrDefault();
|
|
34
34
|
const scheduleFormSchema = createScheduleFormSchema(messages);
|
|
35
|
+
const typeItems = useMemo(() => scheduleTypes.map((t) => ({
|
|
36
|
+
value: t,
|
|
37
|
+
label: messages.paymentScheduleDialog.scheduleTypeLabels[t],
|
|
38
|
+
})), [messages.paymentScheduleDialog.scheduleTypeLabels]);
|
|
39
|
+
const statusItems = useMemo(() => scheduleStatuses.map((s) => ({
|
|
40
|
+
value: s,
|
|
41
|
+
label: messages.paymentScheduleDialog.scheduleStatusLabels[s],
|
|
42
|
+
})), [messages.paymentScheduleDialog.scheduleStatusLabels]);
|
|
35
43
|
const form = useForm({
|
|
36
44
|
resolver: zodResolver(scheduleFormSchema),
|
|
37
45
|
defaultValues: {
|
|
@@ -79,13 +87,7 @@ export function BookingPaymentScheduleDialog({ open, onOpenChange, bookingId, sc
|
|
|
79
87
|
const isSubmitting = create.isPending || update.isPending;
|
|
80
88
|
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { size: "lg", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing
|
|
81
89
|
? messages.paymentScheduleDialog.titles.edit
|
|
82
|
-
: messages.paymentScheduleDialog.titles.create }) }), _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: messages.paymentScheduleDialog.fields.type }), _jsxs(Select, { items: scheduleTypes.map((t) => ({
|
|
83
|
-
label: messages.paymentScheduleDialog.scheduleTypeLabels[t],
|
|
84
|
-
value: t,
|
|
85
|
-
})), 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: messages.paymentScheduleDialog.scheduleTypeLabels[t] }, t))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.paymentScheduleDialog.fields.status }), _jsxs(Select, { items: scheduleStatuses.map((s) => ({
|
|
86
|
-
label: messages.paymentScheduleDialog.scheduleStatusLabels[s],
|
|
87
|
-
value: s,
|
|
88
|
-
})), 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: messages.paymentScheduleDialog.scheduleStatusLabels[s] }, s))) })] })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.paymentScheduleDialog.fields.dueDate }), _jsx(DatePicker, { value: form.watch("dueDate") || null, onChange: (next) => form.setValue("dueDate", next ?? "", {
|
|
90
|
+
: messages.paymentScheduleDialog.titles.create }) }), _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: messages.paymentScheduleDialog.fields.type }), _jsxs(Select, { items: typeItems, 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: messages.paymentScheduleDialog.scheduleTypeLabels[t] }, t))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.paymentScheduleDialog.fields.status }), _jsxs(Select, { items: statusItems, 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: messages.paymentScheduleDialog.scheduleStatusLabels[s] }, s))) })] })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: messages.paymentScheduleDialog.fields.dueDate }), _jsx(DatePicker, { value: form.watch("dueDate") || null, onChange: (next) => form.setValue("dueDate", next ?? "", {
|
|
89
91
|
shouldValidate: true,
|
|
90
92
|
shouldDirty: true,
|
|
91
93
|
}), placeholder: messages.paymentScheduleDialog.placeholders.dueDate, 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: messages.paymentScheduleDialog.fields.currency }), _jsx(CurrencyCombobox, { value: form.watch("currency") || null, onChange: (next) => form.setValue("currency", next ?? DEFAULT_CURRENCY, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"booking-payment-schedule-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-payment-schedule-list.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"booking-payment-schedule-list.d.ts","sourceRoot":"","sources":["../../src/components/booking-payment-schedule-list.tsx"],"names":[],"mappings":"AA+BA,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,0BAA0B,CAAC,EAAE,SAAS,EAAE,EAAE,+BAA+B,2CA4LxF"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { useBooking } from "@voyantjs/bookings-react";
|
|
4
|
+
import { useBookingPaymentScheduleMutation, useBookingPaymentSchedules, useInvoiceMutation, } from "@voyantjs/finance-react";
|
|
4
5
|
import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components";
|
|
5
|
-
import {
|
|
6
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@voyantjs/ui/components/dropdown-menu";
|
|
7
|
+
import { CalendarClock, FileText, Loader2, Pencil, Plus, Receipt, Trash2 } from "lucide-react";
|
|
6
8
|
import * as React from "react";
|
|
7
9
|
import { useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
8
10
|
import { BookingPaymentScheduleDialog } from "./booking-payment-schedule-dialog.js";
|
|
@@ -17,16 +19,43 @@ const statusVariant = {
|
|
|
17
19
|
export function BookingPaymentScheduleList({ bookingId }) {
|
|
18
20
|
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
19
21
|
const [editing, setEditing] = React.useState(undefined);
|
|
22
|
+
const [generatingInvoiceForId, setGeneratingInvoiceForId] = React.useState(null);
|
|
20
23
|
const { data } = useBookingPaymentSchedules(bookingId);
|
|
21
24
|
const { remove } = useBookingPaymentScheduleMutation(bookingId);
|
|
25
|
+
const { data: bookingData } = useBooking(bookingId);
|
|
26
|
+
const booking = bookingData?.data ?? null;
|
|
27
|
+
const { createFromBooking: createInvoiceFromBooking, render: renderInvoice } = useInvoiceMutation();
|
|
22
28
|
const { formatCurrency } = useBookingsUiI18nOrDefault();
|
|
23
29
|
const messages = useBookingsUiMessagesOrDefault();
|
|
24
30
|
const schedules = data?.data ?? [];
|
|
31
|
+
const handleGenerateInvoice = async (schedule, invoiceType) => {
|
|
32
|
+
if (!booking)
|
|
33
|
+
return;
|
|
34
|
+
setGeneratingInvoiceForId(schedule.id);
|
|
35
|
+
try {
|
|
36
|
+
const todayIso = new Date().toISOString().slice(0, 10);
|
|
37
|
+
const dueIso = schedule.dueDate || todayIso;
|
|
38
|
+
// i18n-literal-ok: invoice number prefix, not user-facing copy
|
|
39
|
+
const prefix = invoiceType === "proforma" ? "PRO" : "INV";
|
|
40
|
+
const invoice = await createInvoiceFromBooking.mutateAsync({
|
|
41
|
+
bookingId: booking.id,
|
|
42
|
+
invoiceNumber: `${prefix}-${booking.bookingNumber}-${schedule.scheduleType.toUpperCase().slice(0, 3)}`,
|
|
43
|
+
issueDate: todayIso,
|
|
44
|
+
dueDate: dueIso,
|
|
45
|
+
notes: schedule.notes ?? null,
|
|
46
|
+
invoiceType,
|
|
47
|
+
});
|
|
48
|
+
await renderInvoice.mutateAsync({ id: invoice.id, input: { format: "pdf" } });
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
setGeneratingInvoiceForId(null);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
25
54
|
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" }), messages.bookingPaymentScheduleList.title] }), _jsxs(Button, { size: "sm", onClick: () => {
|
|
26
55
|
setEditing(undefined);
|
|
27
56
|
setDialogOpen(true);
|
|
28
57
|
}, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), messages.bookingPaymentScheduleList.addSchedule] })] }), _jsx(CardContent, { children: schedules.length === 0 ? (_jsx("p", { className: "py-4 text-center text-sm text-muted-foreground", children: messages.bookingPaymentScheduleList.empty })) : (_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: messages.bookingPaymentScheduleList.columns.type }), _jsx("th", { className: "p-2 text-left font-medium", children: messages.bookingPaymentScheduleList.columns.status }), _jsx("th", { className: "p-2 text-left font-medium", children: messages.bookingPaymentScheduleList.columns.dueDate }), _jsx("th", { className: "p-2 text-right font-medium", children: messages.bookingPaymentScheduleList.columns.amount }), _jsx("th", { className: "p-2 text-left font-medium", children: messages.bookingPaymentScheduleList.columns.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", children: messages.paymentScheduleDialog.scheduleTypeLabels[schedule.scheduleType] }), _jsx("td", { className: "p-2", children: _jsx(Badge, { variant: statusVariant[schedule.status] ?? "secondary", children: messages.paymentScheduleDialog.scheduleStatusLabels[schedule.status] }) }), _jsx("td", { className: "p-2", children: schedule.dueDate }), _jsx("td", { className: "p-2 text-right font-mono", children: formatCurrency(schedule.amountCents / 100, schedule.currency) }), _jsx("td", { className: "max-w-[200px] truncate p-2 text-muted-foreground", children: schedule.notes ??
|
|
29
|
-
messages.bookingPaymentScheduleList.values.notesUnavailable }), _jsx("td", { className: "p-2", children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => {
|
|
58
|
+
messages.bookingPaymentScheduleList.values.notesUnavailable }), _jsx("td", { className: "p-2", children: _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { disabled: !booking || generatingInvoiceForId === schedule.id, title: messages.bookingPaymentScheduleList.actions.issueDocument, render: _jsx("button", { type: "button", className: "inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50", children: generatingInvoiceForId === schedule.id ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsx(Receipt, { className: "h-3.5 w-3.5" })) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => void handleGenerateInvoice(schedule, "invoice"), children: [_jsx(FileText, { className: "h-4 w-4" }), messages.bookingPaymentScheduleList.actions.issueInvoice] }), _jsxs(DropdownMenuItem, { onClick: () => void handleGenerateInvoice(schedule, "proforma"), children: [_jsx(FileText, { className: "h-4 w-4" }), messages.bookingPaymentScheduleList.actions.issueProforma] })] })] }), _jsx("button", { type: "button", onClick: () => {
|
|
30
59
|
setEditing(schedule);
|
|
31
60
|
setDialogOpen(true);
|
|
32
61
|
}, className: "text-muted-foreground hover:text-foreground", children: _jsx(Pencil, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => {
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
/** Quantity per option_unit id; omitted ids are treated as 0. */
|
|
2
|
-
export interface
|
|
2
|
+
export interface OptionUnitsStepperValue {
|
|
3
3
|
quantities: Record<string, number>;
|
|
4
4
|
}
|
|
5
|
-
export declare const
|
|
6
|
-
export interface
|
|
5
|
+
export declare const emptyOptionUnitsStepperValue: OptionUnitsStepperValue;
|
|
6
|
+
export interface OptionUnitsStepperUnit {
|
|
7
7
|
optionId: string | null;
|
|
8
8
|
optionUnitId: string;
|
|
9
9
|
unitName: string;
|
|
10
|
+
/** Stable code from the products schema (`ADULT`, `CHILD`, `SENIOR`, …) when present. */
|
|
11
|
+
unitCode?: string | null;
|
|
12
|
+
/** Inclusive lower age bound for this unit, when configured. */
|
|
13
|
+
minAge?: number | null;
|
|
14
|
+
/** Inclusive upper age bound for this unit, when configured. */
|
|
15
|
+
maxAge?: number | null;
|
|
16
|
+
/** Unit category from option_units.unitType — person/group/room/vehicle/service/other. */
|
|
17
|
+
unitType?: "person" | "group" | "room" | "vehicle" | "service" | "other" | null;
|
|
10
18
|
occupancyMax: number | null;
|
|
11
19
|
initial: number | null;
|
|
12
20
|
reserved: number;
|
|
13
21
|
remaining: number | null;
|
|
14
22
|
}
|
|
15
|
-
export interface
|
|
16
|
-
value:
|
|
17
|
-
onChange: (value:
|
|
23
|
+
export interface OptionUnitsStepperSectionProps {
|
|
24
|
+
value: OptionUnitsStepperValue;
|
|
25
|
+
onChange: (value: OptionUnitsStepperValue) => void;
|
|
18
26
|
/** Product whose options become selectable room quantity rows. */
|
|
19
27
|
productId?: string;
|
|
20
28
|
/**
|
|
@@ -28,7 +36,7 @@ export interface RoomsStepperSectionProps {
|
|
|
28
36
|
*/
|
|
29
37
|
optionId?: string | null;
|
|
30
38
|
enabled?: boolean;
|
|
31
|
-
onUnitsChange?: (units:
|
|
39
|
+
onUnitsChange?: (units: OptionUnitsStepperUnit[]) => void;
|
|
32
40
|
labels?: {
|
|
33
41
|
heading?: string;
|
|
34
42
|
noOption?: string;
|
|
@@ -58,5 +66,5 @@ export interface RoomsStepperSectionProps {
|
|
|
58
66
|
* disables the "+" button — we don't let the UI submit a request that
|
|
59
67
|
* would 409 at insert time.
|
|
60
68
|
*/
|
|
61
|
-
export declare function
|
|
62
|
-
//# sourceMappingURL=
|
|
69
|
+
export declare function OptionUnitsStepperSection({ value, onChange, productId, slotId, optionId, enabled, onUnitsChange, labels, }: OptionUnitsStepperSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
70
|
+
//# sourceMappingURL=option-units-stepper-section.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"option-units-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/option-units-stepper-section.tsx"],"names":[],"mappings":"AAgBA,iEAAiE;AACjE,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAED,eAAO,MAAM,4BAA4B,EAAE,uBAA4C,CAAA;AAEvF,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,IAAI,CAAA;IAC/E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,uBAAuB,CAAA;IAC9B,QAAQ,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAA;IAClD,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,EAAE,KAAK,IAAI,CAAA;IACzD,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,CAAA;CACF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,KAAK,EACL,QAAQ,EACR,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAc,EACd,aAAa,EACb,MAAM,GACP,EAAE,8BAA8B,2CAqLhC"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useQueries } from "@tanstack/react-query";
|
|
4
|
+
import { useSlotUnitAvailability } from "@voyantjs/availability-react";
|
|
5
|
+
import { getOptionUnitsQueryOptions, useProductOptions, useVoyantProductsContext, } from "@voyantjs/products-react";
|
|
6
|
+
import { Button, Label } from "@voyantjs/ui/components";
|
|
7
|
+
import { Minus, Plus } from "lucide-react";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
|
|
10
|
+
export const emptyOptionUnitsStepperValue = { quantities: {} };
|
|
11
|
+
/**
|
|
12
|
+
* Rooms / per-unit stepper for booking-create flows. Drives
|
|
13
|
+
* `GET /v1/availability/slots/:id/unit-availability` from #235 when a
|
|
14
|
+
* departure is selected, and product option-level units before departure
|
|
15
|
+
* selection, so operators can build "2 double rooms and 1 single" drafts.
|
|
16
|
+
*
|
|
17
|
+
* The section only tracks **intent** (how many of each unit the operator
|
|
18
|
+
* wants to book). Actual hold/reservation happens when the parent submits
|
|
19
|
+
* the booking — capacity drops the moment the reservation transaction
|
|
20
|
+
* commits; the next refetch of `useSlotUnitAvailability` reflects it.
|
|
21
|
+
*
|
|
22
|
+
* ### Stepper bounds
|
|
23
|
+
*
|
|
24
|
+
* - Minimum is 0 (operator can deselect).
|
|
25
|
+
* - Maximum is the unit's `remaining` count from the server. Unlimited
|
|
26
|
+
* pools (`remaining === null`) have no upper bound.
|
|
27
|
+
* - The server is the truth: entering `3 doubles` when only 2 remain just
|
|
28
|
+
* disables the "+" button — we don't let the UI submit a request that
|
|
29
|
+
* would 409 at insert time.
|
|
30
|
+
*/
|
|
31
|
+
export function OptionUnitsStepperSection({ value, onChange, productId, slotId, optionId, enabled = true, onUnitsChange, labels, }) {
|
|
32
|
+
const productsClient = useVoyantProductsContext();
|
|
33
|
+
const messages = useBookingsUiMessagesOrDefault();
|
|
34
|
+
const merged = { ...messages.roomsStepperSection.labels, ...labels };
|
|
35
|
+
const availability = useSlotUnitAvailability({ slotId, enabled: enabled && Boolean(slotId) });
|
|
36
|
+
// Always fetch option-level units for the product. They're needed
|
|
37
|
+
// both before a slot is picked AND as a fallback after picking a slot
|
|
38
|
+
// whose `availability_slots` row has no per-unit allocation rows wired
|
|
39
|
+
// (the default for product-level slots seeded by the operator).
|
|
40
|
+
const optionsQuery = useProductOptions({
|
|
41
|
+
productId,
|
|
42
|
+
status: "active",
|
|
43
|
+
limit: 100,
|
|
44
|
+
enabled: enabled && Boolean(productId),
|
|
45
|
+
});
|
|
46
|
+
const productOptions = React.useMemo(() => {
|
|
47
|
+
const options = optionsQuery.data?.data ?? [];
|
|
48
|
+
if (!optionId)
|
|
49
|
+
return options;
|
|
50
|
+
const selected = options.find((option) => option.id === optionId);
|
|
51
|
+
const rest = options.filter((option) => option.id !== optionId);
|
|
52
|
+
return selected ? [selected, ...rest] : options;
|
|
53
|
+
}, [optionsQuery.data?.data, optionId]);
|
|
54
|
+
const optionUnitQueries = useQueries({
|
|
55
|
+
queries: productOptions.map((option) => ({
|
|
56
|
+
...getOptionUnitsQueryOptions(productsClient, {
|
|
57
|
+
optionId: option.id,
|
|
58
|
+
limit: 100,
|
|
59
|
+
}),
|
|
60
|
+
enabled: enabled && Boolean(productId),
|
|
61
|
+
})),
|
|
62
|
+
});
|
|
63
|
+
const optionUnitRows = React.useMemo(() => {
|
|
64
|
+
const rows = [];
|
|
65
|
+
productOptions.forEach((option, index) => {
|
|
66
|
+
const units = optionUnitQueries[index]?.data?.data ?? [];
|
|
67
|
+
rows.push(...units.map((unit) => optionUnitToStepperUnit(option, unit, units.length)));
|
|
68
|
+
});
|
|
69
|
+
return rows;
|
|
70
|
+
}, [productOptions, optionUnitQueries]);
|
|
71
|
+
const availabilityUnitRows = React.useMemo(() => (availability.data?.data ?? []).map((unit) => ({
|
|
72
|
+
...unit,
|
|
73
|
+
optionId: optionId ?? null,
|
|
74
|
+
})), [availability.data?.data, optionId]);
|
|
75
|
+
// Slot-specific per-unit availability wins when it actually returns
|
|
76
|
+
// rows; otherwise fall back to the product's option-level units so
|
|
77
|
+
// the operator can still pick quantities. Product-level slots (no
|
|
78
|
+
// option_id) report no per-unit availability — but the product's
|
|
79
|
+
// options still describe the bookable units.
|
|
80
|
+
const units = slotId && availabilityUnitRows.length > 0 ? availabilityUnitRows : optionUnitRows;
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
onUnitsChange?.(units);
|
|
83
|
+
}, [onUnitsChange, units]);
|
|
84
|
+
// Group the unit rows by option — operators choose how many of each
|
|
85
|
+
// *option* to book, not how many of each age-banded unit. The age
|
|
86
|
+
// categorization (Adult / Child / Senior / Infant) is derived from
|
|
87
|
+
// each traveler's date of birth and resolved on submit, so we route
|
|
88
|
+
// the per-option quantity through the option's "primary" unit
|
|
89
|
+
// (preferring `ADULT` by code, otherwise the first unit) here.
|
|
90
|
+
const optionRows = React.useMemo(() => {
|
|
91
|
+
const groups = new Map();
|
|
92
|
+
for (const unit of units) {
|
|
93
|
+
const key = unit.optionId ?? unit.optionUnitId;
|
|
94
|
+
const entry = groups.get(key);
|
|
95
|
+
if (entry) {
|
|
96
|
+
entry.allUnits.push(unit);
|
|
97
|
+
// Prefer an explicit ADULT unit as primary; fall back to whatever
|
|
98
|
+
// arrived first.
|
|
99
|
+
if (isAdultUnit(unit) && !isAdultUnit(entry.primary))
|
|
100
|
+
entry.primary = unit;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
groups.set(key, { primary: unit, allUnits: [unit] });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return Array.from(groups.entries()).map(([optionKey, group]) => {
|
|
107
|
+
const optionName = productOptions.find((option) => option.id === optionKey)?.name ?? group.primary.unitName;
|
|
108
|
+
const totalRemaining = group.allUnits.reduce((acc, unit) => {
|
|
109
|
+
if (unit.remaining === null)
|
|
110
|
+
return null;
|
|
111
|
+
if (acc === null)
|
|
112
|
+
return null;
|
|
113
|
+
return acc + unit.remaining;
|
|
114
|
+
}, 0);
|
|
115
|
+
return {
|
|
116
|
+
optionKey,
|
|
117
|
+
optionName,
|
|
118
|
+
primary: group.primary,
|
|
119
|
+
totalRemaining,
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
}, [units, productOptions]);
|
|
123
|
+
if (!slotId && !productId && !optionId) {
|
|
124
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("p", { className: "text-xs text-muted-foreground", children: merged.noOption })] }));
|
|
125
|
+
}
|
|
126
|
+
// Both data sources need to resolve before declaring an empty result
|
|
127
|
+
// — slot units may legitimately be empty (product-level slot), and
|
|
128
|
+
// we don't want to flash the empty state before option-level units
|
|
129
|
+
// finish loading.
|
|
130
|
+
const optionsLoaded = optionsQuery.isSuccess && optionUnitQueries.every((query) => query.isSuccess);
|
|
131
|
+
const loaded = slotId ? availability.isSuccess && optionsLoaded : optionsLoaded;
|
|
132
|
+
if (loaded && units.length === 0) {
|
|
133
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("p", { className: "text-xs text-muted-foreground", children: merged.noUnits })] }));
|
|
134
|
+
}
|
|
135
|
+
const setQuantity = (unitId, qty) => {
|
|
136
|
+
const next = { ...value.quantities };
|
|
137
|
+
if (qty <= 0) {
|
|
138
|
+
delete next[unitId];
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
next[unitId] = qty;
|
|
142
|
+
}
|
|
143
|
+
onChange({ quantities: next });
|
|
144
|
+
};
|
|
145
|
+
return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("div", { className: "flex flex-col gap-2", children: optionRows.map(({ optionKey, optionName, primary, totalRemaining }) => {
|
|
146
|
+
const qty = value.quantities[primary.optionUnitId] ?? 0;
|
|
147
|
+
const remainingLabel = totalRemaining === null ? merged.unlimited : `${totalRemaining} ${merged.remaining}`;
|
|
148
|
+
const atMax = totalRemaining !== null && qty >= totalRemaining;
|
|
149
|
+
return (_jsxs("div", { className: "flex items-center gap-3 rounded-md border px-3 py-2", children: [_jsxs("div", { className: "flex-1", children: [_jsx("div", { className: "text-sm font-medium", children: optionName }), _jsx("div", { className: "text-xs text-muted-foreground", children: remainingLabel })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => setQuantity(primary.optionUnitId, Math.max(0, qty - 1)), disabled: qty <= 0, "aria-label": `${merged.decreaseUnitPrefix} ${optionName}`, children: _jsx(Minus, { className: "h-3.5 w-3.5" }) }), _jsx("span", { className: "min-w-[1.5rem] text-center text-sm tabular-nums", children: qty }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => setQuantity(primary.optionUnitId, qty + 1), disabled: atMax, "aria-label": `${merged.increaseUnitPrefix} ${optionName}`, children: _jsx(Plus, { className: "h-3.5 w-3.5" }) })] })] }, optionKey));
|
|
150
|
+
}) })] }));
|
|
151
|
+
}
|
|
152
|
+
function isAdultUnit(unit) {
|
|
153
|
+
// The seed creates ADULT / CHILD / SENIOR unit codes; the stepper
|
|
154
|
+
// unit object doesn't carry the code, so fall back to name-matching
|
|
155
|
+
// when the upstream code isn't surfaced.
|
|
156
|
+
return /\badult\b/i.test(unit.unitName);
|
|
157
|
+
}
|
|
158
|
+
function optionUnitToStepperUnit(option, unit, unitCount) {
|
|
159
|
+
return {
|
|
160
|
+
optionId: option.id,
|
|
161
|
+
optionUnitId: unit.id,
|
|
162
|
+
unitName: unitCount === 1 ? option.name : `${option.name} - ${unit.name}`,
|
|
163
|
+
unitCode: unit.code,
|
|
164
|
+
minAge: unit.minAge,
|
|
165
|
+
maxAge: unit.maxAge,
|
|
166
|
+
unitType: unit.unitType,
|
|
167
|
+
occupancyMax: unit.occupancyMax,
|
|
168
|
+
initial: null,
|
|
169
|
+
reserved: 0,
|
|
170
|
+
remaining: unit.maxQuantity ?? null,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type PaymentScheduleMode = "
|
|
1
|
+
export type PaymentScheduleMode = "full" | "split";
|
|
2
2
|
export interface PaymentScheduleValue {
|
|
3
3
|
mode: PaymentScheduleMode;
|
|
4
4
|
/** Used when mode === "full" — single due date for the whole amount. */
|