@voyantjs/bookings-ui 0.52.1 → 0.52.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/components/booking-billing-dialog.d.ts +16 -0
  2. package/dist/components/booking-billing-dialog.d.ts.map +1 -0
  3. package/dist/components/booking-billing-dialog.js +90 -0
  4. package/dist/components/booking-create-dialog.d.ts.map +1 -1
  5. package/dist/components/booking-create-dialog.js +512 -151
  6. package/dist/components/booking-create-page.js +1 -1
  7. package/dist/components/booking-document-dialog.d.ts.map +1 -1
  8. package/dist/components/booking-document-dialog.js +16 -14
  9. package/dist/components/booking-guarantee-dialog.d.ts.map +1 -1
  10. package/dist/components/booking-guarantee-dialog.js +10 -8
  11. package/dist/components/booking-item-dialog.d.ts.map +1 -1
  12. package/dist/components/booking-item-dialog.js +18 -9
  13. package/dist/components/booking-item-travelers.d.ts.map +1 -1
  14. package/dist/components/booking-item-travelers.js +9 -7
  15. package/dist/components/booking-payment-schedule-dialog.d.ts.map +1 -1
  16. package/dist/components/booking-payment-schedule-dialog.js +10 -8
  17. package/dist/components/booking-payment-schedule-list.d.ts.map +1 -1
  18. package/dist/components/booking-payment-schedule-list.js +32 -3
  19. package/dist/components/{rooms-stepper-section.d.ts → option-units-stepper-section.d.ts} +17 -9
  20. package/dist/components/option-units-stepper-section.d.ts.map +1 -0
  21. package/dist/components/option-units-stepper-section.js +172 -0
  22. package/dist/components/payment-schedule-section.d.ts +1 -1
  23. package/dist/components/payment-schedule-section.d.ts.map +1 -1
  24. package/dist/components/payment-schedule-section.js +5 -11
  25. package/dist/components/person-picker-section.d.ts +4 -0
  26. package/dist/components/person-picker-section.d.ts.map +1 -1
  27. package/dist/components/person-picker-section.js +27 -5
  28. package/dist/components/price-breakdown-section.d.ts +8 -2
  29. package/dist/components/price-breakdown-section.d.ts.map +1 -1
  30. package/dist/components/price-breakdown-section.js +17 -5
  31. package/dist/components/status-change-dialog.d.ts.map +1 -1
  32. package/dist/components/status-change-dialog.js +6 -5
  33. package/dist/components/supplier-status-dialog.d.ts.map +1 -1
  34. package/dist/components/supplier-status-dialog.js +6 -5
  35. package/dist/components/traveler-list.d.ts.map +1 -1
  36. package/dist/components/traveler-list.js +12 -1
  37. package/dist/components/travelers-section.d.ts +62 -3
  38. package/dist/components/travelers-section.d.ts.map +1 -1
  39. package/dist/components/travelers-section.js +290 -23
  40. package/dist/i18n/en.d.ts +63 -0
  41. package/dist/i18n/en.d.ts.map +1 -1
  42. package/dist/i18n/en.js +68 -5
  43. package/dist/i18n/messages.d.ts +63 -0
  44. package/dist/i18n/messages.d.ts.map +1 -1
  45. package/dist/i18n/provider.d.ts +126 -0
  46. package/dist/i18n/provider.d.ts.map +1 -1
  47. package/dist/i18n/ro.d.ts +63 -0
  48. package/dist/i18n/ro.d.ts.map +1 -1
  49. package/dist/i18n/ro.js +68 -5
  50. package/dist/index.d.ts +1 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +1 -1
  53. package/package.json +26 -24
  54. package/dist/components/rooms-stepper-section.d.ts.map +0 -1
  55. 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-4xl 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 }) })] }));
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,2CAgK5B"}
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,2CAqN7B"}
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,2CA2RxB"}
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
- }, [form, open, item]);
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,2CA6IpF"}
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,2CAsMnC"}
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":"AAuBA,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,0BAA0B,CAAC,EAAE,SAAS,EAAE,EAAE,+BAA+B,2CA8HxF"}
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 { useBookingPaymentScheduleMutation, useBookingPaymentSchedules, } from "@voyantjs/finance-react";
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 { CalendarClock, Pencil, Plus, Trash2 } from "lucide-react";
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 RoomsStepperValue {
2
+ export interface OptionUnitsStepperValue {
3
3
  quantities: Record<string, number>;
4
4
  }
5
- export declare const emptyRoomsStepperValue: RoomsStepperValue;
6
- export interface RoomsStepperUnit {
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 RoomsStepperSectionProps {
16
- value: RoomsStepperValue;
17
- onChange: (value: RoomsStepperValue) => void;
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: RoomsStepperUnit[]) => void;
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 RoomsStepperSection({ value, onChange, productId, slotId, optionId, enabled, onUnitsChange, labels, }: RoomsStepperSectionProps): import("react/jsx-runtime").JSX.Element;
62
- //# sourceMappingURL=rooms-stepper-section.d.ts.map
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 = "unpaid" | "full" | "advance" | "split";
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. */