@aglyn/plugins-commerce 1.0.0-beta.290 → 1.0.0-beta.292
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 +4 -2
- 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/commerce-console-page.js +2 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.d.ts +6 -0
- package/src/lib/components/console/host-orders-card.component.js +3 -2
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/invoice-send.component.d.ts +44 -0
- package/src/lib/components/console/invoice-send.component.js +237 -0
- package/src/lib/components/console/invoice-send.component.js.map +1 -0
- package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
- package/src/lib/components/console/order-detail-dialog.component.js +9 -2
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/order-invoice.component.d.ts +9 -0
- package/src/lib/components/console/order-invoice.component.js +27 -2
- package/src/lib/components/console/order-invoice.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-editor-dialog.component.d.ts +29 -0
- package/src/lib/components/console/quote-editor-dialog.component.js +102 -4
- package/src/lib/components/console/quote-editor-dialog.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-proofs.component.d.ts +11 -0
- package/src/lib/components/console/quote-proofs.component.js +68 -0
- package/src/lib/components/console/quote-proofs.component.js.map +1 -1
- package/src/lib/components/console/quote-send.component.d.ts +42 -0
- package/src/lib/components/console/quote-send.component.js +304 -0
- package/src/lib/components/console/quote-send.component.js.map +1 -0
- package/src/lib/components/console/quote-start.component.d.ts +45 -0
- package/src/lib/components/console/quote-start.component.js +182 -0
- package/src/lib/components/console/quote-start.component.js.map +1 -0
- 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/components/product-options-field-body.d.ts +44 -0
- package/src/lib/components/product-options-field-body.js +260 -0
- package/src/lib/components/product-options-field-body.js.map +1 -0
- package/src/lib/components/product-options-field.d.ts +32 -0
- package/src/lib/components/product-options-field.js +128 -0
- package/src/lib/components/product-options-field.js.map +1 -0
- package/src/lib/components/quote-page-body.js +39 -20
- package/src/lib/components/quote-page-body.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 +139 -0
- package/src/lib/model/commerce-invoices.js +343 -0
- package/src/lib/model/commerce-invoices.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +35 -5
- 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-product-field.d.ts +23 -0
- package/src/lib/model/commerce-product-field.js +26 -0
- package/src/lib/model/commerce-product-field.js.map +1 -0
- package/src/lib/model/commerce-quote-email.d.ts +81 -0
- package/src/lib/model/commerce-quote-email.js +170 -0
- package/src/lib/model/commerce-quote-email.js.map +1 -0
- package/src/lib/model/commerce-quote-pay.d.ts +49 -6
- package/src/lib/model/commerce-quote-pay.js +69 -36
- package/src/lib/model/commerce-quote-pay.js.map +1 -1
- package/src/lib/model/commerce-quote-request.d.ts +93 -0
- package/src/lib/model/commerce-quote-request.js +207 -0
- package/src/lib/model/commerce-quote-request.js.map +1 -0
- package/src/lib/model/commerce-quotes.d.ts +81 -0
- 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/index.d.ts +3 -0
- package/src/lib/model/index.js +3 -0
- package/src/lib/model/index.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/plugin.js +19 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/billing-webhook.js +164 -98
- 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/commerce-pdf.d.ts +5 -1
- package/src/lib/server/commerce-pdf.js +37 -10
- package/src/lib/server/commerce-pdf.js.map +1 -1
- package/src/lib/server/draft-order.js +23 -3
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/invoice-email.d.ts +76 -0
- package/src/lib/server/invoice-email.js +183 -0
- package/src/lib/server/invoice-email.js.map +1 -0
- package/src/lib/server/local-fulfillment.js +5 -1
- package/src/lib/server/local-fulfillment.js.map +1 -1
- package/src/lib/server/order-invoice-send.d.ts +43 -0
- package/src/lib/server/order-invoice-send.js +218 -0
- package/src/lib/server/order-invoice-send.js.map +1 -0
- package/src/lib/server/order-status.d.ts +11 -0
- package/src/lib/server/order-status.js +44 -8
- package/src/lib/server/order-status.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +3 -2
- package/src/lib/server/person-eraser.js +25 -6
- package/src/lib/server/person-eraser.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.d.ts +85 -0
- package/src/lib/server/quote-email.js +211 -0
- package/src/lib/server/quote-email.js.map +1 -0
- package/src/lib/server/quote-link-token.d.ts +11 -0
- package/src/lib/server/quote-link-token.js +21 -0
- package/src/lib/server/quote-link-token.js.map +1 -1
- package/src/lib/server/quote-pay.d.ts +8 -3
- package/src/lib/server/quote-pay.js +105 -43
- package/src/lib/server/quote-pay.js.map +1 -1
- package/src/lib/server/quote.d.ts +11 -0
- package/src/lib/server/quote.js +309 -12
- package/src/lib/server/quote.js.map +1 -1
- package/src/lib/server/site-page-resolver.js +2 -1
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/site.js +8 -0
- package/src/lib/site.js.map +1 -1
- package/src/lib/tenant-emails.js +243 -0
- package/src/lib/tenant-emails.js.map +1 -1
- 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/commerce-document-pdf.d.ts +5 -0
- package/src/lib/utils/commerce-document-pdf.js +6 -1
- package/src/lib/utils/commerce-document-pdf.js.map +1 -1
- package/src/lib/utils/invoice-email-content.d.ts +45 -0
- package/src/lib/utils/invoice-email-content.js +126 -0
- package/src/lib/utils/invoice-email-content.js.map +1 -0
- package/src/lib/utils/order-invoice.d.ts +1 -1
- package/src/lib/utils/order-invoice.js +14 -6
- package/src/lib/utils/order-invoice.js.map +1 -1
- package/src/lib/utils/quote-email-content.d.ts +63 -0
- package/src/lib/utils/quote-email-content.js +157 -0
- package/src/lib/utils/quote-email-content.js.map +1 -0
|
@@ -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":"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"}
|
|
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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"}
|
|
@@ -29,8 +29,37 @@ export interface QuoteEditorDialogProps {
|
|
|
29
29
|
canEdit: boolean;
|
|
30
30
|
/** Holds "Send quotes and invoices to customers" (`commerce.quotes.send`). */
|
|
31
31
|
canSend: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* What a NEW quote opens with when it is started from a request or a
|
|
34
|
+
* contact (AGL-4098). The editable fields are filled in; a request's own
|
|
35
|
+
* parts (which request, its files, its conversation) are stamped by the
|
|
36
|
+
* server from `from` when the quote is created, never from here.
|
|
37
|
+
*/
|
|
38
|
+
start?: QuoteEditorStart | null;
|
|
32
39
|
onClose: () => void;
|
|
33
40
|
}
|
|
41
|
+
/** A new quote's start: from a request (with what it carried) or from a contact. */
|
|
42
|
+
export interface QuoteEditorStart {
|
|
43
|
+
/** The request it is started from, which the server reads again on create; null for a contact. */
|
|
44
|
+
from: {
|
|
45
|
+
kind: 'submission';
|
|
46
|
+
id: string;
|
|
47
|
+
} | null;
|
|
48
|
+
prefill: CommerceModel.QuoteStartPrefill;
|
|
49
|
+
/** The request, as the editor says where the quote came from. */
|
|
50
|
+
request?: {
|
|
51
|
+
formName: string;
|
|
52
|
+
receivedAtMs: number | null;
|
|
53
|
+
} | null;
|
|
54
|
+
files?: ReadonlyArray<{
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
contentType: string;
|
|
58
|
+
size: number;
|
|
59
|
+
}>;
|
|
60
|
+
/** Whether the request started an email conversation the quote's first email will reply into. */
|
|
61
|
+
threaded?: boolean;
|
|
62
|
+
}
|
|
34
63
|
/**
|
|
35
64
|
* The quote editor (AGL-4093): who it is for, its lines (products with their
|
|
36
65
|
* options and add-ons, typed items, things included at no charge), discount,
|