@aglyn/plugins-commerce 1.0.0-beta.291 → 1.0.0-beta.293

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 (115) hide show
  1. package/README.md +1 -0
  2. package/package.json +13 -13
  3. package/src/lib/components/cart-fulfillment.d.ts +3 -0
  4. package/src/lib/components/cart-fulfillment.js.map +1 -1
  5. package/src/lib/components/cart.js +14 -4
  6. package/src/lib/components/cart.js.map +1 -1
  7. package/src/lib/components/console/host-quotes-card.component.js +11 -9
  8. package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
  9. package/src/lib/components/console/invoice-send.component.js +1 -1
  10. package/src/lib/components/console/invoice-send.component.js.map +1 -1
  11. package/src/lib/components/console/pos/pos-item-dialog.component.js +3 -2
  12. package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
  13. package/src/lib/components/console/pos-kiosk-page.component.js +2 -1
  14. package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -1
  15. package/src/lib/components/console/pos-page.component.js +1 -1
  16. package/src/lib/components/console/pos-page.component.js.map +1 -1
  17. package/src/lib/components/console/product-add-ons-fields.component.d.ts +41 -0
  18. package/src/lib/components/console/product-add-ons-fields.component.js +356 -0
  19. package/src/lib/components/console/product-add-ons-fields.component.js.map +1 -0
  20. package/src/lib/components/console/product-editor-dialog.component.js +31 -5
  21. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  22. package/src/lib/components/console/product-register-fields.component.d.ts +4 -5
  23. package/src/lib/components/console/product-register-fields.component.js +8 -197
  24. package/src/lib/components/console/product-register-fields.component.js.map +1 -1
  25. package/src/lib/components/console/products-hub-zone.component.js +6 -0
  26. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  27. package/src/lib/components/console/quote-pay-link.component.d.ts +8 -1
  28. package/src/lib/components/console/quote-pay-link.component.js +29 -3
  29. package/src/lib/components/console/quote-pay-link.component.js.map +1 -1
  30. package/src/lib/components/console/quote-send.component.js +1 -4
  31. package/src/lib/components/console/quote-send.component.js.map +1 -1
  32. package/src/lib/components/product-add-ons.d.ts +28 -0
  33. package/src/lib/components/product-add-ons.js +136 -0
  34. package/src/lib/components/product-add-ons.js.map +1 -0
  35. package/src/lib/components/product-detail.d.ts +16 -0
  36. package/src/lib/components/product-detail.js +112 -19
  37. package/src/lib/components/product-detail.js.map +1 -1
  38. package/src/lib/components/product-grid.d.ts +8 -2
  39. package/src/lib/components/product-grid.js +6 -1
  40. package/src/lib/components/product-grid.js.map +1 -1
  41. package/src/lib/declarations.console-server.js +15 -0
  42. package/src/lib/declarations.console-server.js.map +1 -1
  43. package/src/lib/model/commerce-cart.d.ts +15 -3
  44. package/src/lib/model/commerce-cart.js +10 -3
  45. package/src/lib/model/commerce-cart.js.map +1 -1
  46. package/src/lib/model/commerce-invoices.d.ts +7 -0
  47. package/src/lib/model/commerce-invoices.js +11 -0
  48. package/src/lib/model/commerce-invoices.js.map +1 -1
  49. package/src/lib/model/commerce-orders.d.ts +6 -3
  50. package/src/lib/model/commerce-orders.js +3 -0
  51. package/src/lib/model/commerce-orders.js.map +1 -1
  52. package/src/lib/model/commerce-pos-kiosk.d.ts +1 -1
  53. package/src/lib/model/commerce-pos-kiosk.js +3 -2
  54. package/src/lib/model/commerce-pos-kiosk.js.map +1 -1
  55. package/src/lib/model/commerce-quote-email.d.ts +14 -0
  56. package/src/lib/model/commerce-quote-email.js +25 -9
  57. package/src/lib/model/commerce-quote-email.js.map +1 -1
  58. package/src/lib/model/commerce-quote-proofs.d.ts +7 -0
  59. package/src/lib/model/commerce-quote-proofs.js +15 -0
  60. package/src/lib/model/commerce-quote-proofs.js.map +1 -1
  61. package/src/lib/model/commerce-quotes.js +1 -1
  62. package/src/lib/model/commerce-quotes.js.map +1 -1
  63. package/src/lib/model/commerce.d.ts +18 -0
  64. package/src/lib/model/commerce.js +26 -0
  65. package/src/lib/model/commerce.js.map +1 -1
  66. package/src/lib/model/product-modifiers.d.ts +74 -4
  67. package/src/lib/model/product-modifiers.js +131 -10
  68. package/src/lib/model/product-modifiers.js.map +1 -1
  69. package/src/lib/server/billing-webhook.js +64 -43
  70. package/src/lib/server/billing-webhook.js.map +1 -1
  71. package/src/lib/server/cart-checkout.js +14 -2
  72. package/src/lib/server/cart-checkout.js.map +1 -1
  73. package/src/lib/server/cart.d.ts +5 -0
  74. package/src/lib/server/cart.js +41 -4
  75. package/src/lib/server/cart.js.map +1 -1
  76. package/src/lib/server/catalog.d.ts +6 -0
  77. package/src/lib/server/catalog.js +2 -0
  78. package/src/lib/server/catalog.js.map +1 -1
  79. package/src/lib/server/checkout-extras.js +3 -1
  80. package/src/lib/server/checkout-extras.js.map +1 -1
  81. package/src/lib/server/checkout.js +28 -3
  82. package/src/lib/server/checkout.js.map +1 -1
  83. package/src/lib/server/local-fulfillment.js +5 -1
  84. package/src/lib/server/local-fulfillment.js.map +1 -1
  85. package/src/lib/server/pos-offline-sync.js +1 -1
  86. package/src/lib/server/pos-offline-sync.js.map +1 -1
  87. package/src/lib/server/pos-order.js +1 -1
  88. package/src/lib/server/pos-order.js.map +1 -1
  89. package/src/lib/server/product.d.ts +26 -0
  90. package/src/lib/server/product.js +18 -0
  91. package/src/lib/server/product.js.map +1 -1
  92. package/src/lib/server/products-write.js +5 -2
  93. package/src/lib/server/products-write.js.map +1 -1
  94. package/src/lib/server/quote-email.js.map +1 -1
  95. package/src/lib/server/quote-link-token.d.ts +4 -2
  96. package/src/lib/server/quote-link-token.js +4 -2
  97. package/src/lib/server/quote-link-token.js.map +1 -1
  98. package/src/lib/server/quote-pay.d.ts +5 -1
  99. package/src/lib/server/quote-pay.js +5 -2
  100. package/src/lib/server/quote-pay.js.map +1 -1
  101. package/src/lib/server/quote-proofs.d.ts +9 -1
  102. package/src/lib/server/quote-proofs.js +92 -23
  103. package/src/lib/server/quote-proofs.js.map +1 -1
  104. package/src/lib/server/quote-upload-keys.d.ts +50 -0
  105. package/src/lib/server/quote-upload-keys.js +86 -0
  106. package/src/lib/server/quote-upload-keys.js.map +1 -0
  107. package/src/lib/server/quote-upload-sweep.d.ts +30 -0
  108. package/src/lib/server/quote-upload-sweep.js +117 -0
  109. package/src/lib/server/quote-upload-sweep.js.map +1 -0
  110. package/src/lib/utils/add-to-cart.d.ts +8 -0
  111. package/src/lib/utils/add-to-cart.js +10 -2
  112. package/src/lib/utils/add-to-cart.js.map +1 -1
  113. package/src/lib/utils/quote-email-content.d.ts +1 -0
  114. package/src/lib/utils/quote-email-content.js +7 -4
  115. package/src/lib/utils/quote-email-content.js.map +1 -1
@@ -0,0 +1,356 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
19
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
+ import { Box, Button, Collapse, FormControlLabel, IconButton, Stack, Switch, TextField, Typography } from "@mui/material";
21
+ import { memo, useCallback, useId, useState } from "react";
22
+ import * as CommerceModel from "../../model/index.js";
23
+ function newId(prefix) {
24
+ return `${prefix}${Math.random().toString(36).slice(2, 10)}`;
25
+ }
26
+ /** Dollars as typed, to whole cents; blank is free. */ function centsFromDollars(value) {
27
+ const number = Math.round(Number(String(value).replace(/[^0-9.]/g, '')) * 100);
28
+ return Number.isFinite(number) && number > 0 ? number : 0;
29
+ }
30
+ /** Cents as the price field shows them; blank for free. */ function dollarsFromCents(cents) {
31
+ return cents ? (cents / 100).toFixed(2) : '';
32
+ }
33
+ /**
34
+ * What a choice's per-variant prices are called (AGL-4100): by the one
35
+ * option a product varies by ("Price by size"), else by variant.
36
+ */ export function priceByLabel(options) {
37
+ const named = options.filter((option)=>{
38
+ var _option_name;
39
+ return (_option_name = option.name) == null ? void 0 : _option_name.trim();
40
+ });
41
+ return named.length === 1 ? `Price by ${named[0].name.trim().toLowerCase()}` : 'Price by variant';
42
+ }
43
+ /** How a variant is named in a choice's price list. */ export function variantPriceLabel(variant) {
44
+ var _variant_options;
45
+ const values = Object.values((_variant_options = variant.options) != null ? _variant_options : {}).filter(Boolean);
46
+ return values.length ? values.join(' / ') : 'Default';
47
+ }
48
+ /** A new group, as the console makes it from now on: shown on the website (AGL-4100). */ export function newAddOnGroup() {
49
+ return {
50
+ id: newId('g'),
51
+ name: '',
52
+ min: 0,
53
+ max: 1,
54
+ online: true,
55
+ options: [
56
+ {
57
+ id: newId('o'),
58
+ name: '',
59
+ priceCents: 0
60
+ }
61
+ ]
62
+ };
63
+ }
64
+ /**
65
+ * A choice's prices by variant with one variant's price set or cleared. A
66
+ * cleared price follows the choice's own; with none left the field is
67
+ * dropped, never stored as `undefined` (the save is a client-direct write,
68
+ * which refuses one).
69
+ */ export function withVariantPrice(option, variantId, raw) {
70
+ const { priceCentsByVariant } = option, rest = _object_without_properties_loose(option, [
71
+ "priceCentsByVariant"
72
+ ]);
73
+ const next = _extends({}, priceCentsByVariant != null ? priceCentsByVariant : {});
74
+ if (raw.trim() === '') delete next[variantId];
75
+ else next[variantId] = centsFromDollars(raw);
76
+ return Object.keys(next).length ? _extends({}, rest, {
77
+ priceCentsByVariant: next
78
+ }) : rest;
79
+ }
80
+ /**
81
+ * The product's add-ons (AGL-4100, from the register's modifiers of
82
+ * AGL-3607): choices added to the item that are not stocked variants — walls
83
+ * and printing on a canopy, a milk in a latte — each free or adding to the
84
+ * price, and able to cost more on one variant than another. A group with a
85
+ * minimum of 1 or more must be answered before the item is added. Each group
86
+ * says whether the website's product page offers it; the register always
87
+ * does.
88
+ *
89
+ * Offered on every store, with or without the point of sale, and saved with
90
+ * the product, so editing it takes exactly what editing the product takes.
91
+ * Memoized on its own props like every section of this editor (AGL-3423).
92
+ */ export const ProductAddOnsFields = /*#__PURE__*/ memo(function ProductAddOnsFields(props) {
93
+ const { modifierGroups, options, variants, update } = props;
94
+ const helpId = useId();
95
+ const [pricedByVariant, setPricedByVariant] = useState({});
96
+ const setGroups = useCallback((groups)=>update({
97
+ modifierGroups: groups
98
+ }), [
99
+ update
100
+ ]);
101
+ const editGroup = (index, patch)=>setGroups(modifierGroups.map((group, at)=>at === index ? _extends({}, group, patch) : group));
102
+ const replaceOption = (groupIndex, optionIndex, next)=>{
103
+ const group = modifierGroups[groupIndex];
104
+ editGroup(groupIndex, {
105
+ options: group.options.map((option, at)=>at === optionIndex ? next : option)
106
+ });
107
+ };
108
+ const editOption = (groupIndex, optionIndex, patch)=>replaceOption(groupIndex, optionIndex, _extends({}, modifierGroups[groupIndex].options[optionIndex], patch));
109
+ const problem = CommerceModel.modifierGroupsProblem(modifierGroups);
110
+ const variesByVariant = variants.length > 1;
111
+ const byLabel = priceByLabel(options);
112
+ return /*#__PURE__*/ _jsxs(Stack, {
113
+ spacing: 2,
114
+ children: [
115
+ /*#__PURE__*/ _jsx(Typography, {
116
+ id: helpId,
117
+ variant: "body2",
118
+ color: "text.secondary",
119
+ children: 'Add-ons are choices added to the item that don’t need their own stock, such as walls or ' + 'printing on a canopy, or a milk in a latte. Each can be free or add to the price, and can ' + 'cost more on a larger size. Show a group on the website to offer it on the product’s page; ' + 'the register offers every group.'
120
+ }),
121
+ modifierGroups.map((group, groupIndex)=>/*#__PURE__*/ _jsx(Box, {
122
+ role: "group",
123
+ "aria-label": group.name || 'Add-on group',
124
+ sx: {
125
+ border: 1,
126
+ borderColor: 'divider',
127
+ borderRadius: 1,
128
+ p: 2
129
+ },
130
+ children: /*#__PURE__*/ _jsxs(Stack, {
131
+ spacing: 1.5,
132
+ children: [
133
+ /*#__PURE__*/ _jsxs(Stack, {
134
+ direction: "row",
135
+ spacing: 1,
136
+ sx: {
137
+ alignItems: 'flex-start',
138
+ flexWrap: 'wrap',
139
+ rowGap: 1
140
+ },
141
+ children: [
142
+ /*#__PURE__*/ _jsx(TextField, {
143
+ label: "Group name",
144
+ size: "small",
145
+ value: group.name,
146
+ onChange: (event)=>editGroup(groupIndex, {
147
+ name: event.target.value
148
+ }),
149
+ sx: {
150
+ flex: 1,
151
+ minWidth: (theme)=>theme.spacing(20)
152
+ }
153
+ }),
154
+ /*#__PURE__*/ _jsx(TextField, {
155
+ label: "At least",
156
+ size: "small",
157
+ value: group.min,
158
+ onChange: (event)=>editGroup(groupIndex, {
159
+ min: Math.max(0, Math.round(Number(event.target.value) || 0))
160
+ }),
161
+ helperText: group.min > 0 ? 'Required' : 'Optional',
162
+ sx: {
163
+ width: (theme)=>theme.spacing(12)
164
+ },
165
+ slotProps: {
166
+ htmlInput: {
167
+ inputMode: 'numeric'
168
+ }
169
+ }
170
+ }),
171
+ /*#__PURE__*/ _jsx(TextField, {
172
+ label: "At most",
173
+ size: "small",
174
+ value: group.max,
175
+ onChange: (event)=>editGroup(groupIndex, {
176
+ max: Math.max(1, Math.round(Number(event.target.value) || 1))
177
+ }),
178
+ sx: {
179
+ width: (theme)=>theme.spacing(12)
180
+ },
181
+ slotProps: {
182
+ htmlInput: {
183
+ inputMode: 'numeric'
184
+ }
185
+ }
186
+ }),
187
+ /*#__PURE__*/ _jsx(Button, {
188
+ color: "error",
189
+ onClick: ()=>setGroups(modifierGroups.filter((_group, at)=>at !== groupIndex)),
190
+ children: 'Remove group'
191
+ })
192
+ ]
193
+ }),
194
+ /*#__PURE__*/ _jsx(FormControlLabel, {
195
+ control: /*#__PURE__*/ _jsx(Switch, {
196
+ checked: group.online === true,
197
+ onChange: (event)=>editGroup(groupIndex, {
198
+ online: event.target.checked
199
+ })
200
+ }),
201
+ label: "Show on the website"
202
+ }),
203
+ /*#__PURE__*/ _jsx(Typography, {
204
+ variant: "caption",
205
+ color: "text.secondary",
206
+ children: group.online === true ? 'Offered on the product’s page and at the register.' : 'Offered at the register only.'
207
+ }),
208
+ group.options.map((option, optionIndex)=>{
209
+ var _pricedByVariant_optionKey, _option_priceCentsByVariant;
210
+ const optionKey = `${group.id}:${option.id}`;
211
+ const byVariantOpen = (_pricedByVariant_optionKey = pricedByVariant[optionKey]) != null ? _pricedByVariant_optionKey : Object.keys((_option_priceCentsByVariant = option.priceCentsByVariant) != null ? _option_priceCentsByVariant : {}).length > 0;
212
+ return /*#__PURE__*/ _jsxs(Box, {
213
+ children: [
214
+ /*#__PURE__*/ _jsxs(Stack, {
215
+ direction: "row",
216
+ spacing: 1,
217
+ sx: {
218
+ alignItems: 'center',
219
+ flexWrap: 'wrap',
220
+ rowGap: 1
221
+ },
222
+ children: [
223
+ /*#__PURE__*/ _jsx(TextField, {
224
+ label: "Choice",
225
+ size: "small",
226
+ value: option.name,
227
+ onChange: (event)=>editOption(groupIndex, optionIndex, {
228
+ name: event.target.value
229
+ }),
230
+ sx: {
231
+ flex: 1,
232
+ minWidth: (theme)=>theme.spacing(20)
233
+ }
234
+ }),
235
+ /*#__PURE__*/ _jsx(TextField, {
236
+ label: "Adds ($)",
237
+ size: "small",
238
+ defaultValue: dollarsFromCents(option.priceCents),
239
+ placeholder: "0.00",
240
+ onBlur: (event)=>editOption(groupIndex, optionIndex, {
241
+ priceCents: centsFromDollars(event.target.value)
242
+ }),
243
+ sx: {
244
+ width: (theme)=>theme.spacing(15)
245
+ },
246
+ slotProps: {
247
+ htmlInput: {
248
+ inputMode: 'decimal'
249
+ }
250
+ }
251
+ }),
252
+ variesByVariant ? /*#__PURE__*/ _jsx(Button, {
253
+ size: "small",
254
+ "aria-expanded": byVariantOpen,
255
+ onClick: ()=>setPricedByVariant((open)=>_extends({}, open, {
256
+ [optionKey]: !byVariantOpen
257
+ })),
258
+ children: byLabel
259
+ }) : null,
260
+ /*#__PURE__*/ _jsx(IconButton, {
261
+ "aria-label": `Remove ${option.name || 'choice'}`,
262
+ onClick: ()=>editGroup(groupIndex, {
263
+ options: group.options.filter((_option, at)=>at !== optionIndex),
264
+ max: Math.max(1, Math.min(group.max, group.options.length - 1))
265
+ }),
266
+ children: '✕'
267
+ })
268
+ ]
269
+ }),
270
+ variesByVariant ? /*#__PURE__*/ _jsx(Collapse, {
271
+ in: byVariantOpen,
272
+ unmountOnExit: true,
273
+ children: /*#__PURE__*/ _jsxs(Stack, {
274
+ spacing: 1,
275
+ sx: {
276
+ pl: 2,
277
+ pt: 1.5
278
+ },
279
+ children: [
280
+ /*#__PURE__*/ _jsx(Typography, {
281
+ variant: "caption",
282
+ color: "text.secondary",
283
+ children: `What “${option.name || 'this choice'}” adds on each. Leave one blank to use ` + 'the price above.'
284
+ }),
285
+ /*#__PURE__*/ _jsx(Box, {
286
+ sx: {
287
+ display: 'grid',
288
+ gridTemplateColumns: {
289
+ xs: '1fr',
290
+ sm: 'repeat(2, minmax(0, 1fr))'
291
+ },
292
+ gap: 1
293
+ },
294
+ children: variants.map((variant)=>{
295
+ var _option_priceCentsByVariant;
296
+ return /*#__PURE__*/ _jsx(TextField, {
297
+ label: variantPriceLabel(variant),
298
+ size: "small",
299
+ defaultValue: dollarsFromCents((_option_priceCentsByVariant = option.priceCentsByVariant) == null ? void 0 : _option_priceCentsByVariant[variant.id]),
300
+ placeholder: dollarsFromCents(option.priceCents) || '0.00',
301
+ onBlur: (event)=>replaceOption(groupIndex, optionIndex, withVariantPrice(modifierGroups[groupIndex].options[optionIndex], variant.id, event.target.value)),
302
+ slotProps: {
303
+ htmlInput: {
304
+ inputMode: 'decimal'
305
+ }
306
+ }
307
+ }, variant.id);
308
+ })
309
+ })
310
+ ]
311
+ })
312
+ }) : null
313
+ ]
314
+ }, option.id);
315
+ }),
316
+ /*#__PURE__*/ _jsx(Box, {
317
+ children: /*#__PURE__*/ _jsx(Button, {
318
+ onClick: ()=>editGroup(groupIndex, {
319
+ options: [
320
+ ...group.options,
321
+ {
322
+ id: newId('o'),
323
+ name: '',
324
+ priceCents: 0
325
+ }
326
+ ]
327
+ }),
328
+ disabled: group.options.length >= CommerceModel.POS_MAX_MODIFIER_OPTIONS,
329
+ children: 'Add choice'
330
+ })
331
+ })
332
+ ]
333
+ })
334
+ }, group.id)),
335
+ /*#__PURE__*/ _jsx(Box, {
336
+ children: /*#__PURE__*/ _jsx(Button, {
337
+ variant: "outlined",
338
+ "aria-describedby": helpId,
339
+ onClick: ()=>setGroups([
340
+ ...modifierGroups,
341
+ newAddOnGroup()
342
+ ]),
343
+ disabled: modifierGroups.length >= CommerceModel.POS_MAX_MODIFIER_GROUPS,
344
+ children: 'New add-on group'
345
+ })
346
+ }),
347
+ problem && modifierGroups.length ? /*#__PURE__*/ _jsx(Typography, {
348
+ variant: "body2",
349
+ color: "warning.main",
350
+ children: problem
351
+ }) : null
352
+ ]
353
+ });
354
+ });
355
+
356
+ //# sourceMappingURL=product-add-ons-fields.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/product-add-ons-fields.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n Box,\n Button,\n Collapse,\n FormControlLabel,\n IconButton,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { memo, useCallback, useId, useState } from 'react'\nimport * as CommerceModel from '../../model'\n\ntype Patch = Partial<CommerceModel.HostProduct>\n\nexport interface ProductAddOnsFieldsProps {\n modifierGroups: readonly CommerceModel.ProductModifierGroup[]\n /** The product's options, to name what a choice is priced by. */\n options: readonly CommerceModel.ProductOption[]\n /** The product's variants, each of which a choice may be priced on. */\n variants: readonly Pick<CommerceModel.ProductVariant, 'id' | 'options'>[]\n update: (patch: Patch) => void\n}\n\nfunction newId(prefix: string): string {\n return `${prefix}${Math.random().toString(36).slice(2, 10)}`\n}\n\n/** Dollars as typed, to whole cents; blank is free. */\nfunction centsFromDollars(value: string): number {\n const number = Math.round(Number(String(value).replace(/[^0-9.]/g, '')) * 100)\n return Number.isFinite(number) && number > 0 ? number : 0\n}\n\n/** Cents as the price field shows them; blank for free. */\nfunction dollarsFromCents(cents: number | undefined): string {\n return cents ? (cents / 100).toFixed(2) : ''\n}\n\n/**\n * What a choice's per-variant prices are called (AGL-4100): by the one\n * option a product varies by (\"Price by size\"), else by variant.\n */\nexport function priceByLabel(options: readonly Pick<CommerceModel.ProductOption, 'name'>[]): string {\n const named = options.filter((option) => option.name?.trim())\n return named.length === 1 ? `Price by ${named[0].name.trim().toLowerCase()}` : 'Price by variant'\n}\n\n/** How a variant is named in a choice's price list. */\nexport function variantPriceLabel(variant: Pick<CommerceModel.ProductVariant, 'options'>): string {\n const values = Object.values(variant.options ?? {}).filter(Boolean)\n return values.length ? values.join(' / ') : 'Default'\n}\n\n/** A new group, as the console makes it from now on: shown on the website (AGL-4100). */\nexport function newAddOnGroup(): CommerceModel.ProductModifierGroup {\n return {\n id: newId('g'),\n name: '',\n min: 0,\n max: 1,\n online: true,\n options: [{ id: newId('o'), name: '', priceCents: 0 }],\n }\n}\n\n/**\n * A choice's prices by variant with one variant's price set or cleared. A\n * cleared price follows the choice's own; with none left the field is\n * dropped, never stored as `undefined` (the save is a client-direct write,\n * which refuses one).\n */\nexport function withVariantPrice(\n option: CommerceModel.ProductModifierOption,\n variantId: string,\n raw: string,\n): CommerceModel.ProductModifierOption {\n const { priceCentsByVariant, ...rest } = option\n const next = { ...(priceCentsByVariant ?? {}) }\n if (raw.trim() === '') delete next[variantId]\n else next[variantId] = centsFromDollars(raw)\n return Object.keys(next).length ? { ...rest, priceCentsByVariant: next } : rest\n}\n\n/**\n * The product's add-ons (AGL-4100, from the register's modifiers of\n * AGL-3607): choices added to the item that are not stocked variants — walls\n * and printing on a canopy, a milk in a latte — each free or adding to the\n * price, and able to cost more on one variant than another. A group with a\n * minimum of 1 or more must be answered before the item is added. Each group\n * says whether the website's product page offers it; the register always\n * does.\n *\n * Offered on every store, with or without the point of sale, and saved with\n * the product, so editing it takes exactly what editing the product takes.\n * Memoized on its own props like every section of this editor (AGL-3423).\n */\nexport const ProductAddOnsFields = memo(function ProductAddOnsFields(\n props: ProductAddOnsFieldsProps,\n) {\n const { modifierGroups, options, variants, update } = props\n const helpId = useId()\n const [pricedByVariant, setPricedByVariant] = useState<Record<string, boolean>>({})\n const setGroups = useCallback(\n (groups: CommerceModel.ProductModifierGroup[]) => update({ modifierGroups: groups }),\n [update],\n )\n const editGroup = (index: number, patch: Partial<CommerceModel.ProductModifierGroup>) =>\n setGroups(modifierGroups.map((group, at) => (at === index ? { ...group, ...patch } : group)))\n const replaceOption = (\n groupIndex: number,\n optionIndex: number,\n next: CommerceModel.ProductModifierOption,\n ) => {\n const group = modifierGroups[groupIndex]\n editGroup(groupIndex, {\n options: group.options.map((option, at) => (at === optionIndex ? next : option)),\n })\n }\n const editOption = (\n groupIndex: number,\n optionIndex: number,\n patch: Partial<CommerceModel.ProductModifierOption>,\n ) => replaceOption(groupIndex, optionIndex, { ...modifierGroups[groupIndex].options[optionIndex], ...patch })\n const problem = CommerceModel.modifierGroupsProblem(modifierGroups)\n const variesByVariant = variants.length > 1\n const byLabel = priceByLabel(options)\n\n return (\n <Stack spacing={2}>\n <Typography id={helpId} variant=\"body2\" color=\"text.secondary\">\n {'Add-ons are choices added to the item that don’t need their own stock, such as walls or ' +\n 'printing on a canopy, or a milk in a latte. Each can be free or add to the price, and can ' +\n 'cost more on a larger size. Show a group on the website to offer it on the product’s page; ' +\n 'the register offers every group.'}\n </Typography>\n {modifierGroups.map((group, groupIndex) => (\n <Box\n key={group.id}\n role=\"group\"\n aria-label={group.name || 'Add-on group'}\n sx={{ border: 1, borderColor: 'divider', borderRadius: 1, p: 2 }}\n >\n <Stack spacing={1.5}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start', flexWrap: 'wrap', rowGap: 1 }}>\n <TextField\n label=\"Group name\"\n size=\"small\"\n value={group.name}\n onChange={(event) => editGroup(groupIndex, { name: event.target.value })}\n sx={{ flex: 1, minWidth: (theme) => theme.spacing(20) }}\n />\n <TextField\n label=\"At least\"\n size=\"small\"\n value={group.min}\n onChange={(event) =>\n editGroup(groupIndex, { min: Math.max(0, Math.round(Number(event.target.value) || 0)) })\n }\n helperText={group.min > 0 ? 'Required' : 'Optional'}\n sx={{ width: (theme) => theme.spacing(12) }}\n slotProps={{ htmlInput: { inputMode: 'numeric' } }}\n />\n <TextField\n label=\"At most\"\n size=\"small\"\n value={group.max}\n onChange={(event) =>\n editGroup(groupIndex, { max: Math.max(1, Math.round(Number(event.target.value) || 1)) })\n }\n sx={{ width: (theme) => theme.spacing(12) }}\n slotProps={{ htmlInput: { inputMode: 'numeric' } }}\n />\n <Button\n color=\"error\"\n onClick={() => setGroups(modifierGroups.filter((_group, at) => at !== groupIndex))}\n >\n {'Remove group'}\n </Button>\n </Stack>\n <FormControlLabel\n control={\n <Switch\n checked={group.online === true}\n onChange={(event) => editGroup(groupIndex, { online: event.target.checked })}\n />\n }\n label=\"Show on the website\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {group.online === true\n ? 'Offered on the product’s page and at the register.'\n : 'Offered at the register only.'}\n </Typography>\n {group.options.map((option, optionIndex) => {\n const optionKey = `${group.id}:${option.id}`\n const byVariantOpen =\n pricedByVariant[optionKey] ?? Object.keys(option.priceCentsByVariant ?? {}).length > 0\n return (\n <Box key={option.id}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}>\n <TextField\n label=\"Choice\"\n size=\"small\"\n value={option.name}\n onChange={(event) => editOption(groupIndex, optionIndex, { name: event.target.value })}\n sx={{ flex: 1, minWidth: (theme) => theme.spacing(20) }}\n />\n <TextField\n label=\"Adds ($)\"\n size=\"small\"\n defaultValue={dollarsFromCents(option.priceCents)}\n placeholder=\"0.00\"\n onBlur={(event) =>\n editOption(groupIndex, optionIndex, { priceCents: centsFromDollars(event.target.value) })\n }\n sx={{ width: (theme) => theme.spacing(15) }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n {variesByVariant ? (\n <Button\n size=\"small\"\n aria-expanded={byVariantOpen}\n onClick={() =>\n setPricedByVariant((open) => ({ ...open, [optionKey]: !byVariantOpen }))\n }\n >\n {byLabel}\n </Button>\n ) : null}\n <IconButton\n aria-label={`Remove ${option.name || 'choice'}`}\n onClick={() =>\n editGroup(groupIndex, {\n options: group.options.filter((_option, at) => at !== optionIndex),\n max: Math.max(1, Math.min(group.max, group.options.length - 1)),\n })\n }\n >\n {'✕'}\n </IconButton>\n </Stack>\n {variesByVariant ? (\n <Collapse in={byVariantOpen} unmountOnExit>\n <Stack spacing={1} sx={{ pl: 2, pt: 1.5 }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`What “${option.name || 'this choice'}” adds on each. Leave one blank to use ` +\n 'the price above.'}\n </Typography>\n <Box\n sx={{\n display: 'grid',\n gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, minmax(0, 1fr))' },\n gap: 1,\n }}\n >\n {variants.map((variant) => (\n <TextField\n key={variant.id}\n label={variantPriceLabel(variant)}\n size=\"small\"\n defaultValue={dollarsFromCents(option.priceCentsByVariant?.[variant.id])}\n placeholder={dollarsFromCents(option.priceCents) || '0.00'}\n onBlur={(event) =>\n replaceOption(\n groupIndex,\n optionIndex,\n withVariantPrice(\n modifierGroups[groupIndex].options[optionIndex],\n variant.id,\n event.target.value,\n ),\n )\n }\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n ))}\n </Box>\n </Stack>\n </Collapse>\n ) : null}\n </Box>\n )\n })}\n <Box>\n <Button\n onClick={() =>\n editGroup(groupIndex, {\n options: [...group.options, { id: newId('o'), name: '', priceCents: 0 }],\n })\n }\n disabled={group.options.length >= CommerceModel.POS_MAX_MODIFIER_OPTIONS}\n >\n {'Add choice'}\n </Button>\n </Box>\n </Stack>\n </Box>\n ))}\n <Box>\n <Button\n variant=\"outlined\"\n aria-describedby={helpId}\n onClick={() => setGroups([...modifierGroups, newAddOnGroup()])}\n disabled={modifierGroups.length >= CommerceModel.POS_MAX_MODIFIER_GROUPS}\n >\n {'New add-on group'}\n </Button>\n </Box>\n {problem && modifierGroups.length ? (\n <Typography variant=\"body2\" color=\"warning.main\">\n {problem}\n </Typography>\n ) : null}\n </Stack>\n )\n})\n"],"names":["Box","Button","Collapse","FormControlLabel","IconButton","Stack","Switch","TextField","Typography","memo","useCallback","useId","useState","CommerceModel","newId","prefix","Math","random","toString","slice","centsFromDollars","value","number","round","Number","String","replace","isFinite","dollarsFromCents","cents","toFixed","priceByLabel","options","named","filter","option","name","trim","length","toLowerCase","variantPriceLabel","variant","values","Object","Boolean","join","newAddOnGroup","id","min","max","online","priceCents","withVariantPrice","variantId","raw","priceCentsByVariant","rest","next","keys","ProductAddOnsFields","props","modifierGroups","variants","update","helpId","pricedByVariant","setPricedByVariant","setGroups","groups","editGroup","index","patch","map","group","at","replaceOption","groupIndex","optionIndex","editOption","problem","modifierGroupsProblem","variesByVariant","byLabel","spacing","color","role","aria-label","sx","border","borderColor","borderRadius","p","direction","alignItems","flexWrap","rowGap","label","size","onChange","event","target","flex","minWidth","theme","helperText","width","slotProps","htmlInput","inputMode","onClick","_group","control","checked","optionKey","byVariantOpen","defaultValue","placeholder","onBlur","aria-expanded","open","_option","in","unmountOnExit","pl","pt","display","gridTemplateColumns","xs","sm","gap","disabled","POS_MAX_MODIFIER_OPTIONS","aria-describedby","POS_MAX_MODIFIER_GROUPS"],"mappings":"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"}
@@ -38,6 +38,7 @@ import { PRODUCT_EDITOR_ZONE } from "./product-zones.js";
38
38
  import { MembersVideosField, PaidDownloadAddButton, PaidMediaProtection } from "./paid-media.js";
39
39
  import { ProductCheckInFields } from "./product-check-in-fields.component.js";
40
40
  import { ProductRegisterFields, ProductSellOnlineField } from "./product-register-fields.component.js";
41
+ import { ProductAddOnsFields } from "./product-add-ons-fields.component.js";
41
42
  import { ProductKioskOfferField } from "./product-kiosk-offer-field.component.js";
42
43
  /**
43
44
  * The platform's own store's "Point of sale hardware" choice (AGL-4038),
@@ -1103,7 +1104,7 @@ const MemoPaidDownloadAddButton = /*#__PURE__*/ memo(PaidDownloadAddButton);
1103
1104
  * first variant so the legacy Product block + checkout API (AGL-90) keep
1104
1105
  * charging correctly without reading the variants array.
1105
1106
  */ export function ProductEditorDialog(props) {
1106
- var _ref, _ref1, _current_description, _current_tags, _current_categoryIds, _current_options, _current_mediaUrls, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, _ref10, _ref11, _ref12, _ref13, _ref14, _current_description1, _ref15, _ref16, _current_description2, _current_tags1, _current_mediaUrls1, _current_options1, _subs_planLabel, _gifts_planLabel, _subs_planLabel1, _gifts_planLabel1, _current_oversellPolicy, _current_modifierGroups, _current_digitalFiles, _current_gatedVideos;
1107
+ var _ref, _ref1, _current_description, _current_tags, _current_categoryIds, _current_options, _current_mediaUrls, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, _ref10, _ref11, _ref12, _ref13, _ref14, _current_description1, _ref15, _ref16, _current_description2, _current_tags1, _current_mediaUrls1, _current_options1, _subs_planLabel, _gifts_planLabel, _subs_planLabel1, _gifts_planLabel1, _current_oversellPolicy, _current_modifierGroups, _current_options2, _current_digitalFiles, _current_gatedVideos;
1107
1108
  var _current_seo, _current_seo1, _current_shipping, _current_shipping1, _current_shipping2, _current_shipping3, _current_shipping4, _current_channel, _current_channel1, _current_channel2, _current_channel3, _current_channel4, _current_seo2, _current_seo3, _current_seo4, _current_seo5, _current_seo6, _current_seo7;
1108
1109
  const { hostId, product, seedFromCache, seedUnreadable, open, onClose } = props;
1109
1110
  /**
@@ -1355,7 +1356,12 @@ const MemoPaidDownloadAddButton = /*#__PURE__*/ memo(PaidDownloadAddButton);
1355
1356
  title: (_ref15 = (_current_seo2 = current.seo) == null ? void 0 : _current_seo2.title) != null ? _ref15 : '',
1356
1357
  description: (_ref16 = (_current_seo3 = current.seo) == null ? void 0 : _current_seo3.description) != null ? _ref16 : ''
1357
1358
  });
1358
- const error = current.name ? CommerceModel.validateProduct(current) : null;
1359
+ /**
1360
+ * More combinations than a product holds (AGL-4100): said with the count
1361
+ * beside the options, the values kept as typed, and Save refused — the
1362
+ * matrix used to stop at 100 and drop the rest without a word.
1363
+ */ const matrixProblem = CommerceModel.variantMatrixProblem(current.options);
1364
+ const error = matrixProblem != null ? matrixProblem : current.name ? CommerceModel.validateProduct(current) : null;
1359
1365
  /**
1360
1366
  * Stock tracking is inert on a subscription-only product (AGL-1744) —
1361
1367
  * nothing decrements it on the first charge or on any renewal, so the
@@ -1426,6 +1432,13 @@ const MemoPaidDownloadAddButton = /*#__PURE__*/ memo(PaidDownloadAddButton);
1426
1432
  name: '',
1427
1433
  values: []
1428
1434
  }, options[index], patch);
1435
+ // Past the variant ceiling (AGL-4100) the options are kept as typed
1436
+ // and the variants left as they are: nothing is built short, nothing
1437
+ // dropped, and Save stays refused (`matrixProblem`) until values are
1438
+ // removed — when the matrix is rebuilt from them as below.
1439
+ if (CommerceModel.variantMatrixProblem(options)) return {
1440
+ options
1441
+ };
1429
1442
  // Regenerate the matrix, carrying data over by option-combo key so
1430
1443
  // edits to prices/SKUs survive option tweaks.
1431
1444
  const previous = new Map(at.variants.map((variant)=>[
@@ -1685,6 +1698,10 @@ const MemoPaidDownloadAddButton = /*#__PURE__*/ memo(PaidDownloadAddButton);
1685
1698
  onChange: handleOptionsChange,
1686
1699
  onValues: setOptionValues
1687
1700
  }),
1701
+ matrixProblem ? /*#__PURE__*/ _jsx(Alert, {
1702
+ severity: "warning",
1703
+ children: matrixProblem
1704
+ }) : null,
1688
1705
  /*#__PURE__*/ _jsx(ProductVariantsTable, {
1689
1706
  variants: current.variants,
1690
1707
  stockApplies: stockApplies,
@@ -1731,13 +1748,22 @@ const MemoPaidDownloadAddButton = /*#__PURE__*/ memo(PaidDownloadAddButton);
1731
1748
  update: update,
1732
1749
  replaceDraft: replaceDraft
1733
1750
  }),
1751
+ /*#__PURE__*/ _jsx(Divider, {
1752
+ textAlign: "left",
1753
+ children: 'Add-ons'
1754
+ }),
1755
+ /*#__PURE__*/ _jsx(ProductAddOnsFields, {
1756
+ modifierGroups: (_current_modifierGroups = current.modifierGroups) != null ? _current_modifierGroups : NO_MODIFIER_GROUPS,
1757
+ options: (_current_options2 = current.options) != null ? _current_options2 : NO_OPTIONS,
1758
+ variants: current.variants,
1759
+ update: update
1760
+ }),
1734
1761
  pos.ready && pos.entitled ? /*#__PURE__*/ _jsxs(_Fragment, {
1735
1762
  children: [
1736
1763
  /*#__PURE__*/ _jsx(ProductRegisterFields, {
1737
1764
  posQuickKey: Boolean(current.posQuickKey),
1738
1765
  sellOnline: CommerceModel.productSellsOnline(current),
1739
1766
  inStoreReviews: current.inStoreReviews,
1740
- modifierGroups: (_current_modifierGroups = current.modifierGroups) != null ? _current_modifierGroups : NO_MODIFIER_GROUPS,
1741
1767
  update: update
1742
1768
  }),
1743
1769
  /*#__PURE__*/ _jsx(ProductKioskOfferField, {
@@ -1764,7 +1790,7 @@ const MemoPaidDownloadAddButton = /*#__PURE__*/ memo(PaidDownloadAddButton);
1764
1790
  /*#__PURE__*/ _jsx(EntitlementUpsell, {
1765
1791
  planLabel: pos.planLabel,
1766
1792
  upgradeHref: pos.upgradeHref,
1767
- children: 'Register quick keys, modifier groups (sizes, add-ons), check-in at the window and selling a product in person only come with the point of sale.'
1793
+ children: 'Register quick keys, check-in at the window and selling a product in person only come with the point of sale.'
1768
1794
  }),
1769
1795
  !CommerceModel.productSellsOnline(current) ? /*#__PURE__*/ _jsx(ProductSellOnlineField, {
1770
1796
  sellOnline: false,
@@ -1809,7 +1835,7 @@ const MemoPaidDownloadAddButton = /*#__PURE__*/ memo(PaidDownloadAddButton);
1809
1835
  hasImage: Boolean((_current_seo7 = current.seo) == null ? void 0 : _current_seo7.imageUrl),
1810
1836
  proposeValues: proposeSeoValues
1811
1837
  }) : null,
1812
- error ? /*#__PURE__*/ _jsx(Alert, {
1838
+ error && error !== matrixProblem ? /*#__PURE__*/ _jsx(Alert, {
1813
1839
  severity: "warning",
1814
1840
  children: error
1815
1841
  }) : null,