@voyantjs/bookings-ui 0.50.5 → 0.50.7

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 (43) hide show
  1. package/dist/components/booking-create-dialog.d.ts +1 -1
  2. package/dist/components/booking-create-dialog.d.ts.map +1 -1
  3. package/dist/components/booking-create-dialog.js +94 -27
  4. package/dist/components/booking-create-page.d.ts +2 -1
  5. package/dist/components/booking-create-page.d.ts.map +1 -1
  6. package/dist/components/booking-create-page.js +2 -2
  7. package/dist/components/booking-create-utils.d.ts +33 -0
  8. package/dist/components/booking-create-utils.d.ts.map +1 -0
  9. package/dist/components/booking-create-utils.js +67 -0
  10. package/dist/components/booking-list.d.ts +2 -1
  11. package/dist/components/booking-list.d.ts.map +1 -1
  12. package/dist/components/booking-list.js +5 -1
  13. package/dist/components/bookings-page.d.ts +2 -1
  14. package/dist/components/bookings-page.d.ts.map +1 -1
  15. package/dist/components/bookings-page.js +2 -2
  16. package/dist/components/payment-schedule-section.d.ts +23 -0
  17. package/dist/components/payment-schedule-section.d.ts.map +1 -1
  18. package/dist/components/payment-schedule-section.js +48 -3
  19. package/dist/components/price-breakdown-section.d.ts +1 -0
  20. package/dist/components/price-breakdown-section.d.ts.map +1 -1
  21. package/dist/components/price-breakdown-section.js +2 -0
  22. package/dist/components/product-picker-section.d.ts.map +1 -1
  23. package/dist/components/product-picker-section.js +8 -4
  24. package/dist/components/rooms-stepper-section.d.ts +12 -7
  25. package/dist/components/rooms-stepper-section.d.ts.map +1 -1
  26. package/dist/components/rooms-stepper-section.js +24 -8
  27. package/dist/components/shared-room-section.d.ts +2 -0
  28. package/dist/components/shared-room-section.d.ts.map +1 -1
  29. package/dist/components/shared-room-section.js +14 -2
  30. package/dist/components/travelers-section.d.ts +15 -10
  31. package/dist/components/travelers-section.d.ts.map +1 -1
  32. package/dist/components/travelers-section.js +96 -14
  33. package/dist/i18n/en.d.ts +32 -0
  34. package/dist/i18n/en.d.ts.map +1 -1
  35. package/dist/i18n/en.js +32 -0
  36. package/dist/i18n/messages.d.ts +32 -0
  37. package/dist/i18n/messages.d.ts.map +1 -1
  38. package/dist/i18n/provider.d.ts +64 -0
  39. package/dist/i18n/provider.d.ts.map +1 -1
  40. package/dist/i18n/ro.d.ts +32 -0
  41. package/dist/i18n/ro.d.ts.map +1 -1
  42. package/dist/i18n/ro.js +32 -0
  43. package/package.json +24 -24
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Button, Label } from "@voyantjs/ui/components";
3
+ import { Button, Checkbox, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
4
4
  import { CurrencyInput } from "@voyantjs/ui/components/currency-input";
5
5
  import { DatePicker } from "@voyantjs/ui/components/date-picker";
6
- import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
6
+ import { useBookingsUiI18nOrDefault, useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
7
7
  export const emptyPaymentScheduleValue = {
8
8
  mode: "unpaid",
9
9
  fullDueDate: null,
@@ -13,6 +13,22 @@ export const emptyPaymentScheduleValue = {
13
13
  splitFirstDueDate: null,
14
14
  splitSecondAmountCents: null,
15
15
  splitSecondDueDate: null,
16
+ fullAlreadyPaid: false,
17
+ fullPaymentDate: null,
18
+ fullPaymentMethod: "bank_transfer",
19
+ fullPaymentReference: "",
20
+ advanceAlreadyPaid: false,
21
+ advancePaymentDate: null,
22
+ advancePaymentMethod: "bank_transfer",
23
+ advancePaymentReference: "",
24
+ splitFirstAlreadyPaid: false,
25
+ splitFirstPaymentDate: null,
26
+ splitFirstPaymentMethod: "bank_transfer",
27
+ splitFirstPaymentReference: "",
28
+ splitSecondAlreadyPaid: false,
29
+ splitSecondPaymentDate: null,
30
+ splitSecondPaymentMethod: "bank_transfer",
31
+ splitSecondPaymentReference: "",
16
32
  };
17
33
  /**
18
34
  * Payment schedule picker for booking-create flows. Operators choose one of
@@ -36,6 +52,7 @@ export const emptyPaymentScheduleValue = {
36
52
  */
37
53
  export function PaymentScheduleSection({ value, onChange, totalAmountCents, currency, labels, }) {
38
54
  const messages = useBookingsUiMessagesOrDefault();
55
+ const { formatCurrency, formatNumber } = useBookingsUiI18nOrDefault();
39
56
  const merged = { ...messages.paymentScheduleSection.labels, ...labels };
40
57
  const set = (patch) => onChange({ ...value, ...patch });
41
58
  const modes = [
@@ -55,5 +72,33 @@ export function PaymentScheduleSection({ value, onChange, totalAmountCents, curr
55
72
  splitSecondAmountCents: totalAmountCents - half,
56
73
  });
57
74
  };
58
- return (_jsxs("div", { className: "flex flex-col gap-3 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsx("div", { className: "flex flex-wrap items-center gap-2", children: modes.map((mode) => (_jsx(Button, { type: "button", size: "sm", variant: value.mode === mode.id ? "default" : "ghost", onClick: () => set({ mode: mode.id }), children: mode.label }, mode.id))) }), value.mode === "unpaid" && (_jsx("p", { className: "text-xs text-muted-foreground", children: merged.unpaidHint })), value.mode === "full" && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.dueDate }), _jsx(DatePicker, { value: value.fullDueDate ?? "", onChange: (nextValue) => set({ fullDueDate: nextValue }) })] })), value.mode === "advance" && (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.amount }), _jsx(CurrencyInput, { value: value.advanceAmountCents, onChange: (next) => set({ advanceAmountCents: next }), currency: currency })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.dueDate }), _jsx(DatePicker, { value: value.advanceDueDate ?? "", onChange: (nextValue) => set({ advanceDueDate: nextValue }) })] })] })), value.mode === "split" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-xs font-medium", children: merged.firstInstallment }), totalAmountCents ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: handlePreset5050, children: merged.preset5050 })) : null] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(CurrencyInput, { placeholder: merged.amount, value: value.splitFirstAmountCents, onChange: (next) => set({ splitFirstAmountCents: next }), currency: currency }), _jsx(DatePicker, { value: value.splitFirstDueDate ?? "", onChange: (nextValue) => set({ splitFirstDueDate: nextValue }) })] }), _jsx("div", { className: "text-xs font-medium", children: merged.secondInstallment }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(CurrencyInput, { placeholder: merged.amount, value: value.splitSecondAmountCents, onChange: (next) => set({ splitSecondAmountCents: next }), currency: currency }), _jsx(DatePicker, { value: value.splitSecondDueDate ?? "", onChange: (nextValue) => set({ splitSecondDueDate: nextValue }) })] })] }))] }));
75
+ const total = typeof totalAmountCents === "number" ? totalAmountCents : null;
76
+ const scheduledTotal = value.mode === "unpaid"
77
+ ? 0
78
+ : value.mode === "full"
79
+ ? (total ?? 0)
80
+ : value.mode === "advance"
81
+ ? (value.advanceAmountCents ?? 0)
82
+ : (value.splitFirstAmountCents ?? 0) + (value.splitSecondAmountCents ?? 0);
83
+ const remaining = total === null ? null : Math.max(0, total - scheduledTotal);
84
+ const formatAmount = (cents) => {
85
+ if (cents === null)
86
+ return "-";
87
+ return currency
88
+ ? formatCurrency(cents / 100, currency)
89
+ : formatNumber(cents / 100, {
90
+ minimumFractionDigits: 2,
91
+ maximumFractionDigits: 2,
92
+ });
93
+ };
94
+ const paymentMethodLabels = messages.bookingPaymentsSummary.paymentMethodLabels;
95
+ const renderPaidFields = (prefix, checked) => {
96
+ const paymentDateKey = `${prefix}PaymentDate`;
97
+ const paymentMethodKey = `${prefix}PaymentMethod`;
98
+ const paymentReferenceKey = `${prefix}PaymentReference`;
99
+ const checkedKey = `${prefix}AlreadyPaid`;
100
+ const checkboxId = `payment-schedule-${prefix}-already-paid`;
101
+ return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border border-dashed p-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: checkboxId, checked: checked, onCheckedChange: (next) => set({ [checkedKey]: next === true }) }), _jsx(Label, { htmlFor: checkboxId, className: "cursor-pointer text-xs", children: merged.alreadyPaid })] }), checked ? (_jsxs("div", { className: "grid gap-2 sm:grid-cols-3", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.paymentDate }), _jsx(DatePicker, { value: value[paymentDateKey] ?? "", onChange: (nextValue) => set({ [paymentDateKey]: nextValue }) })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.paymentMethod }), _jsxs(Select, { value: value[paymentMethodKey] ?? "bank_transfer", onValueChange: (nextValue) => set({ [paymentMethodKey]: nextValue ?? "bank_transfer" }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: ["bank_transfer", "credit_card", "cash", "voucher", "other"].map((method) => (_jsx(SelectItem, { value: method, children: paymentMethodLabels[method === "credit_card" ? "card" : method] }, method))) })] })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.paymentReference }), _jsx(Input, { value: value[paymentReferenceKey] ?? "", onChange: (event) => set({ [paymentReferenceKey]: event.target.value }) })] })] })) : null] }));
102
+ };
103
+ return (_jsxs("div", { className: "flex flex-col gap-3 rounded-md border p-3", children: [_jsx(Label, { children: merged.heading }), _jsxs("div", { className: "grid gap-2 rounded-md bg-muted/40 p-2 text-xs sm:grid-cols-3", children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-muted-foreground", children: merged.totalDue }), _jsx("span", { className: "font-medium tabular-nums", children: formatAmount(total) })] }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-muted-foreground", children: merged.scheduledTotal }), _jsx("span", { className: "font-medium tabular-nums", children: formatAmount(scheduledTotal) })] }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-muted-foreground", children: merged.remaining }), _jsx("span", { className: "font-medium tabular-nums", children: formatAmount(remaining) })] })] }), _jsx("div", { className: "flex flex-wrap items-center gap-2", children: modes.map((mode) => (_jsx(Button, { type: "button", size: "sm", variant: value.mode === mode.id ? "default" : "ghost", onClick: () => set({ mode: mode.id }), children: mode.label }, mode.id))) }), value.mode === "unpaid" && (_jsx("p", { className: "text-xs text-muted-foreground", children: merged.unpaidHint })), value.mode === "full" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.dueDate }), _jsx(DatePicker, { value: value.fullDueDate ?? "", onChange: (nextValue) => set({ fullDueDate: nextValue }) })] }), renderPaidFields("full", value.fullAlreadyPaid)] })), value.mode === "advance" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.amount }), _jsx(CurrencyInput, { value: value.advanceAmountCents, onChange: (next) => set({ advanceAmountCents: next }), currency: currency })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.dueDate }), _jsx(DatePicker, { value: value.advanceDueDate ?? "", onChange: (nextValue) => set({ advanceDueDate: nextValue }) })] })] }), renderPaidFields("advance", value.advanceAlreadyPaid)] })), value.mode === "split" && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-xs font-medium", children: merged.firstInstallment }), totalAmountCents ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: handlePreset5050, children: merged.preset5050 })) : null] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(CurrencyInput, { placeholder: merged.amount, value: value.splitFirstAmountCents, onChange: (next) => set({ splitFirstAmountCents: next }), currency: currency }), _jsx(DatePicker, { value: value.splitFirstDueDate ?? "", onChange: (nextValue) => set({ splitFirstDueDate: nextValue }) })] }), renderPaidFields("splitFirst", value.splitFirstAlreadyPaid), _jsx("div", { className: "text-xs font-medium", children: merged.secondInstallment }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(CurrencyInput, { placeholder: merged.amount, value: value.splitSecondAmountCents, onChange: (next) => set({ splitSecondAmountCents: next }), currency: currency }), _jsx(DatePicker, { value: value.splitSecondDueDate ?? "", onChange: (nextValue) => set({ splitSecondDueDate: nextValue }) })] }), renderPaidFields("splitSecond", value.splitSecondAlreadyPaid)] }))] }));
59
104
  }
@@ -20,6 +20,7 @@ export interface PriceBreakdownValue {
20
20
  priceOverrideReason: string;
21
21
  isManualOverride: boolean;
22
22
  requiresReason: boolean;
23
+ lines: PriceBreakdownLine[];
23
24
  }
24
25
  export interface PriceBreakdownSectionProps {
25
26
  productId?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"price-breakdown-section.d.ts","sourceRoot":"","sources":["../../src/components/price-breakdown-section.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,4EAA4E;IAC5E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gBAAgB,EAAE,OAAO,CAAA;IACzB,cAAc,EAAE,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,sBAAsB,CAAC,EAAE,MAAM,CAAA;KAChC,CAAA;IACD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAA;CAChD;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAQ,EACR,cAAc,EACd,SAAS,EACT,MAAM,EACN,QAAQ,GACT,EAAE,0BAA0B,kDAoQ5B"}
1
+ {"version":3,"file":"price-breakdown-section.d.ts","sourceRoot":"","sources":["../../src/components/price-breakdown-section.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,4EAA4E;IAC5E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,gBAAgB,EAAE,OAAO,CAAA;IACzB,cAAc,EAAE,OAAO,CAAA;IACvB,KAAK,EAAE,kBAAkB,EAAE,CAAA;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,sBAAsB,CAAC,EAAE,MAAM,CAAA;KAChC,CAAA;IACD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAA;CAChD;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAQ,EACR,cAAc,EACd,SAAS,EACT,MAAM,EACN,QAAQ,GACT,EAAE,0BAA0B,kDAsQ5B"}
@@ -167,11 +167,13 @@ export function PriceBreakdownSection({ productId, optionId, unitQuantities, cat
167
167
  priceOverrideReason: overrideReason,
168
168
  isManualOverride,
169
169
  requiresReason,
170
+ lines,
170
171
  });
171
172
  }, [
172
173
  confirmedAmountCents,
173
174
  currency,
174
175
  isManualOverride,
176
+ lines,
175
177
  onChange,
176
178
  overrideReason,
177
179
  requiresReason,
@@ -1 +1 @@
1
- {"version":3,"file":"product-picker-section.d.ts","sourceRoot":"","sources":["../../src/components/product-picker-section.tsx"],"names":[],"mappings":"AA8BA,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,uFAAuF;IACvF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,kBAAkB,CAAA;IACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAC7C,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qGAAqG;IACrG,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,QAAQ,EACR,OAAc,EACd,WAAmB,EACnB,MAAM,GACP,EAAE,yBAAyB,2CAgI3B"}
1
+ {"version":3,"file":"product-picker-section.d.ts","sourceRoot":"","sources":["../../src/components/product-picker-section.tsx"],"names":[],"mappings":"AA+BA,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,uFAAuF;IACvF,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,kBAAkB,CAAA;IACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAC7C,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qGAAqG;IACrG,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,QAAQ,EACR,OAAc,EACd,WAAmB,EACnB,MAAM,GACP,EAAE,yBAAyB,2CAyI3B"}
@@ -5,6 +5,7 @@ import { Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, }
5
5
  import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
6
6
  import * as React from "react";
7
7
  import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
8
+ import { productMatchesPickerSearch } from "./booking-create-utils.js";
8
9
  const OPTION_NONE = "__none__";
9
10
  /**
10
11
  * Controlled product + option picker. Splits `value` + `onChange` so apps can
@@ -13,6 +14,7 @@ const OPTION_NONE = "__none__";
13
14
  */
14
15
  export function ProductPickerSection({ value, onChange, enabled = true, lockProduct = false, labels, }) {
15
16
  const [productSearch, setProductSearch] = React.useState("");
17
+ const cachedProductsRef = React.useRef(new Map());
16
18
  const messages = useBookingsUiMessagesOrDefault();
17
19
  const merged = { ...messages.productPickerSection.labels, ...labels };
18
20
  const { data: productsData } = useProducts({
@@ -24,15 +26,17 @@ export function ProductPickerSection({ value, onChange, enabled = true, lockProd
24
26
  enabled: enabled && Boolean(value.productId),
25
27
  });
26
28
  const products = React.useMemo(() => {
27
- const map = new Map();
29
+ const map = new Map(cachedProductsRef.current);
28
30
  for (const product of productsData?.data ?? [])
29
31
  map.set(product.id, product);
30
32
  if (selectedProductQuery.data)
31
33
  map.set(selectedProductQuery.data.id, selectedProductQuery.data);
34
+ cachedProductsRef.current = map;
32
35
  return Array.from(map.values());
33
36
  }, [productsData?.data, selectedProductQuery.data]);
34
37
  const productMap = React.useMemo(() => new Map(products.map((product) => [product.id, product])), [products]);
35
- const selectedProductLabel = value.productId ? (productMap.get(value.productId)?.name ?? "") : "";
38
+ const resolveProductLabel = React.useCallback((productId) => productMap.get(productId)?.name ?? cachedProductsRef.current.get(productId)?.name ?? "", [productMap]);
39
+ const selectedProductLabel = value.productId ? resolveProductLabel(value.productId) : "";
36
40
  const [productInputValue, setProductInputValue] = React.useState(selectedProductLabel);
37
41
  React.useEffect(() => {
38
42
  if (selectedProductLabel)
@@ -44,7 +48,7 @@ export function ProductPickerSection({ value, onChange, enabled = true, lockProd
44
48
  enabled: enabled && Boolean(value.productId),
45
49
  });
46
50
  const options = optionsData?.data ?? [];
47
- return (_jsxs(_Fragment, { children: [!lockProduct && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs(Label, { children: [merged.product, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Combobox, { items: products.map((product) => product.id), value: value.productId || null, inputValue: productInputValue, autoHighlight: true, disabled: !enabled, itemToStringValue: (id) => productMap.get(id)?.name ?? "", onInputValueChange: (next) => {
51
+ return (_jsxs(_Fragment, { children: [!lockProduct && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs(Label, { children: [merged.product, " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsxs(Combobox, { items: products.map((product) => product.id), value: value.productId || null, inputValue: productInputValue, autoHighlight: true, disabled: !enabled, filter: (id, query) => productMatchesPickerSearch(productMap.get(id), query), itemToStringLabel: (id) => resolveProductLabel(id) || id, itemToStringValue: (id) => id, onInputValueChange: (next) => {
48
52
  setProductInputValue(next);
49
53
  setProductSearch(next);
50
54
  if (!next)
@@ -52,7 +56,7 @@ export function ProductPickerSection({ value, onChange, enabled = true, lockProd
52
56
  }, onValueChange: (next) => {
53
57
  const productId = next ?? "";
54
58
  onChange({ productId, optionId: null });
55
- setProductInputValue(productId ? (productMap.get(productId)?.name ?? "") : "");
59
+ setProductInputValue(productId ? resolveProductLabel(productId) : "");
56
60
  }, children: [_jsx(ComboboxInput, { placeholder: merged.productSearchPlaceholder, showClear: !!value.productId }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: merged.productEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
57
61
  const product = productMap.get(id);
58
62
  if (!product)
@@ -7,14 +7,19 @@ export interface RoomsStepperSectionProps {
7
7
  value: RoomsStepperValue;
8
8
  onChange: (value: RoomsStepperValue) => void;
9
9
  /**
10
- * Departure the operator picked. Section renders nothing until a slot is
11
- * chosen per-unit availability is a property of the slot, not the
12
- * product, so there's nothing to show before then.
10
+ * Departure the operator picked. Departure-specific availability wins
11
+ * when present; otherwise the section falls back to option-level units.
13
12
  */
14
13
  slotId?: string;
14
+ /**
15
+ * Product option whose units should be shown before a departure is picked.
16
+ * Departure-specific availability wins when `slotId` is present.
17
+ */
18
+ optionId?: string | null;
15
19
  enabled?: boolean;
16
20
  labels?: {
17
21
  heading?: string;
22
+ noOption?: string;
18
23
  noSlot?: string;
19
24
  noUnits?: string;
20
25
  remaining?: string;
@@ -23,9 +28,9 @@ export interface RoomsStepperSectionProps {
23
28
  }
24
29
  /**
25
30
  * Rooms / per-unit stepper for booking-create flows. Drives
26
- * `GET /v1/availability/slots/:id/unit-availability` from #235 so the
27
- * operator sees authoritative "3 doubles available" numbers instead of
28
- * client-side math against a sampled booking list.
31
+ * `GET /v1/availability/slots/:id/unit-availability` from #235 when a
32
+ * departure is selected, and option-level units before departure selection,
33
+ * so operators can still build "2 double rooms and 1 single" drafts.
29
34
  *
30
35
  * The section only tracks **intent** (how many of each unit the operator
31
36
  * wants to book). Actual hold/reservation happens when the parent submits
@@ -41,5 +46,5 @@ export interface RoomsStepperSectionProps {
41
46
  * disables the "+" button — we don't let the UI submit a request that
42
47
  * would 409 at insert time.
43
48
  */
44
- export declare function RoomsStepperSection({ value, onChange, slotId, enabled, labels, }: RoomsStepperSectionProps): import("react/jsx-runtime").JSX.Element;
49
+ export declare function RoomsStepperSection({ value, onChange, slotId, optionId, enabled, labels, }: RoomsStepperSectionProps): import("react/jsx-runtime").JSX.Element;
45
50
  //# sourceMappingURL=rooms-stepper-section.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rooms-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/rooms-stepper-section.tsx"],"names":[],"mappings":"AAOA,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAAsC,CAAA;AAE3E,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,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,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,MAAM,EACN,OAAc,EACd,MAAM,GACP,EAAE,wBAAwB,2CAoF1B"}
1
+ {"version":3,"file":"rooms-stepper-section.d.ts","sourceRoot":"","sources":["../../src/components/rooms-stepper-section.tsx"],"names":[],"mappings":"AAQA,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnC;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAAsC,CAAA;AAE3E,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,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,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,OAAc,EACd,MAAM,GACP,EAAE,wBAAwB,2CAmG1B"}
@@ -1,15 +1,16 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useSlotUnitAvailability } from "@voyantjs/availability-react";
4
+ import { useOptionUnits } from "@voyantjs/products-react";
4
5
  import { Button, Label } from "@voyantjs/ui/components";
5
6
  import { Minus, Plus } from "lucide-react";
6
7
  import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
7
8
  export const emptyRoomsStepperValue = { quantities: {} };
8
9
  /**
9
10
  * Rooms / per-unit stepper for booking-create flows. Drives
10
- * `GET /v1/availability/slots/:id/unit-availability` from #235 so the
11
- * operator sees authoritative "3 doubles available" numbers instead of
12
- * client-side math against a sampled booking list.
11
+ * `GET /v1/availability/slots/:id/unit-availability` from #235 when a
12
+ * departure is selected, and option-level units before departure selection,
13
+ * so operators can still build "2 double rooms and 1 single" drafts.
13
14
  *
14
15
  * The section only tracks **intent** (how many of each unit the operator
15
16
  * wants to book). Actual hold/reservation happens when the parent submits
@@ -25,15 +26,30 @@ export const emptyRoomsStepperValue = { quantities: {} };
25
26
  * disables the "+" button — we don't let the UI submit a request that
26
27
  * would 409 at insert time.
27
28
  */
28
- export function RoomsStepperSection({ value, onChange, slotId, enabled = true, labels, }) {
29
+ export function RoomsStepperSection({ value, onChange, slotId, optionId, enabled = true, labels, }) {
29
30
  const messages = useBookingsUiMessagesOrDefault();
30
31
  const merged = { ...messages.roomsStepperSection.labels, ...labels };
31
32
  const availability = useSlotUnitAvailability({ slotId, enabled: enabled && Boolean(slotId) });
32
- const units = availability.data?.data ?? [];
33
- if (!slotId) {
34
- 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.noSlot })] }));
33
+ const optionUnits = useOptionUnits({
34
+ optionId: optionId ?? undefined,
35
+ limit: 100,
36
+ enabled: enabled && !slotId && Boolean(optionId),
37
+ });
38
+ const units = slotId
39
+ ? (availability.data?.data ?? [])
40
+ : (optionUnits.data?.data ?? []).map((unit) => ({
41
+ optionUnitId: unit.id,
42
+ unitName: unit.name,
43
+ occupancyMax: unit.occupancyMax,
44
+ initial: null,
45
+ reserved: 0,
46
+ remaining: unit.maxQuantity ?? null,
47
+ }));
48
+ if (!slotId && !optionId) {
49
+ 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 })] }));
35
50
  }
36
- if (availability.isSuccess && units.length === 0) {
51
+ const loaded = slotId ? availability.isSuccess : optionUnits.isSuccess;
52
+ if (loaded && units.length === 0) {
37
53
  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 })] }));
38
54
  }
39
55
  const setQuantity = (unitId, qty) => {
@@ -8,6 +8,7 @@ export interface SharedRoomValue {
8
8
  groupLabel?: string;
9
9
  }
10
10
  export declare const emptySharedRoomValue: SharedRoomValue;
11
+ export declare function clearSharedRoomValue(): SharedRoomValue;
11
12
  export interface SharedRoomSectionProps {
12
13
  value: SharedRoomValue;
13
14
  onChange: (value: SharedRoomValue) => void;
@@ -28,6 +29,7 @@ export interface SharedRoomSectionProps {
28
29
  groupLabel?: string;
29
30
  groupLabelPlaceholder?: string;
30
31
  createAction?: string;
32
+ remove?: string;
31
33
  };
32
34
  }
33
35
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"shared-room-section.d.ts","sourceRoot":"","sources":["../../src/components/shared-room-section.tsx"],"names":[],"mappings":"AA2BA,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,CAAA;AAE9C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,cAAc,CAAA;IACpB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAA;IACf,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,eAAO,MAAM,oBAAoB,EAAE,eAKlC,CAAA;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,eAAe,CAAA;IACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IAC1C;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAA;CACF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,EACR,SAAS,EACT,OAAc,EACd,MAAM,GACP,EAAE,sBAAsB,2CAyJxB"}
1
+ {"version":3,"file":"shared-room-section.d.ts","sourceRoot":"","sources":["../../src/components/shared-room-section.tsx"],"names":[],"mappings":"AA2BA,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,CAAA;AAE9C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,cAAc,CAAA;IACpB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAA;IACf,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,eAAO,MAAM,oBAAoB,EAAE,eAKlC,CAAA;AAED,wBAAgB,oBAAoB,IAAI,eAAe,CAEtD;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,eAAe,CAAA;IACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IAC1C;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,iBAAiB,CAAC,EAAE,MAAM,CAAA;QAC1B,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;CACF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,EACR,SAAS,EACT,OAAc,EACd,MAAM,GACP,EAAE,sBAAsB,2CAyKxB"}
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { useBookingGroups } from "@voyantjs/bookings-react";
4
4
  import { Button, Input, Label, Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle, } from "@voyantjs/ui/components";
5
5
  import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
6
- import { Link2, Plus } from "lucide-react";
6
+ import { Link2, Plus, X } from "lucide-react";
7
7
  import * as React from "react";
8
8
  import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
9
9
  export const emptySharedRoomValue = {
@@ -12,6 +12,9 @@ export const emptySharedRoomValue = {
12
12
  groupId: "",
13
13
  groupLabel: "",
14
14
  };
15
+ export function clearSharedRoomValue() {
16
+ return { ...emptySharedRoomValue };
17
+ }
15
18
  /**
16
19
  * Shared-room (partaj) attachment section. Operators can create a new group in
17
20
  * a sheet or join an existing product-scoped group with an async combobox.
@@ -47,6 +50,15 @@ export function SharedRoomSection({ value, onChange, productId, enabled = true,
47
50
  setDraftGroupLabel(value.groupLabel ?? "");
48
51
  }, [createSheetOpen, value.groupLabel]);
49
52
  const set = (patch) => onChange({ ...value, ...patch });
53
+ const clear = () => {
54
+ if (!enabled)
55
+ return;
56
+ setCreateSheetOpen(false);
57
+ setDraftGroupLabel("");
58
+ setGroupInputValue("");
59
+ setGroupSearch("");
60
+ onChange(clearSharedRoomValue());
61
+ };
50
62
  const selectCreateMode = () => {
51
63
  if (!enabled)
52
64
  return;
@@ -66,7 +78,7 @@ export function SharedRoomSection({ value, onChange, productId, enabled = true,
66
78
  });
67
79
  setCreateSheetOpen(false);
68
80
  };
69
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-3 rounded-md border p-3", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { children: merged.toggle }), value.enabled && value.mode === "create" && value.groupLabel ? (_jsx("p", { className: "text-xs text-muted-foreground", children: value.groupLabel })) : value.enabled && value.mode === "create" ? (_jsx("p", { className: "text-xs text-muted-foreground", children: merged.createHint })) : null] }), _jsxs("div", { className: "grid gap-2 sm:grid-cols-2", children: [_jsxs(Button, { type: "button", size: "sm", variant: value.enabled && value.mode === "create" ? "default" : "outline", onClick: selectCreateMode, disabled: !enabled, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), merged.createMode] }), _jsxs(Button, { type: "button", size: "sm", variant: value.enabled && value.mode === "join" ? "default" : "outline", onClick: selectJoinMode, disabled: !enabled || !productId, children: [_jsx(Link2, { className: "mr-2 h-4 w-4" }), merged.joinMode] })] }), value.enabled && value.mode === "join" ? (_jsxs(Combobox, { items: existingGroups.map((group) => group.id), value: value.groupId || null, inputValue: groupInputValue, autoHighlight: true, disabled: !enabled || !productId, itemToStringValue: (id) => groupsMap.get(id)?.label ?? "", onInputValueChange: (next) => {
81
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-3 rounded-md border p-3", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsx(Label, { children: merged.toggle }), value.enabled && value.mode === "create" && value.groupLabel ? (_jsx("p", { className: "truncate text-xs text-muted-foreground", children: value.groupLabel })) : value.enabled && value.mode === "create" ? (_jsx("p", { className: "text-xs text-muted-foreground", children: merged.createHint })) : null] }), value.enabled ? (_jsxs(Button, { type: "button", size: "sm", variant: "ghost", onClick: clear, disabled: !enabled, children: [_jsx(X, { className: "mr-2 h-4 w-4" }), merged.remove] })) : null] }), _jsxs("div", { className: "grid gap-2 sm:grid-cols-2", children: [_jsxs(Button, { type: "button", size: "sm", variant: value.enabled && value.mode === "create" ? "default" : "outline", onClick: selectCreateMode, disabled: !enabled, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), merged.createMode] }), _jsxs(Button, { type: "button", size: "sm", variant: value.enabled && value.mode === "join" ? "default" : "outline", onClick: selectJoinMode, disabled: !enabled || !productId, children: [_jsx(Link2, { className: "mr-2 h-4 w-4" }), merged.joinMode] })] }), value.enabled && value.mode === "join" ? (_jsxs(Combobox, { items: existingGroups.map((group) => group.id), value: value.groupId || null, inputValue: groupInputValue, autoHighlight: true, disabled: !enabled || !productId, itemToStringValue: (id) => groupsMap.get(id)?.label ?? "", onInputValueChange: (next) => {
70
82
  setGroupInputValue(next);
71
83
  setGroupSearch(next);
72
84
  if (!next)
@@ -1,5 +1,6 @@
1
1
  export type TravelerRole = "lead" | "adult" | "child" | "infant";
2
2
  export interface TravelerEntry {
3
+ personId: string | null;
3
4
  firstName: string;
4
5
  lastName: string;
5
6
  email: string;
@@ -31,6 +32,7 @@ export interface TravelersSectionProps {
31
32
  * When provided, each traveler gets a room-assignment dropdown.
32
33
  */
33
34
  roomUnits?: RoomUnitOption[];
35
+ billingPersonId?: string | null;
34
36
  labels?: {
35
37
  heading?: string;
36
38
  addTraveler?: string;
@@ -46,27 +48,30 @@ export interface TravelersSectionProps {
46
48
  noRoom?: string;
47
49
  remove?: string;
48
50
  empty?: string;
51
+ person?: string;
52
+ personSearchPlaceholder?: string;
53
+ personEmpty?: string;
54
+ createNewPerson?: string;
55
+ createPersonSheetTitle?: string;
56
+ addBillingPerson?: string;
49
57
  };
50
58
  }
51
59
  /**
52
- * Traveler list for booking-create flows. Each row carries name + optional
53
- * email + role + optional room assignment. Inline-create only for now
54
- * operators who want to pick an existing CRM person can do so from the
55
- * booking detail page afterwards, consistent with the lead-person picker's
56
- * edit-after-create story.
60
+ * Traveler list for booking-create flows. Each row can point at an existing
61
+ * CRM person, create a new CRM person, or carry manual name/email details,
62
+ * plus role and optional room assignment.
57
63
  *
58
64
  * ### Parent contract
59
65
  *
60
66
  * At submit time, the parent:
61
- * 1. Creates a CRM person for each row that doesn't match an existing one
62
- * (email match + name, or skip when the operator intentionally left
63
- * email blank).
64
- * 2. Inserts a `booking_travelers` row per traveler with `participantType`
67
+ * 1. Inserts a `booking_travelers` row per traveler with `participantType`
65
68
  * derived from the role (`lead` / `adult` → traveler; `child` / `infant`
66
69
  * → traveler with travelerCategory set).
70
+ * 2. Carries `personId` through when the traveler is tied to CRM, including
71
+ * when the payer is also traveling.
67
72
  * 3. Exactly one row should have `role: "lead"` — enforced at submit, not
68
73
  * here. The UI lets the operator pick whichever layout they want, then
69
74
  * the submit handler errors if the invariant isn't met.
70
75
  */
71
- export declare function TravelersSection({ value, onChange, roomUnits, labels }: TravelersSectionProps): import("react/jsx-runtime").JSX.Element;
76
+ export declare function TravelersSection({ value, onChange, roomUnits, billingPersonId, labels, }: TravelersSectionProps): import("react/jsx-runtime").JSX.Element;
72
77
  //# sourceMappingURL=travelers-section.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"travelers-section.d.ts","sourceRoot":"","sources":["../../src/components/travelers-section.tsx"],"names":[],"mappings":"AAeA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAA;AAIhE,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,YAAY,CAAA;IAClB,+EAA+E;IAC/E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,aAAa,EAAE,CAAA;CAC3B;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAAqC,CAAA;AAE1E,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,YAAsB,GAAG,aAAa,CAE/E;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAA;IAC5B,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;CACF;AAID;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,qBAAqB,2CA4I7F"}
1
+ {"version":3,"file":"travelers-section.d.ts","sourceRoot":"","sources":["../../src/components/travelers-section.tsx"],"names":[],"mappings":"AAgCA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAA;AAIhE,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,YAAY,CAAA;IAClB,+EAA+E;IAC/E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,aAAa,EAAE,CAAA;CAC3B;AAED,eAAO,MAAM,sBAAsB,EAAE,iBAAqC,CAAA;AAE1E,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,YAAsB,GAAG,aAAa,CAE/E;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,iBAAiB,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;IAC5C;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,EAAE,CAAA;IAC5B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,uBAAuB,CAAC,EAAE,MAAM,CAAA;QAChC,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,sBAAsB,CAAC,EAAE,MAAM,CAAA;QAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B,CAAA;CACF;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,QAAQ,EACR,SAAS,EACT,eAAe,EACf,MAAM,GACP,EAAE,qBAAqB,2CAuLvB"}
@@ -1,38 +1,42 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
4
- import { Trash2 } from "lucide-react";
3
+ import { usePeople, usePerson } from "@voyantjs/crm-react";
4
+ import { PersonForm } from "@voyantjs/crm-ui";
5
+ import { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, } from "@voyantjs/ui/components";
6
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
7
+ import { Trash2, UserPlus } from "lucide-react";
8
+ import * as React from "react";
5
9
  import { useBookingsUiMessagesOrDefault } from "../i18n/provider.js";
6
10
  const ALL_ROLES = ["lead", "adult", "child", "infant"];
7
11
  export const emptyTravelerListValue = { travelers: [] };
8
12
  /** Factory for a blank row — `role` defaults to `adult` unless the list is empty. */
9
13
  export function createBlankTraveler(role = "adult") {
10
- return { firstName: "", lastName: "", email: "", role, roomUnitId: null };
14
+ return { personId: null, firstName: "", lastName: "", email: "", role, roomUnitId: null };
11
15
  }
12
16
  const NO_ROOM = "__unassigned__";
13
17
  /**
14
- * Traveler list for booking-create flows. Each row carries name + optional
15
- * email + role + optional room assignment. Inline-create only for now
16
- * operators who want to pick an existing CRM person can do so from the
17
- * booking detail page afterwards, consistent with the lead-person picker's
18
- * edit-after-create story.
18
+ * Traveler list for booking-create flows. Each row can point at an existing
19
+ * CRM person, create a new CRM person, or carry manual name/email details,
20
+ * plus role and optional room assignment.
19
21
  *
20
22
  * ### Parent contract
21
23
  *
22
24
  * At submit time, the parent:
23
- * 1. Creates a CRM person for each row that doesn't match an existing one
24
- * (email match + name, or skip when the operator intentionally left
25
- * email blank).
26
- * 2. Inserts a `booking_travelers` row per traveler with `participantType`
25
+ * 1. Inserts a `booking_travelers` row per traveler with `participantType`
27
26
  * derived from the role (`lead` / `adult` → traveler; `child` / `infant`
28
27
  * → traveler with travelerCategory set).
28
+ * 2. Carries `personId` through when the traveler is tied to CRM, including
29
+ * when the payer is also traveling.
29
30
  * 3. Exactly one row should have `role: "lead"` — enforced at submit, not
30
31
  * here. The UI lets the operator pick whichever layout they want, then
31
32
  * the submit handler errors if the invariant isn't met.
32
33
  */
33
- export function TravelersSection({ value, onChange, roomUnits, labels }) {
34
+ export function TravelersSection({ value, onChange, roomUnits, billingPersonId, labels, }) {
34
35
  const messages = useBookingsUiMessagesOrDefault();
35
36
  const merged = { ...messages.travelersSection.labels, ...labels };
37
+ const billingPerson = usePerson(billingPersonId ?? undefined, {
38
+ enabled: Boolean(billingPersonId),
39
+ });
36
40
  const roleLabels = {
37
41
  lead: merged.roleLead,
38
42
  adult: merged.roleAdult,
@@ -52,9 +56,87 @@ export function TravelersSection({ value, onChange, roomUnits, labels }) {
52
56
  const role = value.travelers.length === 0 ? "lead" : "adult";
53
57
  onChange({ travelers: [...value.travelers, createBlankTraveler(role)] });
54
58
  };
55
- return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { children: merged.heading }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: addRow, children: merged.addTraveler })] }), value.travelers.length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground", children: merged.empty })) : (_jsx("div", { className: "flex flex-col gap-2", children: value.travelers.map((traveler, index) => (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-2", children: [_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(Input, { placeholder: merged.firstName, value: traveler.firstName, onChange: (e) => updateAt(index, { firstName: e.target.value }) }), _jsx(Input, { placeholder: merged.lastName, value: traveler.lastName, onChange: (e) => updateAt(index, { lastName: e.target.value }) })] }), _jsx(Input, { type: "email", placeholder: merged.email, value: traveler.email, onChange: (e) => updateAt(index, { email: e.target.value }) }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.role }), _jsxs(Select, { value: traveler.role, onValueChange: (v) => updateAt(index, { role: (v ?? "adult") }), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: ALL_ROLES.map((role) => (_jsx(SelectItem, { value: role, children: roleLabels[role] }, role))) })] })] }), roomUnits && roomUnits.length > 0 ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.room }), _jsxs(Select, { value: traveler.roomUnitId ?? NO_ROOM, onValueChange: (v) => updateAt(index, { roomUnitId: v === NO_ROOM ? null : (v ?? null) }), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: NO_ROOM, children: merged.noRoom }), roomUnits.map((unit) => (_jsx(SelectItem, { value: unit.unitId,
59
+ const addBillingPerson = () => {
60
+ if (!billingPerson.data)
61
+ return;
62
+ const role = value.travelers.length === 0 ? "lead" : "adult";
63
+ onChange({
64
+ travelers: [...value.travelers, createTravelerFromPerson(billingPerson.data, role)],
65
+ });
66
+ };
67
+ const hasBillingPersonTraveler = Boolean(billingPersonId && value.travelers.some((traveler) => traveler.personId === billingPersonId));
68
+ return (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { children: merged.heading }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: addRow, children: merged.addTraveler })] }), billingPersonId && !hasBillingPersonTraveler ? (_jsx("div", { children: _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: addBillingPerson, disabled: !billingPerson.data, children: [_jsx(UserPlus, { className: "mr-1 h-3.5 w-3.5" }), merged.addBillingPerson] }) })) : null, value.travelers.length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground", children: merged.empty })) : (_jsx("div", { className: "flex flex-col gap-2", children: value.travelers.map((traveler, index) => (_jsxs("div", { className: "flex flex-col gap-2 rounded-md border p-2", children: [_jsx(TravelerPersonPicker, { personId: traveler.personId, labels: merged, pinnedPeople: billingPerson.data ? [billingPerson.data] : [], onSelect: (person) => updateAt(index, {
69
+ personId: person.id,
70
+ firstName: person.firstName,
71
+ lastName: person.lastName,
72
+ email: person.email ?? "",
73
+ }), onClear: () => updateAt(index, { personId: null }) }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(Input, { placeholder: merged.firstName, value: traveler.firstName, onChange: (e) => updateAt(index, { firstName: e.target.value }) }), _jsx(Input, { placeholder: merged.lastName, value: traveler.lastName, onChange: (e) => updateAt(index, { lastName: e.target.value }) })] }), _jsx(Input, { type: "email", placeholder: merged.email, value: traveler.email, onChange: (e) => updateAt(index, { email: e.target.value }) }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.role }), _jsxs(Select, { value: traveler.role, onValueChange: (v) => updateAt(index, { role: (v ?? "adult") }), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: ALL_ROLES.map((role) => (_jsx(SelectItem, { value: role, children: roleLabels[role] }, role))) })] })] }), roomUnits && roomUnits.length > 0 ? (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx(Label, { className: "text-xs", children: merged.room }), _jsxs(Select, { value: traveler.roomUnitId ?? NO_ROOM, onValueChange: (v) => updateAt(index, { roomUnitId: v === NO_ROOM ? null : (v ?? null) }), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: NO_ROOM, children: merged.noRoom }), roomUnits.map((unit) => (_jsx(SelectItem, { value: unit.unitId,
56
74
  // Only disable other rooms at-capacity — the room the
57
75
  // traveler is *already* in should stay selectable so
58
76
  // re-renders don't strip the selection.
59
77
  disabled: unit.remainingCapacity <= 0 && traveler.roomUnitId !== unit.unitId, children: unit.unitName }, unit.unitId)))] })] })] })) : null] }), _jsx("div", { className: "flex justify-end", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 text-destructive", onClick: () => removeAt(index), "aria-label": merged.remove, children: [_jsx(Trash2, { className: "mr-1 h-3.5 w-3.5" }), merged.remove] }) })] }, index))) }))] }));
60
78
  }
79
+ function TravelerPersonPicker({ personId, labels, pinnedPeople = [], onSelect, onClear, }) {
80
+ const [search, setSearch] = React.useState("");
81
+ const [inputValue, setInputValue] = React.useState("");
82
+ const [sheetOpen, setSheetOpen] = React.useState(false);
83
+ const peopleQuery = usePeople({ search: search || undefined, limit: 20 });
84
+ const selectedPersonQuery = usePerson(personId ?? undefined, { enabled: Boolean(personId) });
85
+ const people = React.useMemo(() => {
86
+ const map = new Map();
87
+ for (const person of peopleQuery.data?.data ?? [])
88
+ map.set(person.id, person);
89
+ for (const person of pinnedPeople)
90
+ map.set(person.id, person);
91
+ if (selectedPersonQuery.data)
92
+ map.set(selectedPersonQuery.data.id, selectedPersonQuery.data);
93
+ return Array.from(map.values());
94
+ }, [peopleQuery.data?.data, pinnedPeople, selectedPersonQuery.data]);
95
+ const peopleMap = React.useMemo(() => new Map(people.map((person) => [person.id, person])), [people]);
96
+ const selectedLabel = personId ? formatPerson(peopleMap.get(personId)) : "";
97
+ React.useEffect(() => {
98
+ if (selectedLabel)
99
+ setInputValue(selectedLabel);
100
+ }, [selectedLabel]);
101
+ return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { className: "text-xs", children: labels.person }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7", onClick: () => setSheetOpen(true), children: [_jsx(UserPlus, { className: "mr-1 h-3.5 w-3.5" }), labels.createNewPerson] })] }), _jsxs(Combobox, { items: people.map((person) => person.id), value: personId, inputValue: inputValue, autoHighlight: true, itemToStringValue: (id) => formatPerson(peopleMap.get(id)), onInputValueChange: (next) => {
102
+ setInputValue(next);
103
+ setSearch(next);
104
+ if (!next)
105
+ onClear();
106
+ }, onValueChange: (next) => {
107
+ const nextPerson = peopleMap.get(next ?? "");
108
+ if (nextPerson)
109
+ onSelect(nextPerson);
110
+ setInputValue(nextPerson ? formatPerson(nextPerson) : "");
111
+ }, children: [_jsx(ComboboxInput, { placeholder: labels.personSearchPlaceholder, showClear: !!personId }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: labels.personEmpty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (id) => {
112
+ const person = peopleMap.get(id);
113
+ if (!person)
114
+ return null;
115
+ return (_jsx(ComboboxItem, { value: person.id, children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-medium", children: formatPersonName(person) }), person.email ? (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: person.email })) : null] }) }, person.id));
116
+ } }) })] })] }), _jsx(Sheet, { open: sheetOpen, onOpenChange: setSheetOpen, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: labels.createPersonSheetTitle }) }), _jsx(SheetBody, { children: _jsx(PersonForm, { mode: { kind: "create" }, onCancel: () => setSheetOpen(false), onSuccess: (saved) => {
117
+ onSelect(saved);
118
+ setInputValue(formatPerson(saved));
119
+ setSheetOpen(false);
120
+ } }) })] }) })] }));
121
+ }
122
+ function createTravelerFromPerson(person, role) {
123
+ return {
124
+ personId: person.id,
125
+ firstName: person.firstName,
126
+ lastName: person.lastName,
127
+ email: person.email ?? "",
128
+ role,
129
+ roomUnitId: null,
130
+ };
131
+ }
132
+ function formatPersonName(person) {
133
+ if (!person)
134
+ return "";
135
+ return [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
136
+ }
137
+ function formatPerson(person) {
138
+ if (!person)
139
+ return "";
140
+ const name = formatPersonName(person);
141
+ return person.email ? `${name} · ${person.email}` : name;
142
+ }