@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.
- package/README.md +1 -0
- package/package.json +13 -13
- package/src/lib/components/cart-fulfillment.d.ts +3 -0
- package/src/lib/components/cart-fulfillment.js.map +1 -1
- package/src/lib/components/cart.js +14 -4
- package/src/lib/components/cart.js.map +1 -1
- package/src/lib/components/console/host-quotes-card.component.js +11 -9
- package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
- package/src/lib/components/console/invoice-send.component.js +1 -1
- package/src/lib/components/console/invoice-send.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-item-dialog.component.js +3 -2
- package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
- package/src/lib/components/console/pos-kiosk-page.component.js +2 -1
- package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +1 -1
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/product-add-ons-fields.component.d.ts +41 -0
- package/src/lib/components/console/product-add-ons-fields.component.js +356 -0
- package/src/lib/components/console/product-add-ons-fields.component.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +31 -5
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-register-fields.component.d.ts +4 -5
- package/src/lib/components/console/product-register-fields.component.js +8 -197
- package/src/lib/components/console/product-register-fields.component.js.map +1 -1
- package/src/lib/components/console/products-hub-zone.component.js +6 -0
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
- package/src/lib/components/console/quote-pay-link.component.d.ts +8 -1
- package/src/lib/components/console/quote-pay-link.component.js +29 -3
- package/src/lib/components/console/quote-pay-link.component.js.map +1 -1
- package/src/lib/components/console/quote-send.component.js +1 -4
- package/src/lib/components/console/quote-send.component.js.map +1 -1
- package/src/lib/components/product-add-ons.d.ts +28 -0
- package/src/lib/components/product-add-ons.js +136 -0
- package/src/lib/components/product-add-ons.js.map +1 -0
- package/src/lib/components/product-detail.d.ts +16 -0
- package/src/lib/components/product-detail.js +112 -19
- package/src/lib/components/product-detail.js.map +1 -1
- package/src/lib/components/product-grid.d.ts +8 -2
- package/src/lib/components/product-grid.js +6 -1
- package/src/lib/components/product-grid.js.map +1 -1
- package/src/lib/declarations.console-server.js +15 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/model/commerce-cart.d.ts +15 -3
- package/src/lib/model/commerce-cart.js +10 -3
- package/src/lib/model/commerce-cart.js.map +1 -1
- package/src/lib/model/commerce-invoices.d.ts +7 -0
- package/src/lib/model/commerce-invoices.js +11 -0
- package/src/lib/model/commerce-invoices.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +6 -3
- package/src/lib/model/commerce-orders.js +3 -0
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-pos-kiosk.d.ts +1 -1
- package/src/lib/model/commerce-pos-kiosk.js +3 -2
- package/src/lib/model/commerce-pos-kiosk.js.map +1 -1
- package/src/lib/model/commerce-quote-email.d.ts +14 -0
- package/src/lib/model/commerce-quote-email.js +25 -9
- package/src/lib/model/commerce-quote-email.js.map +1 -1
- package/src/lib/model/commerce-quote-proofs.d.ts +7 -0
- package/src/lib/model/commerce-quote-proofs.js +15 -0
- package/src/lib/model/commerce-quote-proofs.js.map +1 -1
- package/src/lib/model/commerce-quotes.js +1 -1
- package/src/lib/model/commerce-quotes.js.map +1 -1
- package/src/lib/model/commerce.d.ts +18 -0
- package/src/lib/model/commerce.js +26 -0
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/product-modifiers.d.ts +74 -4
- package/src/lib/model/product-modifiers.js +131 -10
- package/src/lib/model/product-modifiers.js.map +1 -1
- package/src/lib/server/billing-webhook.js +64 -43
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cart-checkout.js +14 -2
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/cart.d.ts +5 -0
- package/src/lib/server/cart.js +41 -4
- package/src/lib/server/cart.js.map +1 -1
- package/src/lib/server/catalog.d.ts +6 -0
- package/src/lib/server/catalog.js +2 -0
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout-extras.js +3 -1
- package/src/lib/server/checkout-extras.js.map +1 -1
- package/src/lib/server/checkout.js +28 -3
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/local-fulfillment.js +5 -1
- package/src/lib/server/local-fulfillment.js.map +1 -1
- package/src/lib/server/pos-offline-sync.js +1 -1
- package/src/lib/server/pos-offline-sync.js.map +1 -1
- package/src/lib/server/pos-order.js +1 -1
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/product.d.ts +26 -0
- package/src/lib/server/product.js +18 -0
- package/src/lib/server/product.js.map +1 -1
- package/src/lib/server/products-write.js +5 -2
- package/src/lib/server/products-write.js.map +1 -1
- package/src/lib/server/quote-email.js.map +1 -1
- package/src/lib/server/quote-link-token.d.ts +4 -2
- package/src/lib/server/quote-link-token.js +4 -2
- package/src/lib/server/quote-link-token.js.map +1 -1
- package/src/lib/server/quote-pay.d.ts +5 -1
- package/src/lib/server/quote-pay.js +5 -2
- package/src/lib/server/quote-pay.js.map +1 -1
- package/src/lib/server/quote-proofs.d.ts +9 -1
- package/src/lib/server/quote-proofs.js +92 -23
- package/src/lib/server/quote-proofs.js.map +1 -1
- package/src/lib/server/quote-upload-keys.d.ts +50 -0
- package/src/lib/server/quote-upload-keys.js +86 -0
- package/src/lib/server/quote-upload-keys.js.map +1 -0
- package/src/lib/server/quote-upload-sweep.d.ts +30 -0
- package/src/lib/server/quote-upload-sweep.js +117 -0
- package/src/lib/server/quote-upload-sweep.js.map +1 -0
- package/src/lib/utils/add-to-cart.d.ts +8 -0
- package/src/lib/utils/add-to-cart.js +10 -2
- package/src/lib/utils/add-to-cart.js.map +1 -1
- package/src/lib/utils/quote-email-content.d.ts +1 -0
- package/src/lib/utils/quote-email-content.js +7 -4
- package/src/lib/utils/quote-email-content.js.map +1 -1
|
@@ -6,7 +6,6 @@ export interface ProductRegisterFieldsProps {
|
|
|
6
6
|
sellOnline: boolean;
|
|
7
7
|
/** What the site does with the reviews of a product sold in person only (AGL-4060). */
|
|
8
8
|
inStoreReviews?: CommerceModel.HostProduct['inStoreReviews'];
|
|
9
|
-
modifierGroups: readonly CommerceModel.ProductModifierGroup[];
|
|
10
9
|
update: (patch: Patch) => void;
|
|
11
10
|
}
|
|
12
11
|
export interface ProductSellOnlineFieldProps {
|
|
@@ -23,10 +22,10 @@ export interface ProductSellOnlineFieldProps {
|
|
|
23
22
|
export declare const ProductSellOnlineField: import("react").MemoExoticComponent<(props: ProductSellOnlineFieldProps) => import("react").JSX.Element>;
|
|
24
23
|
/**
|
|
25
24
|
* The product's register settings (AGL-3607): whether it is a quick key on
|
|
26
|
-
* the register's first screen, and
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
25
|
+
* the register's first screen, and whether it sells online at all. Its
|
|
26
|
+
* add-ons — the register's modifiers — are edited for every store in
|
|
27
|
+
* `ProductAddOnsFields` (AGL-4100), where each group says whether the
|
|
28
|
+
* website offers it too.
|
|
30
29
|
*
|
|
31
30
|
* Memoized on its own props like every section of this editor (AGL-3423):
|
|
32
31
|
* a keystroke elsewhere in the dialog does not redraw it.
|
|
@@ -14,19 +14,10 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
|
-
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
17
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
19
|
-
import {
|
|
18
|
+
import { FormControlLabel, MenuItem, Stack, Switch, TextField, Typography } from "@mui/material";
|
|
20
19
|
import { PLATFORM_BRAND_NAME } from "@aglyn/aglyn/app-utils/platform-brand";
|
|
21
|
-
import { memo,
|
|
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
|
-
}
|
|
20
|
+
import { memo, useId } from "react";
|
|
30
21
|
/** The "Reviews on your site" choices, as the editor words them (AGL-4060). */ const IN_STORE_REVIEWS_OPTIONS = [
|
|
31
22
|
{
|
|
32
23
|
value: 'show',
|
|
@@ -90,28 +81,15 @@ function newId(prefix) {
|
|
|
90
81
|
});
|
|
91
82
|
/**
|
|
92
83
|
* The product's register settings (AGL-3607): whether it is a quick key on
|
|
93
|
-
* the register's first screen, and
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
84
|
+
* the register's first screen, and whether it sells online at all. Its
|
|
85
|
+
* add-ons — the register's modifiers — are edited for every store in
|
|
86
|
+
* `ProductAddOnsFields` (AGL-4100), where each group says whether the
|
|
87
|
+
* website offers it too.
|
|
97
88
|
*
|
|
98
89
|
* Memoized on its own props like every section of this editor (AGL-3423):
|
|
99
90
|
* a keystroke elsewhere in the dialog does not redraw it.
|
|
100
91
|
*/ export const ProductRegisterFields = /*#__PURE__*/ memo(function ProductRegisterFields(props) {
|
|
101
|
-
const {
|
|
102
|
-
const setGroups = useCallback((groups)=>update({
|
|
103
|
-
modifierGroups: groups
|
|
104
|
-
}), [
|
|
105
|
-
update
|
|
106
|
-
]);
|
|
107
|
-
const editGroup = (index, patch)=>setGroups(modifierGroups.map((group, at)=>at === index ? _extends({}, group, patch) : group));
|
|
108
|
-
const editOption = (groupIndex, optionIndex, patch)=>{
|
|
109
|
-
const group = modifierGroups[groupIndex];
|
|
110
|
-
editGroup(groupIndex, {
|
|
111
|
-
options: group.options.map((option, at)=>at === optionIndex ? _extends({}, option, patch) : option)
|
|
112
|
-
});
|
|
113
|
-
};
|
|
114
|
-
const problem = CommerceModel.modifierGroupsProblem(modifierGroups);
|
|
92
|
+
const { update } = props;
|
|
115
93
|
return /*#__PURE__*/ _jsxs(Stack, {
|
|
116
94
|
spacing: 2,
|
|
117
95
|
children: [
|
|
@@ -133,174 +111,7 @@ function newId(prefix) {
|
|
|
133
111
|
sellOnline: props.sellOnline,
|
|
134
112
|
inStoreReviews: props.inStoreReviews,
|
|
135
113
|
update: update
|
|
136
|
-
})
|
|
137
|
-
/*#__PURE__*/ _jsx(Typography, {
|
|
138
|
-
variant: "body2",
|
|
139
|
-
color: "text.secondary",
|
|
140
|
-
children: 'Modifiers are choices added as the item is rung up, such as a milk or an extra shot. ' + 'Each can be free or add to the price.'
|
|
141
|
-
}),
|
|
142
|
-
modifierGroups.map((group, groupIndex)=>/*#__PURE__*/ _jsx(Box, {
|
|
143
|
-
role: "group",
|
|
144
|
-
"aria-label": group.name || 'Modifier group',
|
|
145
|
-
sx: {
|
|
146
|
-
border: 1,
|
|
147
|
-
borderColor: 'divider',
|
|
148
|
-
borderRadius: 1,
|
|
149
|
-
p: 2
|
|
150
|
-
},
|
|
151
|
-
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
152
|
-
spacing: 1.5,
|
|
153
|
-
children: [
|
|
154
|
-
/*#__PURE__*/ _jsxs(Stack, {
|
|
155
|
-
direction: "row",
|
|
156
|
-
spacing: 1,
|
|
157
|
-
sx: {
|
|
158
|
-
alignItems: 'flex-start'
|
|
159
|
-
},
|
|
160
|
-
children: [
|
|
161
|
-
/*#__PURE__*/ _jsx(TextField, {
|
|
162
|
-
label: "Group name",
|
|
163
|
-
size: "small",
|
|
164
|
-
value: group.name,
|
|
165
|
-
onChange: (event)=>editGroup(groupIndex, {
|
|
166
|
-
name: event.target.value
|
|
167
|
-
}),
|
|
168
|
-
sx: {
|
|
169
|
-
flex: 1
|
|
170
|
-
}
|
|
171
|
-
}),
|
|
172
|
-
/*#__PURE__*/ _jsx(TextField, {
|
|
173
|
-
label: "At least",
|
|
174
|
-
size: "small",
|
|
175
|
-
value: group.min,
|
|
176
|
-
onChange: (event)=>editGroup(groupIndex, {
|
|
177
|
-
min: Math.max(0, Math.round(Number(event.target.value) || 0))
|
|
178
|
-
}),
|
|
179
|
-
helperText: group.min > 0 ? 'Required' : 'Optional',
|
|
180
|
-
sx: {
|
|
181
|
-
width: 96
|
|
182
|
-
},
|
|
183
|
-
slotProps: {
|
|
184
|
-
htmlInput: {
|
|
185
|
-
inputMode: 'numeric'
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}),
|
|
189
|
-
/*#__PURE__*/ _jsx(TextField, {
|
|
190
|
-
label: "At most",
|
|
191
|
-
size: "small",
|
|
192
|
-
value: group.max,
|
|
193
|
-
onChange: (event)=>editGroup(groupIndex, {
|
|
194
|
-
max: Math.max(1, Math.round(Number(event.target.value) || 1))
|
|
195
|
-
}),
|
|
196
|
-
sx: {
|
|
197
|
-
width: 96
|
|
198
|
-
},
|
|
199
|
-
slotProps: {
|
|
200
|
-
htmlInput: {
|
|
201
|
-
inputMode: 'numeric'
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}),
|
|
205
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
206
|
-
color: "error",
|
|
207
|
-
onClick: ()=>setGroups(modifierGroups.filter((_group, at)=>at !== groupIndex)),
|
|
208
|
-
children: 'Remove group'
|
|
209
|
-
})
|
|
210
|
-
]
|
|
211
|
-
}),
|
|
212
|
-
group.options.map((option, optionIndex)=>/*#__PURE__*/ _jsxs(Stack, {
|
|
213
|
-
direction: "row",
|
|
214
|
-
spacing: 1,
|
|
215
|
-
sx: {
|
|
216
|
-
alignItems: 'center'
|
|
217
|
-
},
|
|
218
|
-
children: [
|
|
219
|
-
/*#__PURE__*/ _jsx(TextField, {
|
|
220
|
-
label: "Choice",
|
|
221
|
-
size: "small",
|
|
222
|
-
value: option.name,
|
|
223
|
-
onChange: (event)=>editOption(groupIndex, optionIndex, {
|
|
224
|
-
name: event.target.value
|
|
225
|
-
}),
|
|
226
|
-
sx: {
|
|
227
|
-
flex: 1
|
|
228
|
-
}
|
|
229
|
-
}),
|
|
230
|
-
/*#__PURE__*/ _jsx(TextField, {
|
|
231
|
-
label: "Adds ($)",
|
|
232
|
-
size: "small",
|
|
233
|
-
defaultValue: option.priceCents ? (option.priceCents / 100).toFixed(2) : '',
|
|
234
|
-
placeholder: "0.00",
|
|
235
|
-
onBlur: (event)=>editOption(groupIndex, optionIndex, {
|
|
236
|
-
priceCents: centsFromDollars(event.target.value)
|
|
237
|
-
}),
|
|
238
|
-
sx: {
|
|
239
|
-
width: 120
|
|
240
|
-
},
|
|
241
|
-
slotProps: {
|
|
242
|
-
htmlInput: {
|
|
243
|
-
inputMode: 'decimal'
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}),
|
|
247
|
-
/*#__PURE__*/ _jsx(IconButton, {
|
|
248
|
-
"aria-label": `Remove ${option.name || 'choice'}`,
|
|
249
|
-
onClick: ()=>editGroup(groupIndex, {
|
|
250
|
-
options: group.options.filter((_option, at)=>at !== optionIndex),
|
|
251
|
-
max: Math.max(1, Math.min(group.max, group.options.length - 1))
|
|
252
|
-
}),
|
|
253
|
-
children: '✕'
|
|
254
|
-
})
|
|
255
|
-
]
|
|
256
|
-
}, option.id)),
|
|
257
|
-
/*#__PURE__*/ _jsx(Box, {
|
|
258
|
-
children: /*#__PURE__*/ _jsx(Button, {
|
|
259
|
-
onClick: ()=>editGroup(groupIndex, {
|
|
260
|
-
options: [
|
|
261
|
-
...group.options,
|
|
262
|
-
{
|
|
263
|
-
id: newId('o'),
|
|
264
|
-
name: '',
|
|
265
|
-
priceCents: 0
|
|
266
|
-
}
|
|
267
|
-
]
|
|
268
|
-
}),
|
|
269
|
-
disabled: group.options.length >= CommerceModel.POS_MAX_MODIFIER_OPTIONS,
|
|
270
|
-
children: 'Add choice'
|
|
271
|
-
})
|
|
272
|
-
})
|
|
273
|
-
]
|
|
274
|
-
})
|
|
275
|
-
}, group.id)),
|
|
276
|
-
/*#__PURE__*/ _jsx(Box, {
|
|
277
|
-
children: /*#__PURE__*/ _jsx(Button, {
|
|
278
|
-
variant: "outlined",
|
|
279
|
-
onClick: ()=>setGroups([
|
|
280
|
-
...modifierGroups,
|
|
281
|
-
{
|
|
282
|
-
id: newId('g'),
|
|
283
|
-
name: '',
|
|
284
|
-
min: 0,
|
|
285
|
-
max: 1,
|
|
286
|
-
options: [
|
|
287
|
-
{
|
|
288
|
-
id: newId('o'),
|
|
289
|
-
name: '',
|
|
290
|
-
priceCents: 0
|
|
291
|
-
}
|
|
292
|
-
]
|
|
293
|
-
}
|
|
294
|
-
]),
|
|
295
|
-
disabled: modifierGroups.length >= CommerceModel.POS_MAX_MODIFIER_GROUPS,
|
|
296
|
-
children: 'Add modifier group'
|
|
297
|
-
})
|
|
298
|
-
}),
|
|
299
|
-
problem && modifierGroups.length ? /*#__PURE__*/ _jsx(Typography, {
|
|
300
|
-
variant: "body2",
|
|
301
|
-
color: "warning.main",
|
|
302
|
-
children: problem
|
|
303
|
-
}) : null
|
|
114
|
+
})
|
|
304
115
|
]
|
|
305
116
|
});
|
|
306
117
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/product-register-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 FormControlLabel,\n IconButton,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { memo, useCallback, useId } from 'react'\nimport * as CommerceModel from '../../model'\n\ntype Patch = Partial<CommerceModel.HostProduct>\n\nexport interface ProductRegisterFieldsProps {\n posQuickKey: boolean\n /** `productSellsOnline` of the product: absent reads as on. */\n sellOnline: boolean\n /** What the site does with the reviews of a product sold in person only (AGL-4060). */\n inStoreReviews?: CommerceModel.HostProduct['inStoreReviews']\n modifierGroups: readonly CommerceModel.ProductModifierGroup[]\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\nexport interface ProductSellOnlineFieldProps {\n sellOnline: boolean\n /** What the site does with its reviews while it sells in person only; absent reads as `show`. */\n inStoreReviews?: CommerceModel.HostProduct['inStoreReviews']\n update: (patch: Patch) => void\n}\n\n/** The \"Reviews on your site\" choices, as the editor words them (AGL-4060). */\nconst IN_STORE_REVIEWS_OPTIONS: ReadonlyArray<{ value: 'show' | 'hide'; label: string; help: string }> = [\n {\n value: 'show',\n label: 'Show, marked Sold in store',\n help: 'Its reviews appear in your site’s reviews and its average, marked “Sold in store”, with no link to a product page.',\n },\n {\n value: 'hide',\n label: 'Hide',\n help: 'Its reviews appear nowhere on your site: not in the reviews list and not in its average. You still see them under Reviews here.',\n },\n]\n\n/**\n * The product's \"Sell online\" switch (AGL-4060). On (the default, and what\n * every product stored before it reads as) the product sells on the site and\n * its channels as it always has; off, it sells in person only.\n */\nexport const ProductSellOnlineField = memo(function ProductSellOnlineField(\n props: ProductSellOnlineFieldProps,\n) {\n const { sellOnline, update } = props\n const helpId = useId()\n const reviews = props.inStoreReviews === 'hide' ? 'hide' : 'show'\n return (\n <Stack spacing={1}>\n <FormControlLabel\n control={\n <Switch\n checked={sellOnline}\n onChange={(event) => update({ sellOnline: event.target.checked })}\n slotProps={{ input: { 'aria-describedby': helpId } }}\n />\n }\n label=\"Sell online\"\n />\n <Typography id={helpId} variant=\"body2\" color=\"text.secondary\">\n {sellOnline\n ? 'Sold on your site — its product page, product grids, search, the cart and checkout — and in any shopping feeds and marketplaces you connect, as well as in person.'\n : `In person only: sold at the register, the kiosk, the ${PLATFORM_BRAND_NAME} POS apps and on draft orders. It has no page on your site and is left out of product grids, search, the sitemap, the cart, checkout and every shopping feed.`}\n </Typography>\n {/*\n Its reviews, while it sells in person only (AGL-4060): shown on the\n site marked \"Sold in store\" by default, or kept off it. Online, a\n product's reviews are always on its own page, so the choice is not\n offered.\n */}\n {sellOnline ? null : (\n <TextField\n select\n size=\"small\"\n label=\"Reviews on your site\"\n value={reviews}\n onChange={(event) => update({ inStoreReviews: event.target.value as 'show' | 'hide' })}\n helperText={IN_STORE_REVIEWS_OPTIONS.find((option) => option.value === reviews)?.help}\n >\n {IN_STORE_REVIEWS_OPTIONS.map((option) => (\n <MenuItem key={option.value} value={option.value}>\n {option.label}\n </MenuItem>\n ))}\n </TextField>\n )}\n </Stack>\n )\n})\n\n/**\n * The product's register settings (AGL-3607): whether it is a quick key on\n * the register's first screen, and its modifier groups — choices such as\n * \"Milk\" or \"Add-ons\" the cashier picks as the item is rung up, each choice\n * free or with a price added to the item. A group with a minimum of 1 or\n * more must be answered before the item can be added.\n *\n * Memoized on its own props like every section of this editor (AGL-3423):\n * a keystroke elsewhere in the dialog does not redraw it.\n */\nexport const ProductRegisterFields = memo(function ProductRegisterFields(\n props: ProductRegisterFieldsProps,\n) {\n const { modifierGroups, update } = props\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 editOption = (\n groupIndex: number,\n optionIndex: number,\n patch: Partial<CommerceModel.ProductModifierOption>,\n ) => {\n const group = modifierGroups[groupIndex]\n editGroup(groupIndex, {\n options: group.options.map((option, at) => (at === optionIndex ? { ...option, ...patch } : option)),\n })\n }\n const problem = CommerceModel.modifierGroupsProblem(modifierGroups)\n\n return (\n <Stack spacing={2}>\n <Typography variant=\"subtitle1\" component=\"h3\">\n {'At the register'}\n </Typography>\n <FormControlLabel\n control={\n <Switch\n checked={props.posQuickKey}\n onChange={(event) => update({ posQuickKey: event.target.checked })}\n />\n }\n label=\"Quick key at the register\"\n />\n <ProductSellOnlineField sellOnline={props.sellOnline} inStoreReviews={props.inStoreReviews} update={update} />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Modifiers are choices added as the item is rung up, such as a milk or an extra shot. ' +\n 'Each can be free or add to the price.'}\n </Typography>\n {modifierGroups.map((group, groupIndex) => (\n <Box\n key={group.id}\n role=\"group\"\n aria-label={group.name || 'Modifier 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' }}>\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 }}\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: 96 }}\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: 96 }}\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 {group.options.map((option, optionIndex) => (\n <Stack key={option.id} direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\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 }}\n />\n <TextField\n label=\"Adds ($)\"\n size=\"small\"\n defaultValue={option.priceCents ? (option.priceCents / 100).toFixed(2) : ''}\n placeholder=\"0.00\"\n onBlur={(event) =>\n editOption(groupIndex, optionIndex, { priceCents: centsFromDollars(event.target.value) })\n }\n sx={{ width: 120 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\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 ))}\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 onClick={() =>\n setGroups([\n ...modifierGroups,\n {\n id: newId('g'),\n name: '',\n min: 0,\n max: 1,\n options: [{ id: newId('o'), name: '', priceCents: 0 }],\n },\n ])\n }\n disabled={modifierGroups.length >= CommerceModel.POS_MAX_MODIFIER_GROUPS}\n >\n {'Add modifier 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","FormControlLabel","IconButton","MenuItem","Stack","Switch","TextField","Typography","PLATFORM_BRAND_NAME","memo","useCallback","useId","CommerceModel","newId","prefix","Math","random","toString","slice","centsFromDollars","value","number","round","Number","String","replace","isFinite","IN_STORE_REVIEWS_OPTIONS","label","help","ProductSellOnlineField","props","sellOnline","update","helpId","reviews","inStoreReviews","spacing","control","checked","onChange","event","target","slotProps","input","id","variant","color","select","size","helperText","find","option","map","ProductRegisterFields","modifierGroups","setGroups","groups","editGroup","index","patch","group","at","editOption","groupIndex","optionIndex","options","problem","modifierGroupsProblem","component","posQuickKey","role","aria-label","name","sx","border","borderColor","borderRadius","p","direction","alignItems","flex","min","max","width","htmlInput","inputMode","onClick","filter","_group","defaultValue","priceCents","toFixed","placeholder","onBlur","_option","length","disabled","POS_MAX_MODIFIER_OPTIONS","POS_MAX_MODIFIER_GROUPS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SACEA,GAAG,EACHC,MAAM,EACNC,gBAAgB,EAChBC,UAAU,EACVC,QAAQ,EACRC,KAAK,EACLC,MAAM,EACNC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,mBAAmB,QAAQ,wCAAuC;AAC3E,SAASC,IAAI,EAAEC,WAAW,EAAEC,KAAK,QAAQ,QAAO;AAChD,YAAYC,mBAAmB,uBAAa;AAc5C,SAASC,MAAMC,MAAc;IAC3B,OAAO,GAAGA,SAASC,KAAKC,MAAM,GAAGC,QAAQ,CAAC,IAAIC,KAAK,CAAC,GAAG,KAAK;AAC9D;AAEA,qDAAqD,GACrD,SAASC,iBAAiBC,KAAa;IACrC,MAAMC,SAASN,KAAKO,KAAK,CAACC,OAAOC,OAAOJ,OAAOK,OAAO,CAAC,YAAY,OAAO;IAC1E,OAAOF,OAAOG,QAAQ,CAACL,WAAWA,SAAS,IAAIA,SAAS;AAC1D;AASA,6EAA6E,GAC7E,MAAMM,2BAAmG;IACvG;QACEP,OAAO;QACPQ,OAAO;QACPC,MAAM;IACR;IACA;QACET,OAAO;QACPQ,OAAO;QACPC,MAAM;IACR;CACD;AAED;;;;CAIC,GACD,OAAO,MAAMC,uCAAyBrB,KAAK,SAASqB,uBAClDC,KAAkC;QAmCdJ;IAjCpB,MAAM,EAAEK,UAAU,EAAEC,MAAM,EAAE,GAAGF;IAC/B,MAAMG,SAASvB;IACf,MAAMwB,UAAUJ,MAAMK,cAAc,KAAK,SAAS,SAAS;IAC3D,qBACE,MAAChC;QAAMiC,SAAS;;0BACd,KAACpC;gBACCqC,uBACE,KAACjC;oBACCkC,SAASP;oBACTQ,UAAU,CAACC,QAAUR,OAAO;4BAAED,YAAYS,MAAMC,MAAM,CAACH,OAAO;wBAAC;oBAC/DI,WAAW;wBAAEC,OAAO;4BAAE,oBAAoBV;wBAAO;oBAAE;;gBAGvDN,OAAM;;0BAER,KAACrB;gBAAWsC,IAAIX;gBAAQY,SAAQ;gBAAQC,OAAM;0BAC3Cf,aACG,uKACA,CAAC,qDAAqD,EAAExB,oBAAoB,6JAA6J,CAAC;;YAQ/OwB,aAAa,qBACZ,KAAC1B;gBACC0C,MAAM;gBACNC,MAAK;gBACLrB,OAAM;gBACNR,OAAOe;gBACPK,UAAU,CAACC,QAAUR,OAAO;wBAAEG,gBAAgBK,MAAMC,MAAM,CAACtB,KAAK;oBAAoB;gBACpF8B,UAAU,GAAEvB,iCAAAA,yBAAyBwB,IAAI,CAAC,CAACC,SAAWA,OAAOhC,KAAK,KAAKe,6BAA3DR,+BAAqEE,IAAI;0BAEpFF,yBAAyB0B,GAAG,CAAC,CAACD,uBAC7B,KAACjD;wBAA4BiB,OAAOgC,OAAOhC,KAAK;kCAC7CgC,OAAOxB,KAAK;uBADAwB,OAAOhC,KAAK;;;;AAQvC,GAAE;AAEF;;;;;;;;;CASC,GACD,OAAO,MAAMkC,sCAAwB7C,KAAK,SAAS6C,sBACjDvB,KAAiC;IAEjC,MAAM,EAAEwB,cAAc,EAAEtB,MAAM,EAAE,GAAGF;IACnC,MAAMyB,YAAY9C,YAChB,CAAC+C,SAAiDxB,OAAO;YAAEsB,gBAAgBE;QAAO,IAClF;QAACxB;KAAO;IAEV,MAAMyB,YAAY,CAACC,OAAeC,QAChCJ,UAAUD,eAAeF,GAAG,CAAC,CAACQ,OAAOC,KAAQA,OAAOH,QAAQ,aAAKE,OAAUD,SAAUC;IACvF,MAAME,aAAa,CACjBC,YACAC,aACAL;QAEA,MAAMC,QAAQN,cAAc,CAACS,WAAW;QACxCN,UAAUM,YAAY;YACpBE,SAASL,MAAMK,OAAO,CAACb,GAAG,CAAC,CAACD,QAAQU,KAAQA,OAAOG,cAAc,aAAKb,QAAWQ,SAAUR;QAC7F;IACF;IACA,MAAMe,UAAUvD,cAAcwD,qBAAqB,CAACb;IAEpD,qBACE,MAACnD;QAAMiC,SAAS;;0BACd,KAAC9B;gBAAWuC,SAAQ;gBAAYuB,WAAU;0BACvC;;0BAEH,KAACpE;gBACCqC,uBACE,KAACjC;oBACCkC,SAASR,MAAMuC,WAAW;oBAC1B9B,UAAU,CAACC,QAAUR,OAAO;4BAAEqC,aAAa7B,MAAMC,MAAM,CAACH,OAAO;wBAAC;;gBAGpEX,OAAM;;0BAER,KAACE;gBAAuBE,YAAYD,MAAMC,UAAU;gBAAEI,gBAAgBL,MAAMK,cAAc;gBAAEH,QAAQA;;0BACpG,KAAC1B;gBAAWuC,SAAQ;gBAAQC,OAAM;0BAC/B,0FACC;;YAEHQ,eAAeF,GAAG,CAAC,CAACQ,OAAOG,2BAC1B,KAACjE;oBAECwE,MAAK;oBACLC,cAAYX,MAAMY,IAAI,IAAI;oBAC1BC,IAAI;wBAAEC,QAAQ;wBAAGC,aAAa;wBAAWC,cAAc;wBAAGC,GAAG;oBAAE;8BAE/D,cAAA,MAAC1E;wBAAMiC,SAAS;;0CACd,MAACjC;gCAAM2E,WAAU;gCAAM1C,SAAS;gCAAGqC,IAAI;oCAAEM,YAAY;gCAAa;;kDAChE,KAAC1E;wCACCsB,OAAM;wCACNqB,MAAK;wCACL7B,OAAOyC,MAAMY,IAAI;wCACjBjC,UAAU,CAACC,QAAUiB,UAAUM,YAAY;gDAAES,MAAMhC,MAAMC,MAAM,CAACtB,KAAK;4CAAC;wCACtEsD,IAAI;4CAAEO,MAAM;wCAAE;;kDAEhB,KAAC3E;wCACCsB,OAAM;wCACNqB,MAAK;wCACL7B,OAAOyC,MAAMqB,GAAG;wCAChB1C,UAAU,CAACC,QACTiB,UAAUM,YAAY;gDAAEkB,KAAKnE,KAAKoE,GAAG,CAAC,GAAGpE,KAAKO,KAAK,CAACC,OAAOkB,MAAMC,MAAM,CAACtB,KAAK,KAAK;4CAAI;wCAExF8B,YAAYW,MAAMqB,GAAG,GAAG,IAAI,aAAa;wCACzCR,IAAI;4CAAEU,OAAO;wCAAG;wCAChBzC,WAAW;4CAAE0C,WAAW;gDAAEC,WAAW;4CAAU;wCAAE;;kDAEnD,KAAChF;wCACCsB,OAAM;wCACNqB,MAAK;wCACL7B,OAAOyC,MAAMsB,GAAG;wCAChB3C,UAAU,CAACC,QACTiB,UAAUM,YAAY;gDAAEmB,KAAKpE,KAAKoE,GAAG,CAAC,GAAGpE,KAAKO,KAAK,CAACC,OAAOkB,MAAMC,MAAM,CAACtB,KAAK,KAAK;4CAAI;wCAExFsD,IAAI;4CAAEU,OAAO;wCAAG;wCAChBzC,WAAW;4CAAE0C,WAAW;gDAAEC,WAAW;4CAAU;wCAAE;;kDAEnD,KAACtF;wCACC+C,OAAM;wCACNwC,SAAS,IAAM/B,UAAUD,eAAeiC,MAAM,CAAC,CAACC,QAAQ3B,KAAOA,OAAOE;kDAErE;;;;4BAGJH,MAAMK,OAAO,CAACb,GAAG,CAAC,CAACD,QAAQa,4BAC1B,MAAC7D;oCAAsB2E,WAAU;oCAAM1C,SAAS;oCAAGqC,IAAI;wCAAEM,YAAY;oCAAS;;sDAC5E,KAAC1E;4CACCsB,OAAM;4CACNqB,MAAK;4CACL7B,OAAOgC,OAAOqB,IAAI;4CAClBjC,UAAU,CAACC,QAAUsB,WAAWC,YAAYC,aAAa;oDAAEQ,MAAMhC,MAAMC,MAAM,CAACtB,KAAK;gDAAC;4CACpFsD,IAAI;gDAAEO,MAAM;4CAAE;;sDAEhB,KAAC3E;4CACCsB,OAAM;4CACNqB,MAAK;4CACLyC,cAActC,OAAOuC,UAAU,GAAG,AAACvC,CAAAA,OAAOuC,UAAU,GAAG,GAAE,EAAGC,OAAO,CAAC,KAAK;4CACzEC,aAAY;4CACZC,QAAQ,CAACrD,QACPsB,WAAWC,YAAYC,aAAa;oDAAE0B,YAAYxE,iBAAiBsB,MAAMC,MAAM,CAACtB,KAAK;gDAAE;4CAEzFsD,IAAI;gDAAEU,OAAO;4CAAI;4CACjBzC,WAAW;gDAAE0C,WAAW;oDAAEC,WAAW;gDAAU;4CAAE;;sDAEnD,KAACpF;4CACCsE,cAAY,CAAC,OAAO,EAAEpB,OAAOqB,IAAI,IAAI,UAAU;4CAC/Cc,SAAS,IACP7B,UAAUM,YAAY;oDACpBE,SAASL,MAAMK,OAAO,CAACsB,MAAM,CAAC,CAACO,SAASjC,KAAOA,OAAOG;oDACtDkB,KAAKpE,KAAKoE,GAAG,CAAC,GAAGpE,KAAKmE,GAAG,CAACrB,MAAMsB,GAAG,EAAEtB,MAAMK,OAAO,CAAC8B,MAAM,GAAG;gDAC9D;sDAGD;;;mCA5BO5C,OAAOP,EAAE;0CAgCvB,KAAC9C;0CACC,cAAA,KAACC;oCACCuF,SAAS,IACP7B,UAAUM,YAAY;4CACpBE,SAAS;mDAAIL,MAAMK,OAAO;gDAAE;oDAAErB,IAAIhC,MAAM;oDAAM4D,MAAM;oDAAIkB,YAAY;gDAAE;6CAAE;wCAC1E;oCAEFM,UAAUpC,MAAMK,OAAO,CAAC8B,MAAM,IAAIpF,cAAcsF,wBAAwB;8CAEvE;;;;;mBApFFrC,MAAMhB,EAAE;0BA0FjB,KAAC9C;0BACC,cAAA,KAACC;oBACC8C,SAAQ;oBACRyC,SAAS,IACP/B,UAAU;+BACLD;4BACH;gCACEV,IAAIhC,MAAM;gCACV4D,MAAM;gCACNS,KAAK;gCACLC,KAAK;gCACLjB,SAAS;oCAAC;wCAAErB,IAAIhC,MAAM;wCAAM4D,MAAM;wCAAIkB,YAAY;oCAAE;iCAAE;4BACxD;yBACD;oBAEHM,UAAU1C,eAAeyC,MAAM,IAAIpF,cAAcuF,uBAAuB;8BAEvE;;;YAGJhC,WAAWZ,eAAeyC,MAAM,iBAC/B,KAACzF;gBAAWuC,SAAQ;gBAAQC,OAAM;0BAC/BoB;iBAED;;;AAGV,GAAE"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/product-register-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 FormControlLabel,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { memo, useId } from 'react'\nimport * as CommerceModel from '../../model'\n\ntype Patch = Partial<CommerceModel.HostProduct>\n\nexport interface ProductRegisterFieldsProps {\n posQuickKey: boolean\n /** `productSellsOnline` of the product: absent reads as on. */\n sellOnline: boolean\n /** What the site does with the reviews of a product sold in person only (AGL-4060). */\n inStoreReviews?: CommerceModel.HostProduct['inStoreReviews']\n update: (patch: Patch) => void\n}\n\nexport interface ProductSellOnlineFieldProps {\n sellOnline: boolean\n /** What the site does with its reviews while it sells in person only; absent reads as `show`. */\n inStoreReviews?: CommerceModel.HostProduct['inStoreReviews']\n update: (patch: Patch) => void\n}\n\n/** The \"Reviews on your site\" choices, as the editor words them (AGL-4060). */\nconst IN_STORE_REVIEWS_OPTIONS: ReadonlyArray<{ value: 'show' | 'hide'; label: string; help: string }> = [\n {\n value: 'show',\n label: 'Show, marked Sold in store',\n help: 'Its reviews appear in your site’s reviews and its average, marked “Sold in store”, with no link to a product page.',\n },\n {\n value: 'hide',\n label: 'Hide',\n help: 'Its reviews appear nowhere on your site: not in the reviews list and not in its average. You still see them under Reviews here.',\n },\n]\n\n/**\n * The product's \"Sell online\" switch (AGL-4060). On (the default, and what\n * every product stored before it reads as) the product sells on the site and\n * its channels as it always has; off, it sells in person only.\n */\nexport const ProductSellOnlineField = memo(function ProductSellOnlineField(\n props: ProductSellOnlineFieldProps,\n) {\n const { sellOnline, update } = props\n const helpId = useId()\n const reviews = props.inStoreReviews === 'hide' ? 'hide' : 'show'\n return (\n <Stack spacing={1}>\n <FormControlLabel\n control={\n <Switch\n checked={sellOnline}\n onChange={(event) => update({ sellOnline: event.target.checked })}\n slotProps={{ input: { 'aria-describedby': helpId } }}\n />\n }\n label=\"Sell online\"\n />\n <Typography id={helpId} variant=\"body2\" color=\"text.secondary\">\n {sellOnline\n ? 'Sold on your site — its product page, product grids, search, the cart and checkout — and in any shopping feeds and marketplaces you connect, as well as in person.'\n : `In person only: sold at the register, the kiosk, the ${PLATFORM_BRAND_NAME} POS apps and on draft orders. It has no page on your site and is left out of product grids, search, the sitemap, the cart, checkout and every shopping feed.`}\n </Typography>\n {/*\n Its reviews, while it sells in person only (AGL-4060): shown on the\n site marked \"Sold in store\" by default, or kept off it. Online, a\n product's reviews are always on its own page, so the choice is not\n offered.\n */}\n {sellOnline ? null : (\n <TextField\n select\n size=\"small\"\n label=\"Reviews on your site\"\n value={reviews}\n onChange={(event) => update({ inStoreReviews: event.target.value as 'show' | 'hide' })}\n helperText={IN_STORE_REVIEWS_OPTIONS.find((option) => option.value === reviews)?.help}\n >\n {IN_STORE_REVIEWS_OPTIONS.map((option) => (\n <MenuItem key={option.value} value={option.value}>\n {option.label}\n </MenuItem>\n ))}\n </TextField>\n )}\n </Stack>\n )\n})\n\n/**\n * The product's register settings (AGL-3607): whether it is a quick key on\n * the register's first screen, and whether it sells online at all. Its\n * add-ons — the register's modifiers — are edited for every store in\n * `ProductAddOnsFields` (AGL-4100), where each group says whether the\n * website offers it too.\n *\n * Memoized on its own props like every section of this editor (AGL-3423):\n * a keystroke elsewhere in the dialog does not redraw it.\n */\nexport const ProductRegisterFields = memo(function ProductRegisterFields(\n props: ProductRegisterFieldsProps,\n) {\n const { update } = props\n return (\n <Stack spacing={2}>\n <Typography variant=\"subtitle1\" component=\"h3\">\n {'At the register'}\n </Typography>\n <FormControlLabel\n control={\n <Switch\n checked={props.posQuickKey}\n onChange={(event) => update({ posQuickKey: event.target.checked })}\n />\n }\n label=\"Quick key at the register\"\n />\n <ProductSellOnlineField sellOnline={props.sellOnline} inStoreReviews={props.inStoreReviews} update={update} />\n </Stack>\n )\n})\n"],"names":["FormControlLabel","MenuItem","Stack","Switch","TextField","Typography","PLATFORM_BRAND_NAME","memo","useId","IN_STORE_REVIEWS_OPTIONS","value","label","help","ProductSellOnlineField","props","sellOnline","update","helpId","reviews","inStoreReviews","spacing","control","checked","onChange","event","target","slotProps","input","id","variant","color","select","size","helperText","find","option","map","ProductRegisterFields","component","posQuickKey"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,gBAAgB,EAChBC,QAAQ,EACRC,KAAK,EACLC,MAAM,EACNC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,mBAAmB,QAAQ,wCAAuC;AAC3E,SAASC,IAAI,EAAEC,KAAK,QAAQ,QAAO;AAqBnC,6EAA6E,GAC7E,MAAMC,2BAAmG;IACvG;QACEC,OAAO;QACPC,OAAO;QACPC,MAAM;IACR;IACA;QACEF,OAAO;QACPC,OAAO;QACPC,MAAM;IACR;CACD;AAED;;;;CAIC,GACD,OAAO,MAAMC,uCAAyBN,KAAK,SAASM,uBAClDC,KAAkC;QAmCdL;IAjCpB,MAAM,EAAEM,UAAU,EAAEC,MAAM,EAAE,GAAGF;IAC/B,MAAMG,SAAST;IACf,MAAMU,UAAUJ,MAAMK,cAAc,KAAK,SAAS,SAAS;IAC3D,qBACE,MAACjB;QAAMkB,SAAS;;0BACd,KAACpB;gBACCqB,uBACE,KAAClB;oBACCmB,SAASP;oBACTQ,UAAU,CAACC,QAAUR,OAAO;4BAAED,YAAYS,MAAMC,MAAM,CAACH,OAAO;wBAAC;oBAC/DI,WAAW;wBAAEC,OAAO;4BAAE,oBAAoBV;wBAAO;oBAAE;;gBAGvDN,OAAM;;0BAER,KAACN;gBAAWuB,IAAIX;gBAAQY,SAAQ;gBAAQC,OAAM;0BAC3Cf,aACG,uKACA,CAAC,qDAAqD,EAAET,oBAAoB,6JAA6J,CAAC;;YAQ/OS,aAAa,qBACZ,KAACX;gBACC2B,MAAM;gBACNC,MAAK;gBACLrB,OAAM;gBACND,OAAOQ;gBACPK,UAAU,CAACC,QAAUR,OAAO;wBAAEG,gBAAgBK,MAAMC,MAAM,CAACf,KAAK;oBAAoB;gBACpFuB,UAAU,GAAExB,iCAAAA,yBAAyByB,IAAI,CAAC,CAACC,SAAWA,OAAOzB,KAAK,KAAKQ,6BAA3DT,+BAAqEG,IAAI;0BAEpFH,yBAAyB2B,GAAG,CAAC,CAACD,uBAC7B,KAAClC;wBAA4BS,OAAOyB,OAAOzB,KAAK;kCAC7CyB,OAAOxB,KAAK;uBADAwB,OAAOzB,KAAK;;;;AAQvC,GAAE;AAEF;;;;;;;;;CASC,GACD,OAAO,MAAM2B,sCAAwB9B,KAAK,SAAS8B,sBACjDvB,KAAiC;IAEjC,MAAM,EAAEE,MAAM,EAAE,GAAGF;IACnB,qBACE,MAACZ;QAAMkB,SAAS;;0BACd,KAACf;gBAAWwB,SAAQ;gBAAYS,WAAU;0BACvC;;0BAEH,KAACtC;gBACCqB,uBACE,KAAClB;oBACCmB,SAASR,MAAMyB,WAAW;oBAC1BhB,UAAU,CAACC,QAAUR,OAAO;4BAAEuB,aAAaf,MAAMC,MAAM,CAACH,OAAO;wBAAC;;gBAGpEX,OAAM;;0BAER,KAACE;gBAAuBE,YAAYD,MAAMC,UAAU;gBAAEI,gBAAgBL,MAAMK,cAAc;gBAAEH,QAAQA;;;;AAG1G,GAAE"}
|
|
@@ -116,6 +116,12 @@ export function ProductsHubZone(props) {
|
|
|
116
116
|
return true;
|
|
117
117
|
});
|
|
118
118
|
if (!fresh.length) return [];
|
|
119
|
+
// A proposal whose options make more variants than a product holds is
|
|
120
|
+
// refused by name (AGL-4100), never built short of its values.
|
|
121
|
+
for (const proposal of fresh){
|
|
122
|
+
const tooMany = CommerceModel.variantMatrixProblem(proposal.options);
|
|
123
|
+
if (tooMany) throw new Error(`“${proposal.name}”: ${tooMany}`);
|
|
124
|
+
}
|
|
119
125
|
const room = roomFor(fresh.length);
|
|
120
126
|
if (!room) throw new Error(PLAN_LOADING_COPY);
|
|
121
127
|
if (!room.allowed) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/products-hub-zone.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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n PRODUCTS_HUB_ZONE,\n type ConsoleProductCopyValues,\n type ConsoleProductSummary,\n type ConsoleProductsHubZoneProps,\n type ConsoleProposedDiscount,\n type ConsoleProposedProduct,\n} from './product-zones'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n useFirestore,\n useHostResourceApi,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n commitWithSiteWideEntry,\n settleSiteWideChange,\n} from '@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change'\nimport {\n collection,\n doc,\n getDocs,\n limit,\n query,\n runTransaction,\n setDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo } from 'react'\nimport * as CommerceModel from '../../model'\nimport { productSlugLedger } from './product-slugs'\nimport { readSmartCollections } from './smart-collections'\n\n/**\n * The products hub's zone (AGL-2916): the page of catalog rows the hub's table\n * shows, and the writes a widget there may ask the hub to make. A widget proposes; these are the\n * hub's writes, each through the door the hub's own controls use.\n *\n * - **Copy onto a saved product** is a transaction over the product as the\n * store holds it now, never a row the table was seeded with: it changes\n * only the fields the copy names, and renames options through\n * `renameProductOptions`, so a sale or a colleague's edit since the table\n * loaded is kept.\n * - **A proposed product** is created through the quota-enforcing resources\n * API, as Add product and Import create one, after the hub's own allowance\n * check for the batch. It is a draft at its suggested price, else the\n * default price (`proposedProductPriceUsd`, `defaultPricedProductDraft`,\n * AGL-3676), for the owner to change.\n * - **A category** is created as the catalog card creates one.\n * - **A discount** is created switched off (`switchedOffDiscount`), for a\n * person to switch on from the discounts card.\n *\n * Each create passes over what the site already has — a product of the same\n * name, a category of the same name, a discount with the same code or, with no\n * code, the same name — so a proposal applied twice, by anyone, creates\n * nothing twice. Each check is a read made when a person applies, never while\n * the hub is open, and reaches past the page the hub shows: a product is\n * looked up by its search key, and its slug by the slugs it could take\n * (`productSlugLedger`, AGL-3321).\n */\n\ntype ProductRow = CommerceModel.HostProduct & { $id: string }\n\n/** The hub's allowance check for `count` more products: `null` while the plan loads. */\nexport type ProductsRoomCheck = (count: number) => { allowed: boolean; limit: number } | null\n\nexport interface ProductsHubZoneProps {\n hostId: string\n /**\n * The page of catalog rows the hub's table shows, in its order — the\n * products its filters and search matched, one page at a time (AGL-3321).\n * Never the catalog: nothing here decides what exists from these rows.\n */\n products: readonly ProductRow[]\n roomFor: ProductsRoomCheck\n lastImport: ConsoleProductsHubZoneProps['lastImport']\n /** Called once a create has moved the catalog's count. */\n onCreated: () => void\n}\n\nexport const PRODUCT_GONE_COPY = 'This product is no longer in the catalog.'\nexport const PLAN_LOADING_COPY = 'Your plan is still loading. Try again in a moment.'\n\n/** How many of the site's categories the duplicate check reads. */\nconst CATEGORY_NAMES_READ = 250\n\n/** How a name is compared for a duplicate: case and spacing aside. */\nconst nameKey = (name: string) => name.replace(/\\s+/g, ' ').trim().toLowerCase()\n\n/** The most values one Firestore `in` clause takes. */\nconst IN_CLAUSE_MAX = 10\n\n/** No slug taken: a draft's own base slug, before the store is asked. */\nconst NO_SLUGS: ReadonlySet<string> = new Set()\n\nexport function ProductsHubZone(props: ProductsHubZoneProps) {\n const { hostId, products, roomFor, lastImport, onCreated } = props\n const Slot = useConsoleWidgetSlot()\n const firestore = useFirestore()\n const { data: user } = useUser()\n const createHostResource = useHostResourceApi()\n\n const summaries = useMemo<ConsoleProductSummary[]>(\n () =>\n products.map((product) => ({\n id: product.$id,\n name: product.name,\n status: product.status,\n priceMissing: CommerceModel.productPriceMissing(product),\n hasPhoto: Boolean(product.mediaUrls?.[0] ?? product.imageUrl),\n })),\n [products],\n )\n\n const applyProductCopy = useCallback(\n async (productId: string, values: ConsoleProductCopyValues) => {\n const ref = doc(firestore, 'hosts', hostId, 'products', productId)\n const smartCollections = await readSmartCollections(firestore, hostId)\n // The product page renders the copy with no publish step, so the\n // transaction carries the site's cache drop with it (AGL-3386).\n const entry = await commitWithSiteWideEntry(hostId, (stage) =>\n runTransaction(firestore, async (transaction) => {\n const snapshot = await transaction.get(ref)\n const stored = snapshot.exists() ? (snapshot.data() as CommerceModel.HostProduct) : null\n if (!stored || stored.deletedAt) throw new Error(PRODUCT_GONE_COPY)\n const lifted = CommerceModel.liftLegacyProduct(stored)\n const patch = CommerceModel.productCopyPatch(lifted, values)\n if (!Object.keys(patch).length) return\n // Tags and categories may move, and with them its smart collections\n // (AGL-3321).\n // Written only when it moved, so a copy that changes no rule input\n // writes only the copy.\n const collectionIds = CommerceModel.productCollectionIds(\n { ...lifted, ...patch },\n smartCollections,\n )\n const held = stored.collectionIds ?? []\n const moved =\n held.length !== collectionIds.length ||\n collectionIds.some((id, at) => held[at] !== id)\n transaction.update(ref, {\n ...patch,\n ...(moved ? { collectionIds } : {}),\n updatedAtMs: Date.now(),\n updatedAt: Timestamp.now(),\n })\n stage?.(transaction, firestore)\n }),\n )\n settleSiteWideChange({ user, hostId, entry })\n },\n [firestore, hostId, user],\n )\n\n const createProductDrafts = useCallback(\n async (proposals: readonly ConsoleProposedProduct[]) => {\n // What exists is asked of the store, never read off the page on screen.\n const held = new Set<string>()\n const searchKeys = [\n ...new Set(proposals.map((proposal) => CommerceModel.productSearchFields({ name: proposal.name.trim() }).nameLower)),\n ].filter(Boolean)\n const productsRef = collection(firestore, 'hosts', hostId, 'products')\n const stored = await Promise.all(\n Array.from({ length: Math.ceil(searchKeys.length / IN_CLAUSE_MAX) }, (_, index) =>\n getDocs(query(productsRef, where('nameLower', 'in', searchKeys.slice(index * IN_CLAUSE_MAX, (index + 1) * IN_CLAUSE_MAX)))),\n ),\n )\n for (const read of stored) {\n for (const existing of read.docs) {\n if (!existing.get('deletedAt')) held.add(nameKey(String(existing.get('name') ?? '')))\n }\n }\n const fresh = proposals.filter((proposal) => {\n const key = nameKey(proposal.name)\n if (!key || held.has(key)) return false\n held.add(key)\n return true\n })\n if (!fresh.length) return []\n const room = roomFor(fresh.length)\n if (!room) throw new Error(PLAN_LOADING_COPY)\n if (!room.allowed) {\n throw new Error(\n `These products need ${fresh.length} product slots — your plan allows ` +\n `${room.limit}. See Billing to upgrade.`,\n )\n }\n // Each draft's slug is asked of the store: the base slug its name makes,\n // then `-2`, `-3`, … until one no product holds, live or deleted.\n const slugs = productSlugLedger(firestore, hostId)\n const created: string[] = []\n try {\n // At its suggested price, held to range here, else the default\n // (Zach, 2026-10-12). Only the proposal's own fields reach the draft.\n const drafts = fresh.map(({ priceUsd, ...proposal }) =>\n CommerceModel.defaultPricedProductDraft(\n proposal,\n NO_SLUGS,\n Date.now(),\n CommerceModel.proposedProductPriceUsd(priceUsd),\n ),\n )\n await slugs.ask(drafts.map((draft) => draft.slug))\n const smartCollections = await readSmartCollections(firestore, hostId)\n for (const draft of drafts) {\n const slug = await slugs.claim(draft.slug)\n const collectionIds = CommerceModel.productCollectionIds(draft, smartCollections)\n const { id } = await createHostResource({\n hostId,\n resource: 'product',\n data: { ...draft, slug, collectionIds },\n })\n created.push(id)\n }\n } finally {\n if (created.length) onCreated()\n }\n return created\n },\n [roomFor, createHostResource, firestore, hostId, onCreated],\n )\n\n const createCategories = useCallback(\n async (names: readonly string[]) => {\n const existing = await getDocs(\n query(collection(firestore, 'hosts', hostId, 'productCategories'), limit(CATEGORY_NAMES_READ)),\n )\n const held = new Set(existing.docs.map((category) => nameKey(String(category.get('name') ?? ''))))\n let created = 0\n for (const raw of names) {\n const name = raw.trim().slice(0, 80)\n if (!name || held.has(nameKey(name))) continue\n held.add(nameKey(name))\n await setDoc(doc(firestore, 'hosts', hostId, 'productCategories', createResourceUid()), {\n name,\n slug: CommerceModel.commerceSlug(name),\n parentId: null,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n })\n created += 1\n }\n return created\n },\n [firestore, hostId],\n )\n\n const createDiscountDrafts = useCallback(\n async (discounts: readonly ConsoleProposedDiscount[]) => {\n const drafts = discounts\n .map((proposal) => CommerceModel.switchedOffDiscount(proposal))\n .filter((discount): discount is CommerceModel.StoredDiscount => discount !== null)\n if (!drafts.length) return 0\n const codes = [...new Set(drafts.map((discount) => discount.code).filter((code): code is string => Boolean(code)))]\n // A stored name is compared as typed, and the query matches it exactly.\n const names = [...new Set(drafts.filter((discount) => !discount.code).map((discount) => discount.name ?? ''))]\n const discountsRef = collection(firestore, 'hosts', hostId, 'discounts')\n const [byCode, byName] = await Promise.all([\n codes.length ? getDocs(query(discountsRef, where('code', 'in', codes.slice(0, 10)))) : null,\n names.length ? getDocs(query(discountsRef, where('name', 'in', names.slice(0, 10)))) : null,\n ])\n const heldCodes = new Set((byCode?.docs ?? []).map((existing) => String(existing.get('code'))))\n const heldNames = new Set((byName?.docs ?? []).map((existing) => nameKey(String(existing.get('name') ?? ''))))\n let created = 0\n for (const discount of drafts) {\n const name = nameKey(discount.name ?? '')\n if (discount.code ? heldCodes.has(discount.code) : heldNames.has(name)) continue\n if (discount.code) heldCodes.add(discount.code)\n else heldNames.add(name)\n await setDoc(doc(firestore, 'hosts', hostId, 'discounts', createResourceUid()), discount)\n created += 1\n }\n return created\n },\n [firestore, hostId],\n )\n\n if (!Slot) return null\n return (\n <Slot\n slot={PRODUCTS_HUB_ZONE.id}\n hostId={hostId}\n orgId={undefined}\n products={summaries}\n lastImport={lastImport}\n applyProductCopy={applyProductCopy}\n createProductDrafts={createProductDrafts}\n createCategories={createCategories}\n createDiscountDrafts={createDiscountDrafts}\n />\n )\n}\nProductsHubZone.displayName = 'ProductsHubZone'\n\nexport default ProductsHubZone\n"],"names":["createResourceUid","useConsoleWidgetSlot","PRODUCTS_HUB_ZONE","Timestamp","useFirestore","useHostResourceApi","useUser","commitWithSiteWideEntry","settleSiteWideChange","collection","doc","getDocs","limit","query","runTransaction","setDoc","where","useCallback","useMemo","CommerceModel","productSlugLedger","readSmartCollections","PRODUCT_GONE_COPY","PLAN_LOADING_COPY","CATEGORY_NAMES_READ","nameKey","name","replace","trim","toLowerCase","IN_CLAUSE_MAX","NO_SLUGS","Set","ProductsHubZone","props","hostId","products","roomFor","lastImport","onCreated","Slot","firestore","data","user","createHostResource","summaries","map","product","id","$id","status","priceMissing","productPriceMissing","hasPhoto","Boolean","mediaUrls","imageUrl","applyProductCopy","productId","values","ref","smartCollections","entry","stage","transaction","stored","snapshot","get","exists","deletedAt","Error","lifted","liftLegacyProduct","patch","productCopyPatch","Object","keys","length","collectionIds","productCollectionIds","held","moved","some","at","update","updatedAtMs","Date","now","updatedAt","createProductDrafts","proposals","searchKeys","proposal","productSearchFields","nameLower","filter","productsRef","Promise","all","Array","from","Math","ceil","_","index","slice","read","existing","docs","add","String","fresh","key","has","room","allowed","slugs","created","drafts","priceUsd","defaultPricedProductDraft","proposedProductPriceUsd","ask","draft","slug","claim","resource","push","createCategories","names","category","raw","commerceSlug","parentId","createdAt","createDiscountDrafts","discounts","switchedOffDiscount","discount","codes","code","discountsRef","byCode","byName","heldCodes","heldNames","slot","orgId","undefined","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/products-hub-zone.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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n PRODUCTS_HUB_ZONE,\n type ConsoleProductCopyValues,\n type ConsoleProductSummary,\n type ConsoleProductsHubZoneProps,\n type ConsoleProposedDiscount,\n type ConsoleProposedProduct,\n} from './product-zones'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n useFirestore,\n useHostResourceApi,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n commitWithSiteWideEntry,\n settleSiteWideChange,\n} from '@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change'\nimport {\n collection,\n doc,\n getDocs,\n limit,\n query,\n runTransaction,\n setDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo } from 'react'\nimport * as CommerceModel from '../../model'\nimport { productSlugLedger } from './product-slugs'\nimport { readSmartCollections } from './smart-collections'\n\n/**\n * The products hub's zone (AGL-2916): the page of catalog rows the hub's table\n * shows, and the writes a widget there may ask the hub to make. A widget proposes; these are the\n * hub's writes, each through the door the hub's own controls use.\n *\n * - **Copy onto a saved product** is a transaction over the product as the\n * store holds it now, never a row the table was seeded with: it changes\n * only the fields the copy names, and renames options through\n * `renameProductOptions`, so a sale or a colleague's edit since the table\n * loaded is kept.\n * - **A proposed product** is created through the quota-enforcing resources\n * API, as Add product and Import create one, after the hub's own allowance\n * check for the batch. It is a draft at its suggested price, else the\n * default price (`proposedProductPriceUsd`, `defaultPricedProductDraft`,\n * AGL-3676), for the owner to change.\n * - **A category** is created as the catalog card creates one.\n * - **A discount** is created switched off (`switchedOffDiscount`), for a\n * person to switch on from the discounts card.\n *\n * Each create passes over what the site already has — a product of the same\n * name, a category of the same name, a discount with the same code or, with no\n * code, the same name — so a proposal applied twice, by anyone, creates\n * nothing twice. Each check is a read made when a person applies, never while\n * the hub is open, and reaches past the page the hub shows: a product is\n * looked up by its search key, and its slug by the slugs it could take\n * (`productSlugLedger`, AGL-3321).\n */\n\ntype ProductRow = CommerceModel.HostProduct & { $id: string }\n\n/** The hub's allowance check for `count` more products: `null` while the plan loads. */\nexport type ProductsRoomCheck = (count: number) => { allowed: boolean; limit: number } | null\n\nexport interface ProductsHubZoneProps {\n hostId: string\n /**\n * The page of catalog rows the hub's table shows, in its order — the\n * products its filters and search matched, one page at a time (AGL-3321).\n * Never the catalog: nothing here decides what exists from these rows.\n */\n products: readonly ProductRow[]\n roomFor: ProductsRoomCheck\n lastImport: ConsoleProductsHubZoneProps['lastImport']\n /** Called once a create has moved the catalog's count. */\n onCreated: () => void\n}\n\nexport const PRODUCT_GONE_COPY = 'This product is no longer in the catalog.'\nexport const PLAN_LOADING_COPY = 'Your plan is still loading. Try again in a moment.'\n\n/** How many of the site's categories the duplicate check reads. */\nconst CATEGORY_NAMES_READ = 250\n\n/** How a name is compared for a duplicate: case and spacing aside. */\nconst nameKey = (name: string) => name.replace(/\\s+/g, ' ').trim().toLowerCase()\n\n/** The most values one Firestore `in` clause takes. */\nconst IN_CLAUSE_MAX = 10\n\n/** No slug taken: a draft's own base slug, before the store is asked. */\nconst NO_SLUGS: ReadonlySet<string> = new Set()\n\nexport function ProductsHubZone(props: ProductsHubZoneProps) {\n const { hostId, products, roomFor, lastImport, onCreated } = props\n const Slot = useConsoleWidgetSlot()\n const firestore = useFirestore()\n const { data: user } = useUser()\n const createHostResource = useHostResourceApi()\n\n const summaries = useMemo<ConsoleProductSummary[]>(\n () =>\n products.map((product) => ({\n id: product.$id,\n name: product.name,\n status: product.status,\n priceMissing: CommerceModel.productPriceMissing(product),\n hasPhoto: Boolean(product.mediaUrls?.[0] ?? product.imageUrl),\n })),\n [products],\n )\n\n const applyProductCopy = useCallback(\n async (productId: string, values: ConsoleProductCopyValues) => {\n const ref = doc(firestore, 'hosts', hostId, 'products', productId)\n const smartCollections = await readSmartCollections(firestore, hostId)\n // The product page renders the copy with no publish step, so the\n // transaction carries the site's cache drop with it (AGL-3386).\n const entry = await commitWithSiteWideEntry(hostId, (stage) =>\n runTransaction(firestore, async (transaction) => {\n const snapshot = await transaction.get(ref)\n const stored = snapshot.exists() ? (snapshot.data() as CommerceModel.HostProduct) : null\n if (!stored || stored.deletedAt) throw new Error(PRODUCT_GONE_COPY)\n const lifted = CommerceModel.liftLegacyProduct(stored)\n const patch = CommerceModel.productCopyPatch(lifted, values)\n if (!Object.keys(patch).length) return\n // Tags and categories may move, and with them its smart collections\n // (AGL-3321).\n // Written only when it moved, so a copy that changes no rule input\n // writes only the copy.\n const collectionIds = CommerceModel.productCollectionIds(\n { ...lifted, ...patch },\n smartCollections,\n )\n const held = stored.collectionIds ?? []\n const moved =\n held.length !== collectionIds.length ||\n collectionIds.some((id, at) => held[at] !== id)\n transaction.update(ref, {\n ...patch,\n ...(moved ? { collectionIds } : {}),\n updatedAtMs: Date.now(),\n updatedAt: Timestamp.now(),\n })\n stage?.(transaction, firestore)\n }),\n )\n settleSiteWideChange({ user, hostId, entry })\n },\n [firestore, hostId, user],\n )\n\n const createProductDrafts = useCallback(\n async (proposals: readonly ConsoleProposedProduct[]) => {\n // What exists is asked of the store, never read off the page on screen.\n const held = new Set<string>()\n const searchKeys = [\n ...new Set(proposals.map((proposal) => CommerceModel.productSearchFields({ name: proposal.name.trim() }).nameLower)),\n ].filter(Boolean)\n const productsRef = collection(firestore, 'hosts', hostId, 'products')\n const stored = await Promise.all(\n Array.from({ length: Math.ceil(searchKeys.length / IN_CLAUSE_MAX) }, (_, index) =>\n getDocs(query(productsRef, where('nameLower', 'in', searchKeys.slice(index * IN_CLAUSE_MAX, (index + 1) * IN_CLAUSE_MAX)))),\n ),\n )\n for (const read of stored) {\n for (const existing of read.docs) {\n if (!existing.get('deletedAt')) held.add(nameKey(String(existing.get('name') ?? '')))\n }\n }\n const fresh = proposals.filter((proposal) => {\n const key = nameKey(proposal.name)\n if (!key || held.has(key)) return false\n held.add(key)\n return true\n })\n if (!fresh.length) return []\n // A proposal whose options make more variants than a product holds is\n // refused by name (AGL-4100), never built short of its values.\n for (const proposal of fresh) {\n const tooMany = CommerceModel.variantMatrixProblem(proposal.options)\n if (tooMany) throw new Error(`“${proposal.name}”: ${tooMany}`)\n }\n const room = roomFor(fresh.length)\n if (!room) throw new Error(PLAN_LOADING_COPY)\n if (!room.allowed) {\n throw new Error(\n `These products need ${fresh.length} product slots — your plan allows ` +\n `${room.limit}. See Billing to upgrade.`,\n )\n }\n // Each draft's slug is asked of the store: the base slug its name makes,\n // then `-2`, `-3`, … until one no product holds, live or deleted.\n const slugs = productSlugLedger(firestore, hostId)\n const created: string[] = []\n try {\n // At its suggested price, held to range here, else the default\n // (Zach, 2026-10-12). Only the proposal's own fields reach the draft.\n const drafts = fresh.map(({ priceUsd, ...proposal }) =>\n CommerceModel.defaultPricedProductDraft(\n proposal,\n NO_SLUGS,\n Date.now(),\n CommerceModel.proposedProductPriceUsd(priceUsd),\n ),\n )\n await slugs.ask(drafts.map((draft) => draft.slug))\n const smartCollections = await readSmartCollections(firestore, hostId)\n for (const draft of drafts) {\n const slug = await slugs.claim(draft.slug)\n const collectionIds = CommerceModel.productCollectionIds(draft, smartCollections)\n const { id } = await createHostResource({\n hostId,\n resource: 'product',\n data: { ...draft, slug, collectionIds },\n })\n created.push(id)\n }\n } finally {\n if (created.length) onCreated()\n }\n return created\n },\n [roomFor, createHostResource, firestore, hostId, onCreated],\n )\n\n const createCategories = useCallback(\n async (names: readonly string[]) => {\n const existing = await getDocs(\n query(collection(firestore, 'hosts', hostId, 'productCategories'), limit(CATEGORY_NAMES_READ)),\n )\n const held = new Set(existing.docs.map((category) => nameKey(String(category.get('name') ?? ''))))\n let created = 0\n for (const raw of names) {\n const name = raw.trim().slice(0, 80)\n if (!name || held.has(nameKey(name))) continue\n held.add(nameKey(name))\n await setDoc(doc(firestore, 'hosts', hostId, 'productCategories', createResourceUid()), {\n name,\n slug: CommerceModel.commerceSlug(name),\n parentId: null,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n })\n created += 1\n }\n return created\n },\n [firestore, hostId],\n )\n\n const createDiscountDrafts = useCallback(\n async (discounts: readonly ConsoleProposedDiscount[]) => {\n const drafts = discounts\n .map((proposal) => CommerceModel.switchedOffDiscount(proposal))\n .filter((discount): discount is CommerceModel.StoredDiscount => discount !== null)\n if (!drafts.length) return 0\n const codes = [...new Set(drafts.map((discount) => discount.code).filter((code): code is string => Boolean(code)))]\n // A stored name is compared as typed, and the query matches it exactly.\n const names = [...new Set(drafts.filter((discount) => !discount.code).map((discount) => discount.name ?? ''))]\n const discountsRef = collection(firestore, 'hosts', hostId, 'discounts')\n const [byCode, byName] = await Promise.all([\n codes.length ? getDocs(query(discountsRef, where('code', 'in', codes.slice(0, 10)))) : null,\n names.length ? getDocs(query(discountsRef, where('name', 'in', names.slice(0, 10)))) : null,\n ])\n const heldCodes = new Set((byCode?.docs ?? []).map((existing) => String(existing.get('code'))))\n const heldNames = new Set((byName?.docs ?? []).map((existing) => nameKey(String(existing.get('name') ?? ''))))\n let created = 0\n for (const discount of drafts) {\n const name = nameKey(discount.name ?? '')\n if (discount.code ? heldCodes.has(discount.code) : heldNames.has(name)) continue\n if (discount.code) heldCodes.add(discount.code)\n else heldNames.add(name)\n await setDoc(doc(firestore, 'hosts', hostId, 'discounts', createResourceUid()), discount)\n created += 1\n }\n return created\n },\n [firestore, hostId],\n )\n\n if (!Slot) return null\n return (\n <Slot\n slot={PRODUCTS_HUB_ZONE.id}\n hostId={hostId}\n orgId={undefined}\n products={summaries}\n lastImport={lastImport}\n applyProductCopy={applyProductCopy}\n createProductDrafts={createProductDrafts}\n createCategories={createCategories}\n createDiscountDrafts={createDiscountDrafts}\n />\n )\n}\nProductsHubZone.displayName = 'ProductsHubZone'\n\nexport default ProductsHubZone\n"],"names":["createResourceUid","useConsoleWidgetSlot","PRODUCTS_HUB_ZONE","Timestamp","useFirestore","useHostResourceApi","useUser","commitWithSiteWideEntry","settleSiteWideChange","collection","doc","getDocs","limit","query","runTransaction","setDoc","where","useCallback","useMemo","CommerceModel","productSlugLedger","readSmartCollections","PRODUCT_GONE_COPY","PLAN_LOADING_COPY","CATEGORY_NAMES_READ","nameKey","name","replace","trim","toLowerCase","IN_CLAUSE_MAX","NO_SLUGS","Set","ProductsHubZone","props","hostId","products","roomFor","lastImport","onCreated","Slot","firestore","data","user","createHostResource","summaries","map","product","id","$id","status","priceMissing","productPriceMissing","hasPhoto","Boolean","mediaUrls","imageUrl","applyProductCopy","productId","values","ref","smartCollections","entry","stage","transaction","stored","snapshot","get","exists","deletedAt","Error","lifted","liftLegacyProduct","patch","productCopyPatch","Object","keys","length","collectionIds","productCollectionIds","held","moved","some","at","update","updatedAtMs","Date","now","updatedAt","createProductDrafts","proposals","searchKeys","proposal","productSearchFields","nameLower","filter","productsRef","Promise","all","Array","from","Math","ceil","_","index","slice","read","existing","docs","add","String","fresh","key","has","tooMany","variantMatrixProblem","options","room","allowed","slugs","created","drafts","priceUsd","defaultPricedProductDraft","proposedProductPriceUsd","ask","draft","slug","claim","resource","push","createCategories","names","category","raw","commerceSlug","parentId","createdAt","createDiscountDrafts","discounts","switchedOffDiscount","discount","codes","code","discountsRef","byCode","byName","heldCodes","heldNames","slot","orgId","undefined","displayName"],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ import * as CommerceModel from '../../model';
|
|
|
18
18
|
export interface QuotePayLinkSectionProps {
|
|
19
19
|
hostId: string;
|
|
20
20
|
quoteId: string;
|
|
21
|
-
quote: Pick<CommerceModel.HostOrder, 'status' | 'channel' | 'number'>;
|
|
21
|
+
quote: Pick<CommerceModel.HostOrder, 'status' | 'channel' | 'number' | 'quote'>;
|
|
22
22
|
/** Holds "Send quotes and invoices to customers" (`commerce.quotes.send`). */
|
|
23
23
|
canSend: boolean;
|
|
24
24
|
/** The editor holds changes not saved yet, which the link does not show. */
|
|
@@ -30,6 +30,13 @@ export interface QuotePayLinkSectionProps {
|
|
|
30
30
|
* button. "Copy pay link" asks the server for it (it is signed there) and puts
|
|
31
31
|
* it on the clipboard and on screen; "Replace link" makes a new one and the
|
|
32
32
|
* one sent before stops opening the quote.
|
|
33
|
+
*
|
|
34
|
+
* The link shown is kept with the version it was made at, and drops as soon
|
|
35
|
+
* as the quote's stored version (`quote.linkVersion`) moves past it: a
|
|
36
|
+
* revision's first email and Replace link (here or anywhere else) both move
|
|
37
|
+
* it, and the link from before then opens only "This quote was updated". A
|
|
38
|
+
* link already on screen is fetched again at the new version; Copy pay link
|
|
39
|
+
* never copies one older than the quote's.
|
|
33
40
|
*/
|
|
34
41
|
export declare function QuotePayLinkSection(props: QuotePayLinkSectionProps): import("react").JSX.Element;
|
|
35
42
|
export declare namespace QuotePayLinkSection {
|
|
@@ -22,7 +22,7 @@ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
|
22
22
|
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
23
23
|
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
24
24
|
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Stack, TextField, Typography } from "@mui/material";
|
|
25
|
-
import { useCallback, useState } from "react";
|
|
25
|
+
import { useCallback, useEffect, useState } from "react";
|
|
26
26
|
const SEND_REFUSAL = commerceSitePermissionRefusal('commerce.quotes.send');
|
|
27
27
|
/**
|
|
28
28
|
* A quote's pay link (AGL-4094): the address the business sends the
|
|
@@ -30,14 +30,24 @@ const SEND_REFUSAL = commerceSitePermissionRefusal('commerce.quotes.send');
|
|
|
30
30
|
* button. "Copy pay link" asks the server for it (it is signed there) and puts
|
|
31
31
|
* it on the clipboard and on screen; "Replace link" makes a new one and the
|
|
32
32
|
* one sent before stops opening the quote.
|
|
33
|
+
*
|
|
34
|
+
* The link shown is kept with the version it was made at, and drops as soon
|
|
35
|
+
* as the quote's stored version (`quote.linkVersion`) moves past it: a
|
|
36
|
+
* revision's first email and Replace link (here or anywhere else) both move
|
|
37
|
+
* it, and the link from before then opens only "This quote was updated". A
|
|
38
|
+
* link already on screen is fetched again at the new version; Copy pay link
|
|
39
|
+
* never copies one older than the quote's.
|
|
33
40
|
*/ export function QuotePayLinkSection(props) {
|
|
34
41
|
const { hostId, quoteId, quote, canSend, edited } = props;
|
|
35
42
|
const { data: user } = useUser();
|
|
36
43
|
const { enqueueSnackbar } = useSnackbar();
|
|
37
|
-
const [
|
|
44
|
+
const [signed, setSigned] = useState(null);
|
|
38
45
|
const [busy, setBusy] = useState(false);
|
|
39
46
|
const [confirmingReplace, setConfirmingReplace] = useState(false);
|
|
40
47
|
const unpaid = CommerceModel.isUnpaidQuote(quote);
|
|
48
|
+
const version = CommerceModel.quoteLinkVersion(quote.quote);
|
|
49
|
+
// A link the server made at a version the stored quote has not caught up to yet is the newest one.
|
|
50
|
+
const url = signed && signed.version >= version ? signed.url : '';
|
|
41
51
|
const ask = useCallback(async (action)=>{
|
|
42
52
|
setBusy(true);
|
|
43
53
|
try {
|
|
@@ -61,7 +71,11 @@ const SEND_REFUSAL = commerceSitePermissionRefusal('commerce.quotes.send');
|
|
|
61
71
|
});
|
|
62
72
|
return null;
|
|
63
73
|
}
|
|
64
|
-
|
|
74
|
+
const made = Math.floor(Number(payload.linkVersion));
|
|
75
|
+
setSigned({
|
|
76
|
+
url: String(payload.url),
|
|
77
|
+
version: Number.isFinite(made) && made >= 1 ? made : version
|
|
78
|
+
});
|
|
65
79
|
return String(payload.url);
|
|
66
80
|
} finally{
|
|
67
81
|
setBusy(false);
|
|
@@ -70,8 +84,20 @@ const SEND_REFUSAL = commerceSitePermissionRefusal('commerce.quotes.send');
|
|
|
70
84
|
user,
|
|
71
85
|
hostId,
|
|
72
86
|
quoteId,
|
|
87
|
+
version,
|
|
73
88
|
enqueueSnackbar
|
|
74
89
|
]);
|
|
90
|
+
// The quote's link moved on (a revision was emailed, or the link replaced):
|
|
91
|
+
// the one on screen opens nothing now, so fetch the current one in its place.
|
|
92
|
+
const stale = signed !== null && signed.version < version;
|
|
93
|
+
useEffect(()=>{
|
|
94
|
+
if (stale) void ask('pay-link');
|
|
95
|
+
// Once per version the quote moves to; `ask` changing does not ask again.
|
|
96
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
97
|
+
}, [
|
|
98
|
+
stale,
|
|
99
|
+
version
|
|
100
|
+
]);
|
|
75
101
|
const copy = useCallback(async (link, done)=>{
|
|
76
102
|
try {
|
|
77
103
|
await navigator.clipboard.writeText(link);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/quote-pay-link.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\n'use client'\n\nimport * as CommerceModel from '../../model'\nimport { commerceSitePermissionRefusal } from '../../model/plugin-permissions'\nimport { BlockedControl } from '@aglyn/shared-ui-jsx/components/blocked-control.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useState } from 'react'\n\nconst SEND_REFUSAL = commerceSitePermissionRefusal('commerce.quotes.send')\n\nexport interface QuotePayLinkSectionProps {\n hostId: string\n quoteId: string\n quote: Pick<CommerceModel.HostOrder, 'status' | 'channel' | 'number'>\n /** Holds \"Send quotes and invoices to customers\" (`commerce.quotes.send`). */\n canSend: boolean\n /** The editor holds changes not saved yet, which the link does not show. */\n edited: boolean\n}\n\n/**\n * A quote's pay link (AGL-4094): the address the business sends the\n * customer, which opens the quote on the business's own site with a Pay\n * button. \"Copy pay link\" asks the server for it (it is signed there) and puts\n * it on the clipboard and on screen; \"Replace link\" makes a new one and the\n * one sent before stops opening the quote.\n */\nexport function QuotePayLinkSection(props: QuotePayLinkSectionProps) {\n const { hostId, quoteId, quote, canSend, edited } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const [url, setUrl] = useState('')\n const [busy, setBusy] = useState(false)\n const [confirmingReplace, setConfirmingReplace] = useState(false)\n const unpaid = CommerceModel.isUnpaidQuote(quote)\n\n const ask = useCallback(\n async (action: 'pay-link' | 'replace-link'): Promise<string | null> => {\n setBusy(true)\n try {\n const response = await authorizedFetch(user, '/api/commerce/quote', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ action, hostId, quoteId }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok || !payload?.url) {\n enqueueSnackbar(payload?.error ?? 'The pay link could not be made', { variant: 'error', allowDuplicate: true })\n return null\n }\n setUrl(String(payload.url))\n return String(payload.url)\n } finally {\n setBusy(false)\n }\n },\n [user, hostId, quoteId, enqueueSnackbar],\n )\n\n const copy = useCallback(\n async (link: string, done: string) => {\n try {\n await navigator.clipboard.writeText(link)\n enqueueSnackbar(done, { variant: 'success', persist: false })\n } catch {\n // The link is on screen to copy by hand when the clipboard is refused.\n enqueueSnackbar('Copy the link below to send it', { variant: 'info', persist: false })\n }\n },\n [enqueueSnackbar],\n )\n\n const copyLink = useCallback(async () => {\n const link = url || (await ask('pay-link'))\n if (link) await copy(link, 'Pay link copied')\n }, [url, ask, copy])\n\n const replaceLink = useCallback(async () => {\n const link = await ask('replace-link')\n setConfirmingReplace(false)\n if (link) await copy(link, 'New pay link copied. The earlier link no longer opens the quote.')\n }, [ask, copy])\n\n return (\n <Stack spacing={1.5}>\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Pay link'}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {unpaid\n ? 'Send this link to the customer. It opens the quote on your site, where they can pay it.'\n : 'The link opens the paid quote and the customer’s order.'}\n </Typography>\n {edited && unpaid ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Save your changes first: the link opens the quote as it was last saved.'}\n </Typography>\n ) : null}\n </Stack>\n {url ? (\n <TextField\n label=\"Pay link\"\n value={url}\n size=\"small\"\n fullWidth\n slotProps={{ htmlInput: { readOnly: true }, inputLabel: { shrink: true } }}\n onFocus={(event) => event.target.select()}\n />\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ flexWrap: 'wrap', rowGap: 1 }}>\n <BlockedControl blocked={!canSend} reason={SEND_REFUSAL}>\n <Button variant=\"outlined\" size=\"small\" disabled={busy} onClick={() => void copyLink()}>\n {'Copy pay link'}\n </Button>\n </BlockedControl>\n {unpaid ? (\n <BlockedControl blocked={!canSend} reason={SEND_REFUSAL}>\n <Button size=\"small\" disabled={busy} onClick={() => setConfirmingReplace(true)}>\n {'Replace link'}\n </Button>\n </BlockedControl>\n ) : null}\n </Stack>\n <Dialog open={confirmingReplace} onClose={() => setConfirmingReplace(false)} maxWidth=\"xs\" fullWidth>\n <DialogTitle>{'Replace the pay link?'}</DialogTitle>\n <DialogContent>\n <DialogContentText>\n {'The link you sent before will stop working: it will tell the customer the quote was updated, ' +\n 'and show nothing of it. Send them the new link.'}\n </DialogContentText>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setConfirmingReplace(false)} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" disabled={busy} onClick={() => void replaceLink()}>\n {'Replace link'}\n </Button>\n </DialogActions>\n </Dialog>\n </Stack>\n )\n}\nQuotePayLinkSection.displayName = 'QuotePayLinkSection'\n\nexport default QuotePayLinkSection\n"],"names":["CommerceModel","commerceSitePermissionRefusal","BlockedControl","useSnackbar","authorizedFetch","useUser","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","TextField","Typography","useCallback","useState","SEND_REFUSAL","QuotePayLinkSection","props","hostId","quoteId","quote","canSend","edited","data","user","enqueueSnackbar","url","setUrl","busy","setBusy","confirmingReplace","setConfirmingReplace","unpaid","isUnpaidQuote","ask","action","response","method","headers","body","JSON","stringify","payload","json","catch","ok","error","variant","allowDuplicate","String","copy","link","done","navigator","clipboard","writeText","persist","copyLink","replaceLink","spacing","color","label","value","size","fullWidth","slotProps","htmlInput","readOnly","inputLabel","shrink","onFocus","event","target","select","direction","sx","flexWrap","rowGap","blocked","reason","disabled","onClick","open","onClose","maxWidth","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/quote-pay-link.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\n'use client'\n\nimport * as CommerceModel from '../../model'\nimport { commerceSitePermissionRefusal } from '../../model/plugin-permissions'\nimport { BlockedControl } from '@aglyn/shared-ui-jsx/components/blocked-control.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useState } from 'react'\n\nconst SEND_REFUSAL = commerceSitePermissionRefusal('commerce.quotes.send')\n\nexport interface QuotePayLinkSectionProps {\n hostId: string\n quoteId: string\n quote: Pick<CommerceModel.HostOrder, 'status' | 'channel' | 'number' | 'quote'>\n /** Holds \"Send quotes and invoices to customers\" (`commerce.quotes.send`). */\n canSend: boolean\n /** The editor holds changes not saved yet, which the link does not show. */\n edited: boolean\n}\n\n/**\n * A quote's pay link (AGL-4094): the address the business sends the\n * customer, which opens the quote on the business's own site with a Pay\n * button. \"Copy pay link\" asks the server for it (it is signed there) and puts\n * it on the clipboard and on screen; \"Replace link\" makes a new one and the\n * one sent before stops opening the quote.\n *\n * The link shown is kept with the version it was made at, and drops as soon\n * as the quote's stored version (`quote.linkVersion`) moves past it: a\n * revision's first email and Replace link (here or anywhere else) both move\n * it, and the link from before then opens only \"This quote was updated\". A\n * link already on screen is fetched again at the new version; Copy pay link\n * never copies one older than the quote's.\n */\nexport function QuotePayLinkSection(props: QuotePayLinkSectionProps) {\n const { hostId, quoteId, quote, canSend, edited } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const [signed, setSigned] = useState<{ url: string; version: number } | null>(null)\n const [busy, setBusy] = useState(false)\n const [confirmingReplace, setConfirmingReplace] = useState(false)\n const unpaid = CommerceModel.isUnpaidQuote(quote)\n const version = CommerceModel.quoteLinkVersion(quote.quote)\n // A link the server made at a version the stored quote has not caught up to yet is the newest one.\n const url = signed && signed.version >= version ? signed.url : ''\n\n const ask = useCallback(\n async (action: 'pay-link' | 'replace-link'): Promise<string | null> => {\n setBusy(true)\n try {\n const response = await authorizedFetch(user, '/api/commerce/quote', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ action, hostId, quoteId }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok || !payload?.url) {\n enqueueSnackbar(payload?.error ?? 'The pay link could not be made', { variant: 'error', allowDuplicate: true })\n return null\n }\n const made = Math.floor(Number(payload.linkVersion))\n setSigned({ url: String(payload.url), version: Number.isFinite(made) && made >= 1 ? made : version })\n return String(payload.url)\n } finally {\n setBusy(false)\n }\n },\n [user, hostId, quoteId, version, enqueueSnackbar],\n )\n\n // The quote's link moved on (a revision was emailed, or the link replaced):\n // the one on screen opens nothing now, so fetch the current one in its place.\n const stale = signed !== null && signed.version < version\n useEffect(() => {\n if (stale) void ask('pay-link')\n // Once per version the quote moves to; `ask` changing does not ask again.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [stale, version])\n\n const copy = useCallback(\n async (link: string, done: string) => {\n try {\n await navigator.clipboard.writeText(link)\n enqueueSnackbar(done, { variant: 'success', persist: false })\n } catch {\n // The link is on screen to copy by hand when the clipboard is refused.\n enqueueSnackbar('Copy the link below to send it', { variant: 'info', persist: false })\n }\n },\n [enqueueSnackbar],\n )\n\n const copyLink = useCallback(async () => {\n const link = url || (await ask('pay-link'))\n if (link) await copy(link, 'Pay link copied')\n }, [url, ask, copy])\n\n const replaceLink = useCallback(async () => {\n const link = await ask('replace-link')\n setConfirmingReplace(false)\n if (link) await copy(link, 'New pay link copied. The earlier link no longer opens the quote.')\n }, [ask, copy])\n\n return (\n <Stack spacing={1.5}>\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Pay link'}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {unpaid\n ? 'Send this link to the customer. It opens the quote on your site, where they can pay it.'\n : 'The link opens the paid quote and the customer’s order.'}\n </Typography>\n {edited && unpaid ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Save your changes first: the link opens the quote as it was last saved.'}\n </Typography>\n ) : null}\n </Stack>\n {url ? (\n <TextField\n label=\"Pay link\"\n value={url}\n size=\"small\"\n fullWidth\n slotProps={{ htmlInput: { readOnly: true }, inputLabel: { shrink: true } }}\n onFocus={(event) => event.target.select()}\n />\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ flexWrap: 'wrap', rowGap: 1 }}>\n <BlockedControl blocked={!canSend} reason={SEND_REFUSAL}>\n <Button variant=\"outlined\" size=\"small\" disabled={busy} onClick={() => void copyLink()}>\n {'Copy pay link'}\n </Button>\n </BlockedControl>\n {unpaid ? (\n <BlockedControl blocked={!canSend} reason={SEND_REFUSAL}>\n <Button size=\"small\" disabled={busy} onClick={() => setConfirmingReplace(true)}>\n {'Replace link'}\n </Button>\n </BlockedControl>\n ) : null}\n </Stack>\n <Dialog open={confirmingReplace} onClose={() => setConfirmingReplace(false)} maxWidth=\"xs\" fullWidth>\n <DialogTitle>{'Replace the pay link?'}</DialogTitle>\n <DialogContent>\n <DialogContentText>\n {'The link you sent before will stop working: it will tell the customer the quote was updated, ' +\n 'and show nothing of it. Send them the new link.'}\n </DialogContentText>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setConfirmingReplace(false)} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" disabled={busy} onClick={() => void replaceLink()}>\n {'Replace link'}\n </Button>\n </DialogActions>\n </Dialog>\n </Stack>\n )\n}\nQuotePayLinkSection.displayName = 'QuotePayLinkSection'\n\nexport default QuotePayLinkSection\n"],"names":["CommerceModel","commerceSitePermissionRefusal","BlockedControl","useSnackbar","authorizedFetch","useUser","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","Stack","TextField","Typography","useCallback","useEffect","useState","SEND_REFUSAL","QuotePayLinkSection","props","hostId","quoteId","quote","canSend","edited","data","user","enqueueSnackbar","signed","setSigned","busy","setBusy","confirmingReplace","setConfirmingReplace","unpaid","isUnpaidQuote","version","quoteLinkVersion","url","ask","action","response","method","headers","body","JSON","stringify","payload","json","catch","ok","error","variant","allowDuplicate","made","Math","floor","Number","linkVersion","String","isFinite","stale","copy","link","done","navigator","clipboard","writeText","persist","copyLink","replaceLink","spacing","color","label","value","size","fullWidth","slotProps","htmlInput","readOnly","inputLabel","shrink","onFocus","event","target","select","direction","sx","flexWrap","rowGap","blocked","reason","disabled","onClick","open","onClose","maxWidth","displayName"],"mappings":"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"}
|