@voyantjs/bookings-ui 0.52.2 → 0.52.4

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.
@@ -67,4 +67,24 @@ export interface OptionUnitsStepperSectionProps {
67
67
  * would 409 at insert time.
68
68
  */
69
69
  export declare function OptionUnitsStepperSection({ value, onChange, productId, slotId, optionId, enabled, onUnitsChange, labels, }: OptionUnitsStepperSectionProps): import("react/jsx-runtime").JSX.Element;
70
+ /**
71
+ * Returns the `optionId` the slot is bound to, derived from the first
72
+ * slot-availability row whose `optionUnitId` we can map to a known
73
+ * product option. Falls back to the caller's `fallbackOptionId` (the
74
+ * dialog's currently-selected option) when no rows resolve — that lets
75
+ * the existing `optionId` prop drive the previous-behavior path for
76
+ * unit pickers that haven't loaded yet.
77
+ */
78
+ export declare function resolveSlotOptionId(slotRows: ReadonlyArray<{
79
+ optionUnitId: string;
80
+ }>, optionByUnitId: ReadonlyMap<string, string>, fallbackOptionId: string | null): string | null;
81
+ /**
82
+ * Merges slot-bound per-unit availability with the product's option-unit
83
+ * catalog. Slot rows are authoritative for the slot's option (they carry
84
+ * real-time `remaining`); product-level rows fill in the other options
85
+ * the product offers so the operator can still pick mixes the slot isn't
86
+ * explicitly tracking. When the slot is product-level (no `option_id`)
87
+ * or hasn't loaded slot rows yet, the product-level rows cover everything.
88
+ */
89
+ export declare function mergeStepperUnits(slotRows: ReadonlyArray<OptionUnitsStepperUnit>, productRows: ReadonlyArray<OptionUnitsStepperUnit>, slotOptionId: string | null, hasSlot: boolean): OptionUnitsStepperUnit[];
70
90
  //# sourceMappingURL=option-units-stepper-section.d.ts.map
@@ -1 +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"}
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,2CA+MhC;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,aAAa,CAAC;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,EACjD,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3C,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAC9B,MAAM,GAAG,IAAI,CAMf;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAC/C,WAAW,EAAE,aAAa,CAAC,sBAAsB,CAAC,EAClD,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,EAAE,OAAO,GACf,sBAAsB,EAAE,CAM1B"}
@@ -68,16 +68,36 @@ export function OptionUnitsStepperSection({ value, onChange, productId, slotId,
68
68
  });
69
69
  return rows;
70
70
  }, [productOptions, optionUnitQueries]);
71
+ // optionUnitId → optionId lookup, derived from the product's own option
72
+ // catalog. The slot-availability endpoint only returns option_unit rows
73
+ // for the slot's bound option and doesn't stamp the option_id on each
74
+ // row, so we look it up from the units we already fetched per option.
75
+ const optionByUnitId = React.useMemo(() => {
76
+ const map = new Map();
77
+ productOptions.forEach((option, index) => {
78
+ const units = optionUnitQueries[index]?.data?.data ?? [];
79
+ for (const unit of units) {
80
+ map.set(unit.id, option.id);
81
+ }
82
+ });
83
+ return map;
84
+ }, [productOptions, optionUnitQueries]);
85
+ // The slot's bound option, derived from the first availability row.
86
+ // `null` when the slot is product-level (no option_id) — that path goes
87
+ // through the product-level fallback below.
88
+ const slotOptionId = React.useMemo(() => resolveSlotOptionId(availability.data?.data ?? [], optionByUnitId, optionId ?? null), [availability.data?.data, optionByUnitId, optionId]);
71
89
  const availabilityUnitRows = React.useMemo(() => (availability.data?.data ?? []).map((unit) => ({
72
90
  ...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;
91
+ optionId: slotOptionId ?? optionId ?? null,
92
+ })), [availability.data?.data, slotOptionId, optionId]);
93
+ // Slot-bound per-unit availability stays authoritative for the slot's
94
+ // option (real-time `remaining` from active bookings). For *other*
95
+ // options the same product offers, fall back to the product-level
96
+ // option_units so the operator can still pick a DBL/TWN even when the
97
+ // slot is option-scoped to SGL. Product-level slots (no option_id) hit
98
+ // the no-slot-rows branch and use the product fallback for everything.
99
+ // See issue #960.
100
+ const units = React.useMemo(() => mergeStepperUnits(availabilityUnitRows, optionUnitRows, slotOptionId, Boolean(slotId)), [availabilityUnitRows, optionUnitRows, slotOptionId, slotId]);
81
101
  React.useEffect(() => {
82
102
  onUnitsChange?.(units);
83
103
  }, [onUnitsChange, units]);
@@ -149,6 +169,37 @@ export function OptionUnitsStepperSection({ value, onChange, productId, slotId,
149
169
  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
170
  }) })] }));
151
171
  }
172
+ /**
173
+ * Returns the `optionId` the slot is bound to, derived from the first
174
+ * slot-availability row whose `optionUnitId` we can map to a known
175
+ * product option. Falls back to the caller's `fallbackOptionId` (the
176
+ * dialog's currently-selected option) when no rows resolve — that lets
177
+ * the existing `optionId` prop drive the previous-behavior path for
178
+ * unit pickers that haven't loaded yet.
179
+ */
180
+ export function resolveSlotOptionId(slotRows, optionByUnitId, fallbackOptionId) {
181
+ for (const row of slotRows) {
182
+ const resolved = optionByUnitId.get(row.optionUnitId);
183
+ if (resolved)
184
+ return resolved;
185
+ }
186
+ return fallbackOptionId;
187
+ }
188
+ /**
189
+ * Merges slot-bound per-unit availability with the product's option-unit
190
+ * catalog. Slot rows are authoritative for the slot's option (they carry
191
+ * real-time `remaining`); product-level rows fill in the other options
192
+ * the product offers so the operator can still pick mixes the slot isn't
193
+ * explicitly tracking. When the slot is product-level (no `option_id`)
194
+ * or hasn't loaded slot rows yet, the product-level rows cover everything.
195
+ */
196
+ export function mergeStepperUnits(slotRows, productRows, slotOptionId, hasSlot) {
197
+ if (!hasSlot || slotRows.length === 0 || !slotOptionId) {
198
+ return [...productRows];
199
+ }
200
+ const otherOptionRows = productRows.filter((row) => row.optionId !== slotOptionId);
201
+ return [...slotRows, ...otherOptionRows];
202
+ }
152
203
  function isAdultUnit(unit) {
153
204
  // The seed creates ADULT / CHILD / SENIOR unit codes; the stepper
154
205
  // unit object doesn't carry the code, so fall back to name-matching
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/bookings-ui",
3
- "version": "0.52.2",
3
+ "version": "0.52.4",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -51,21 +51,21 @@
51
51
  "react-dom": "^19.0.0",
52
52
  "react-hook-form": "^7.60.0",
53
53
  "zod": "^4.3.6",
54
- "@voyantjs/availability-react": "0.52.2",
55
- "@voyantjs/bookings-react": "0.52.2",
56
- "@voyantjs/catalog": "0.52.2",
57
- "@voyantjs/catalog-react": "0.52.2",
58
- "@voyantjs/crm-react": "0.52.2",
59
- "@voyantjs/crm-ui": "0.52.2",
60
- "@voyantjs/finance-react": "0.52.2",
61
- "@voyantjs/identity-react": "0.52.2",
62
- "@voyantjs/legal-react": "0.52.2",
63
- "@voyantjs/products-react": "0.52.2",
64
- "@voyantjs/suppliers-react": "0.52.2",
65
- "@voyantjs/ui": "0.52.2"
54
+ "@voyantjs/availability-react": "0.52.4",
55
+ "@voyantjs/bookings-react": "0.52.4",
56
+ "@voyantjs/catalog": "0.52.4",
57
+ "@voyantjs/catalog-react": "0.52.4",
58
+ "@voyantjs/crm-react": "0.52.4",
59
+ "@voyantjs/crm-ui": "0.52.4",
60
+ "@voyantjs/finance-react": "0.52.4",
61
+ "@voyantjs/identity-react": "0.52.4",
62
+ "@voyantjs/legal-react": "0.52.4",
63
+ "@voyantjs/products-react": "0.52.4",
64
+ "@voyantjs/suppliers-react": "0.52.4",
65
+ "@voyantjs/ui": "0.52.4"
66
66
  },
67
67
  "dependencies": {
68
- "@voyantjs/i18n": "0.52.2"
68
+ "@voyantjs/i18n": "0.52.4"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@tanstack/react-query": "^5.96.2",
@@ -78,19 +78,19 @@
78
78
  "typescript": "^6.0.2",
79
79
  "vitest": "^4.1.2",
80
80
  "zod": "^4.3.6",
81
- "@voyantjs/availability-react": "0.52.2",
82
- "@voyantjs/bookings-react": "0.52.2",
83
- "@voyantjs/catalog": "0.52.2",
84
- "@voyantjs/catalog-react": "0.52.2",
85
- "@voyantjs/crm-react": "0.52.2",
86
- "@voyantjs/crm-ui": "0.52.2",
87
- "@voyantjs/finance-react": "0.52.2",
88
- "@voyantjs/identity-react": "0.52.2",
89
- "@voyantjs/legal-react": "0.52.2",
90
- "@voyantjs/products-react": "0.52.2",
91
- "@voyantjs/suppliers-react": "0.52.2",
81
+ "@voyantjs/availability-react": "0.52.4",
82
+ "@voyantjs/bookings-react": "0.52.4",
83
+ "@voyantjs/catalog": "0.52.4",
84
+ "@voyantjs/catalog-react": "0.52.4",
85
+ "@voyantjs/crm-react": "0.52.4",
86
+ "@voyantjs/crm-ui": "0.52.4",
87
+ "@voyantjs/finance-react": "0.52.4",
88
+ "@voyantjs/identity-react": "0.52.4",
89
+ "@voyantjs/legal-react": "0.52.4",
90
+ "@voyantjs/products-react": "0.52.4",
91
+ "@voyantjs/suppliers-react": "0.52.4",
92
92
  "@voyantjs/voyant-typescript-config": "0.1.0",
93
- "@voyantjs/ui": "0.52.2"
93
+ "@voyantjs/ui": "0.52.4"
94
94
  },
95
95
  "files": [
96
96
  "dist",