@aglyn/plugins-commerce 1.0.0-beta.218 → 1.0.0-beta.220

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +6 -0
  2. package/package.json +14 -14
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js +1 -19
  5. package/src/index.js.map +1 -1
  6. package/src/lib/components/cart.js +2 -1
  7. package/src/lib/components/cart.js.map +1 -1
  8. package/src/lib/components/console/catalog-organization-card.component.js +665 -459
  9. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  10. package/src/lib/components/console/host-orders-card.component.js +2 -1
  11. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  12. package/src/lib/components/console/member-posts-card.component.js +1 -1
  13. package/src/lib/components/console/member-posts-card.component.js.map +1 -1
  14. package/src/lib/components/console/order-detail-dialog.component.js +8 -7
  15. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  16. package/src/lib/components/console/payments-settings-card.component.js +2 -2
  17. package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
  18. package/src/lib/components/console/product-editor-dialog.component.js +1221 -889
  19. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  20. package/src/lib/components/console/product-zones.d.ts +184 -0
  21. package/src/lib/components/console/product-zones.js +54 -0
  22. package/src/lib/components/console/product-zones.js.map +1 -0
  23. package/src/lib/components/console/products-hub-card.component.js +3 -2
  24. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  25. package/src/lib/components/console/products-hub-zone.component.d.ts +1 -1
  26. package/src/lib/components/console/products-hub-zone.component.js +2 -1
  27. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  28. package/src/lib/components/console/site-member-purchases.component.d.ts +24 -0
  29. package/src/lib/components/console/site-member-purchases.component.js +339 -0
  30. package/src/lib/components/console/site-member-purchases.component.js.map +1 -0
  31. package/src/lib/components/console/store-settings-card.component.js +2 -2
  32. package/src/lib/components/console/store-settings-card.component.js.map +1 -1
  33. package/src/lib/components/console/storefront-tax-summary-card.component.js +1 -1
  34. package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -1
  35. package/src/lib/components/member-signup.js +2 -2
  36. package/src/lib/components/member-signup.js.map +1 -1
  37. package/src/lib/components/newsletter-signup.js +2 -2
  38. package/src/lib/components/newsletter-signup.js.map +1 -1
  39. package/src/lib/components/product-detail.js +2 -1
  40. package/src/lib/components/product-detail.js.map +1 -1
  41. package/src/lib/components/product-grid.js +1 -1
  42. package/src/lib/components/product-grid.js.map +1 -1
  43. package/src/lib/constants/operator-alerts.js +5 -3
  44. package/src/lib/constants/operator-alerts.js.map +1 -1
  45. package/src/lib/constants/storefront-payments.d.ts +33 -0
  46. package/src/lib/constants/storefront-payments.js +35 -0
  47. package/src/lib/constants/storefront-payments.js.map +1 -0
  48. package/src/lib/declarations.server.d.ts +3 -1
  49. package/src/lib/declarations.server.js +19 -1
  50. package/src/lib/declarations.server.js.map +1 -1
  51. package/src/lib/model/commerce-io.js +1 -1
  52. package/src/lib/model/commerce-io.js.map +1 -1
  53. package/src/lib/model/commerce-record-lists.d.ts +31 -0
  54. package/src/lib/model/commerce-record-lists.js +45 -0
  55. package/src/lib/model/commerce-record-lists.js.map +1 -0
  56. package/src/lib/model/commerce-record-routes.d.ts +45 -0
  57. package/src/lib/model/commerce-record-routes.js +72 -0
  58. package/src/lib/model/commerce-record-routes.js.map +1 -0
  59. package/src/lib/model/commerce-search-sources.d.ts +21 -0
  60. package/src/lib/model/commerce-search-sources.js +42 -0
  61. package/src/lib/model/commerce-search-sources.js.map +1 -0
  62. package/src/lib/model/commerce-topics.d.ts +20 -0
  63. package/src/lib/model/commerce-topics.js +33 -0
  64. package/src/lib/model/commerce-topics.js.map +1 -0
  65. package/src/lib/model/index.d.ts +1 -0
  66. package/src/lib/model/index.js +1 -0
  67. package/src/lib/model/index.js.map +1 -1
  68. package/src/lib/model/product-record.d.ts +32 -0
  69. package/src/lib/model/product-record.js +82 -0
  70. package/src/lib/model/product-record.js.map +1 -0
  71. package/src/lib/model/site-member-purchases.d.ts +48 -0
  72. package/src/lib/model/site-member-purchases.js +50 -0
  73. package/src/lib/model/site-member-purchases.js.map +1 -0
  74. package/src/lib/model/storefront-tax-summary.d.ts +121 -0
  75. package/src/lib/model/storefront-tax-summary.js +148 -0
  76. package/src/lib/model/storefront-tax-summary.js.map +1 -0
  77. package/src/lib/plan-entitlements.d.ts +35 -0
  78. package/src/lib/plan-entitlements.js +54 -0
  79. package/src/lib/plan-entitlements.js.map +1 -0
  80. package/src/lib/plugin.js +21 -0
  81. package/src/lib/plugin.js.map +1 -1
  82. package/src/lib/server/billing-webhook.js +387 -248
  83. package/src/lib/server/billing-webhook.js.map +1 -1
  84. package/src/lib/server/cart-checkout.js +3 -3
  85. package/src/lib/server/cart-checkout.js.map +1 -1
  86. package/src/lib/server/checkout.js +2 -2
  87. package/src/lib/server/checkout.js.map +1 -1
  88. package/src/lib/server/connect.js +5 -5
  89. package/src/lib/server/connect.js.map +1 -1
  90. package/src/lib/server/draft-order.js +2 -2
  91. package/src/lib/server/draft-order.js.map +1 -1
  92. package/src/lib/server/gift-cards.js +7 -3
  93. package/src/lib/server/gift-cards.js.map +1 -1
  94. package/src/lib/server/low-stock.js +3 -1
  95. package/src/lib/server/low-stock.js.map +1 -1
  96. package/src/lib/server/member-post.js +2 -2
  97. package/src/lib/server/member-post.js.map +1 -1
  98. package/src/lib/server/membership-admin-password.js +4 -1
  99. package/src/lib/server/membership-admin-password.js.map +1 -1
  100. package/src/lib/server/membership-content.js +1 -1
  101. package/src/lib/server/membership-content.js.map +1 -1
  102. package/src/lib/server/membership-register.js +13 -2
  103. package/src/lib/server/membership-register.js.map +1 -1
  104. package/src/lib/server/newsletter.js +32 -19
  105. package/src/lib/server/newsletter.js.map +1 -1
  106. package/src/lib/server/pos-order.js +3 -3
  107. package/src/lib/server/pos-order.js.map +1 -1
  108. package/src/lib/server/process-abandoned.js +4 -2
  109. package/src/lib/server/process-abandoned.js.map +1 -1
  110. package/src/lib/server/process-restock.js +7 -5
  111. package/src/lib/server/process-restock.js.map +1 -1
  112. package/src/lib/server/product-record-index.d.ts +19 -0
  113. package/src/lib/server/product-record-index.js +83 -0
  114. package/src/lib/server/product-record-index.js.map +1 -0
  115. package/src/lib/server/reconcile-stock.js +1 -1
  116. package/src/lib/server/reconcile-stock.js.map +1 -1
  117. package/src/lib/server/reserve-stock.js +6 -1
  118. package/src/lib/server/reserve-stock.js.map +1 -1
  119. package/src/lib/server/reserve.js +2 -2
  120. package/src/lib/server/reserve.js.map +1 -1
  121. package/src/lib/server/revenue-source.d.ts +130 -0
  122. package/src/lib/server/revenue-source.js +286 -0
  123. package/src/lib/server/revenue-source.js.map +1 -0
  124. package/src/lib/server/supplier-outbox.js +16 -1
  125. package/src/lib/server/supplier-outbox.js.map +1 -1
  126. package/src/lib/server/supplier-update.js +19 -9
  127. package/src/lib/server/supplier-update.js.map +1 -1
  128. package/src/lib/server/tax-profile.d.ts +8 -5
  129. package/src/lib/server/tax-profile.js +22 -5
  130. package/src/lib/server/tax-profile.js.map +1 -1
  131. package/src/lib/server/tax-return-source.d.ts +125 -0
  132. package/src/lib/server/tax-return-source.js +524 -0
  133. package/src/lib/server/tax-return-source.js.map +1 -0
  134. package/src/lib/server/tax-summary.d.ts +92 -0
  135. package/src/lib/server/tax-summary.js +258 -0
  136. package/src/lib/server/tax-summary.js.map +1 -0
  137. package/src/lib/server.d.ts +1 -0
  138. package/src/lib/server.js +23 -0
  139. package/src/lib/server.js.map +1 -1
  140. package/src/lib/starter-templates.d.ts +19 -0
  141. package/src/lib/starter-templates.js +157 -0
  142. package/src/lib/starter-templates.js.map +1 -0
  143. package/src/lib/tenant-emails.d.ts +33 -0
  144. package/src/lib/tenant-emails.js +551 -0
  145. package/src/lib/tenant-emails.js.map +1 -0
  146. package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
  147. package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
  148. package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
@@ -24,7 +24,7 @@ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
24
24
  import { Timestamp } from "@aglyn/shared-util-timestamp";
25
25
  import { Alert, Autocomplete, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Divider, MenuItem, Stack, Switch, TextField, Typography } from "@mui/material";
26
26
  import { collection, deleteDoc, doc, setDoc } from "firebase/firestore";
27
- import { useCallback, useMemo, useState } from "react";
27
+ import { memo, useCallback, useMemo, useRef, useState } from "react";
28
28
  import { useFirestore, useUser } from "@aglyn/tenant-feature-instance";
29
29
  import { ceilingedWindow, collectionCeiling, useFirestoreCollection, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
30
30
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
@@ -89,13 +89,601 @@ const RULE_OPS = [
89
89
  label: 'contains'
90
90
  }
91
91
  ];
92
+ const productLabel = (product)=>product.name;
93
+ const sameProduct = (option, value)=>option.$id === value.$id;
94
+ /**
95
+ * A manual collection's products, as chips — MEMOIZED, and every prop it
96
+ * takes is stable while the picked products are (AGL-3423).
97
+ *
98
+ * A multiple Autocomplete hands its input a new chip array as
99
+ * `startAdornment` on every render, and MUI's `InputBase` copies that into
100
+ * its `FormControl` from a passive effect: a state update left pending after
101
+ * every commit the field takes part in. React 19 counts each such commit as a
102
+ * nested update, and keystrokes delivered back to back commit one after
103
+ * another with nothing to clear the count, so the fifty-first throws
104
+ * "Maximum update depth exceeded" (#185). The collection's name, slug and
105
+ * rules share one draft with this list, so each keystroke in them
106
+ * re-rendered it; kept out of those renders, it commits only when it changes.
107
+ */ const CollectionProductsField = /*#__PURE__*/ memo(function CollectionProductsField(props) {
108
+ const { onChange } = props;
109
+ return /*#__PURE__*/ _jsx(Autocomplete, {
110
+ multiple: true,
111
+ options: props.products,
112
+ getOptionLabel: productLabel,
113
+ isOptionEqualToValue: sameProduct,
114
+ value: props.value,
115
+ onChange: (_event, picked)=>onChange(picked),
116
+ renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
117
+ label: "Products",
118
+ size: "small"
119
+ }))
120
+ });
121
+ });
122
+ /**
123
+ * A text field that redraws only when what it shows changes (AGL-3423).
124
+ *
125
+ * Both editors keep their draft in this card, so a keystroke in either one
126
+ * re-renders the card, and drawn whole that is both lists with a match count
127
+ * per collection, their pagers, and every field of the dialog typed into.
128
+ * When a keystroke costs that much, typed input queues and is processed back
129
+ * to back, which is what lets a chip list's pending update climb to #185 (see
130
+ * `CollectionProductsField`). The lists and each part of the dialogs are
131
+ * memoized on what they show, and each field is this, handed a handler and
132
+ * styles that keep their identity, so the keystroke redraws the field it
133
+ * lands in.
134
+ */ const MemoTextField = /*#__PURE__*/ memo(TextField);
135
+ const FIRST_FIELD = {
136
+ mt: 1
137
+ };
138
+ const RULE_FIELD = {
139
+ width: 130
140
+ };
141
+ const RULE_OP = {
142
+ width: 110
143
+ };
144
+ const RULE_VALUE = {
145
+ flex: 1
146
+ };
147
+ const MODE_MENU = [
148
+ /*#__PURE__*/ _jsx(MenuItem, {
149
+ value: "manual",
150
+ children: 'Manual — pick products'
151
+ }, "manual"),
152
+ /*#__PURE__*/ _jsx(MenuItem, {
153
+ value: "smart",
154
+ children: 'Smart — rule based'
155
+ }, "smart")
156
+ ];
157
+ const RULE_FIELD_MENU = RULE_FIELDS.map((field)=>/*#__PURE__*/ _jsx(MenuItem, {
158
+ value: field.value,
159
+ children: field.label
160
+ }, field.value));
161
+ const RULE_OP_MENU = RULE_OPS.map((op)=>/*#__PURE__*/ _jsx(MenuItem, {
162
+ value: op.value,
163
+ children: op.label
164
+ }, op.value));
165
+ /** The category tree, a page at a time, and the button that adds one. */ const CategoriesSection = /*#__PURE__*/ memo(function CategoriesSection(props) {
166
+ const { categories, visible, onEdit, onDelete } = props;
167
+ return /*#__PURE__*/ _jsxs(_Fragment, {
168
+ children: [
169
+ /*#__PURE__*/ _jsx(Typography, {
170
+ variant: "subtitle2",
171
+ children: 'Categories'
172
+ }),
173
+ categories.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
174
+ variant: "body2",
175
+ color: "text.secondary",
176
+ children: 'Group products into a browsable tree (e.g. Brakes → Pads).'
177
+ }) : visible.map((category)=>/*#__PURE__*/ _jsxs(Stack, {
178
+ direction: "row",
179
+ spacing: 1,
180
+ sx: {
181
+ alignItems: 'center',
182
+ pl: category.depth * 2
183
+ },
184
+ children: [
185
+ /*#__PURE__*/ _jsxs(Typography, {
186
+ variant: "body2",
187
+ sx: {
188
+ flex: 1
189
+ },
190
+ noWrap: true,
191
+ children: [
192
+ category.name,
193
+ /*#__PURE__*/ _jsx(Typography, {
194
+ component: "span",
195
+ variant: "caption",
196
+ color: "text.secondary",
197
+ children: ` /${category.slug}`
198
+ })
199
+ ]
200
+ }),
201
+ /*#__PURE__*/ _jsx(Button, {
202
+ size: "small",
203
+ onClick: ()=>{
204
+ var _category_parentId;
205
+ return onEdit({
206
+ id: category.$id,
207
+ name: category.name,
208
+ parentId: (_category_parentId = category.parentId) != null ? _category_parentId : ''
209
+ });
210
+ },
211
+ children: 'Edit'
212
+ }),
213
+ /*#__PURE__*/ _jsx(Button, {
214
+ size: "small",
215
+ color: "error",
216
+ onClick: onDelete(category),
217
+ children: 'Delete'
218
+ })
219
+ ]
220
+ }, category.$id)),
221
+ categories.length === 0 ? null : /*#__PURE__*/ _jsx(ListPagination, {
222
+ page: props.page,
223
+ pageSize: props.pageSize,
224
+ rowCount: visible.length,
225
+ // The categories the card HOLDS — a client slice of rows already
226
+ // read, so the total is known exactly.
227
+ count: categories.length,
228
+ onPageChange: props.onPageChange,
229
+ onPageSizeChange: props.onPageSizeChange
230
+ }),
231
+ props.truncated ? /*#__PURE__*/ _jsx(Alert, {
232
+ severity: "info",
233
+ children: `Showing ${CATEGORY_CEILING} categories, ordered by id. This ` + 'catalog has more — a category whose parent is past that ' + 'boundary is drawn at the top level here rather than under it.'
234
+ }) : null,
235
+ /*#__PURE__*/ _jsx(Button, {
236
+ size: "small",
237
+ sx: {
238
+ alignSelf: 'flex-start'
239
+ },
240
+ onClick: ()=>onEdit({
241
+ id: null,
242
+ name: '',
243
+ parentId: ''
244
+ }),
245
+ children: 'Add category'
246
+ })
247
+ ]
248
+ });
249
+ });
250
+ /** The catalog collections, a page at a time, and the button that adds one. */ const CollectionsSection = /*#__PURE__*/ memo(function CollectionsSection(props) {
251
+ const { collections, visible, products, countPrefix, onEdit, onDelete } = props;
252
+ /**
253
+ * How many products a collection holds — over the CATALOG WINDOW.
254
+ *
255
+ * Rendered with "at least" when the probe found a product past the ceiling,
256
+ * because past that point this is a lower bound and printing it as a total
257
+ * is the count-that-is-a-window-length defect. The number itself is honest
258
+ * either way; only the claim around it changes.
259
+ */ const collectionCount = (row)=>products.filter((product)=>CommerceModel.matchesCollection(product, row, product.$id)).length;
260
+ return /*#__PURE__*/ _jsxs(_Fragment, {
261
+ children: [
262
+ /*#__PURE__*/ _jsx(Typography, {
263
+ variant: "subtitle2",
264
+ children: 'Collections'
265
+ }),
266
+ visible.map((row)=>{
267
+ var _row_mode;
268
+ return /*#__PURE__*/ _jsxs(Stack, {
269
+ direction: "row",
270
+ spacing: 1,
271
+ sx: {
272
+ alignItems: 'center'
273
+ },
274
+ children: [
275
+ /*#__PURE__*/ _jsxs(Typography, {
276
+ variant: "body2",
277
+ sx: {
278
+ flex: 1
279
+ },
280
+ noWrap: true,
281
+ children: [
282
+ row.name,
283
+ /*#__PURE__*/ _jsx(Typography, {
284
+ component: "span",
285
+ variant: "caption",
286
+ color: "text.secondary",
287
+ children: ` · ${(_row_mode = row.mode) != null ? _row_mode : 'manual'} · ${countPrefix}${collectionCount(row)} products`
288
+ })
289
+ ]
290
+ }),
291
+ /*#__PURE__*/ _jsx(Button, {
292
+ size: "small",
293
+ onClick: ()=>{
294
+ var _row_slug, _row_mode, _row_productIds, _row_rules;
295
+ return onEdit({
296
+ id: row.$id,
297
+ name: row.name,
298
+ slug: (_row_slug = row.slug) != null ? _row_slug : '',
299
+ mode: (_row_mode = row.mode) != null ? _row_mode : 'manual',
300
+ productIds: (_row_productIds = row.productIds) != null ? _row_productIds : [],
301
+ rules: (_row_rules = row.rules) != null ? _row_rules : [],
302
+ matchAll: row.matchAll !== false
303
+ });
304
+ },
305
+ children: 'Edit'
306
+ }),
307
+ /*#__PURE__*/ _jsx(Button, {
308
+ size: "small",
309
+ color: "error",
310
+ onClick: onDelete(row),
311
+ children: 'Delete'
312
+ })
313
+ ]
314
+ }, row.$id);
315
+ }),
316
+ collections.length === 0 ? null : /*#__PURE__*/ _jsx(ListPagination, {
317
+ page: props.page,
318
+ pageSize: props.pageSize,
319
+ rowCount: visible.length,
320
+ // The CATALOG collections, which is what this list is. Not the
321
+ // window's length: content collections share the subcollection
322
+ // and are classified out before this count is taken.
323
+ count: collections.length,
324
+ onPageChange: props.onPageChange,
325
+ onPageSizeChange: props.onPageSizeChange
326
+ }),
327
+ props.truncated ? /*#__PURE__*/ _jsx(Alert, {
328
+ severity: "info",
329
+ children: `Showing ${COLLECTION_CEILING} collections, ordered by id. This ` + 'site has more — the slug check below only covers the ones ' + 'listed here, so an address may already be taken by one that ' + 'is not.'
330
+ }) : null,
331
+ /*#__PURE__*/ _jsx(Button, {
332
+ size: "small",
333
+ sx: {
334
+ alignSelf: 'flex-start'
335
+ },
336
+ onClick: ()=>onEdit({
337
+ id: null,
338
+ name: '',
339
+ slug: '',
340
+ mode: 'manual',
341
+ productIds: [],
342
+ rules: [],
343
+ matchAll: true
344
+ }),
345
+ children: 'Add collection'
346
+ })
347
+ ]
348
+ });
349
+ });
350
+ /** The category editor: a name and a parent. */ const CategoryDialog = /*#__PURE__*/ memo(function CategoryDialog(props) {
351
+ var _ref, _ref1;
352
+ const { draft, categories, setDraft } = props;
353
+ const draftId = draft == null ? void 0 : draft.id;
354
+ const close = useCallback(()=>setDraft(null), [
355
+ setDraft
356
+ ]);
357
+ const setName = useCallback((event)=>setDraft((prev)=>prev ? _extends({}, prev, {
358
+ name: event.target.value
359
+ }) : prev), [
360
+ setDraft
361
+ ]);
362
+ const setParent = useCallback((event)=>setDraft((prev)=>prev ? _extends({}, prev, {
363
+ parentId: event.target.value
364
+ }) : prev), [
365
+ setDraft
366
+ ]);
367
+ const parentMenu = useMemo(()=>[
368
+ /*#__PURE__*/ _jsx(MenuItem, {
369
+ value: "",
370
+ children: 'None (top level)'
371
+ }, ""),
372
+ // The whole set, not `visibleCategories` — a parent picker that
373
+ // offered only the current page could not reparent a category under
374
+ // one two pages away.
375
+ ...categories.filter((category)=>category.$id !== draftId).map((category)=>/*#__PURE__*/ _jsx(MenuItem, {
376
+ value: category.$id,
377
+ children: `${'— '.repeat(category.depth)}${category.name}`
378
+ }, category.$id))
379
+ ], [
380
+ categories,
381
+ draftId
382
+ ]);
383
+ return /*#__PURE__*/ _jsxs(Dialog, {
384
+ open: Boolean(draft),
385
+ onClose: close,
386
+ maxWidth: "xs",
387
+ fullWidth: true,
388
+ children: [
389
+ /*#__PURE__*/ _jsx(DialogTitle, {
390
+ children: (draft == null ? void 0 : draft.id) ? 'Edit category' : 'New category'
391
+ }),
392
+ /*#__PURE__*/ _jsxs(DialogContent, {
393
+ sx: {
394
+ display: 'flex',
395
+ flexDirection: 'column',
396
+ gap: 2
397
+ },
398
+ children: [
399
+ /*#__PURE__*/ _jsx(MemoTextField, {
400
+ label: "Name",
401
+ value: (_ref = draft == null ? void 0 : draft.name) != null ? _ref : '',
402
+ onChange: setName,
403
+ size: "small",
404
+ autoFocus: true,
405
+ sx: FIRST_FIELD
406
+ }),
407
+ /*#__PURE__*/ _jsx(MemoTextField, {
408
+ label: "Parent category",
409
+ value: (_ref1 = draft == null ? void 0 : draft.parentId) != null ? _ref1 : '',
410
+ onChange: setParent,
411
+ size: "small",
412
+ select: true,
413
+ children: parentMenu
414
+ })
415
+ ]
416
+ }),
417
+ /*#__PURE__*/ _jsxs(DialogActions, {
418
+ children: [
419
+ /*#__PURE__*/ _jsx(Button, {
420
+ onClick: close,
421
+ children: 'Cancel'
422
+ }),
423
+ /*#__PURE__*/ _jsx(Button, {
424
+ variant: "contained",
425
+ color: "primary",
426
+ disabled: !(draft == null ? void 0 : draft.name.trim()),
427
+ onClick: props.onSave,
428
+ children: 'Save'
429
+ })
430
+ ]
431
+ })
432
+ ]
433
+ });
434
+ });
435
+ const CollectionRuleRow = /*#__PURE__*/ memo(function CollectionRuleRow(props) {
436
+ const { index, rule, onChange } = props;
437
+ const setField = useCallback((event)=>onChange(index, {
438
+ field: event.target.value
439
+ }), [
440
+ index,
441
+ onChange
442
+ ]);
443
+ const setOp = useCallback((event)=>onChange(index, {
444
+ op: event.target.value
445
+ }), [
446
+ index,
447
+ onChange
448
+ ]);
449
+ const setCategory = useCallback((event)=>onChange(index, {
450
+ value: event.target.value
451
+ }), [
452
+ index,
453
+ onChange
454
+ ]);
455
+ const ruleField = rule.field;
456
+ const setValue = useCallback((event)=>onChange(index, {
457
+ value: ruleField === 'priceUsd' ? Number(event.target.value) : event.target.value
458
+ }), [
459
+ index,
460
+ onChange,
461
+ ruleField
462
+ ]);
463
+ return /*#__PURE__*/ _jsxs(Stack, {
464
+ direction: "row",
465
+ spacing: 1,
466
+ children: [
467
+ /*#__PURE__*/ _jsx(MemoTextField, {
468
+ value: rule.field,
469
+ onChange: setField,
470
+ size: "small",
471
+ select: true,
472
+ sx: RULE_FIELD,
473
+ children: RULE_FIELD_MENU
474
+ }),
475
+ /*#__PURE__*/ _jsx(MemoTextField, {
476
+ value: rule.op,
477
+ onChange: setOp,
478
+ size: "small",
479
+ select: true,
480
+ sx: RULE_OP,
481
+ children: RULE_OP_MENU
482
+ }),
483
+ rule.field === 'categoryId' ? /*#__PURE__*/ _jsx(MemoTextField, {
484
+ value: rule.value,
485
+ onChange: setCategory,
486
+ size: "small",
487
+ select: true,
488
+ sx: RULE_VALUE,
489
+ children: props.categoryMenu
490
+ }) : /*#__PURE__*/ _jsx(MemoTextField, {
491
+ value: rule.value,
492
+ onChange: setValue,
493
+ size: "small",
494
+ sx: RULE_VALUE,
495
+ placeholder: rule.field === 'type' ? 'physical' : 'Value'
496
+ }),
497
+ /*#__PURE__*/ _jsx(Button, {
498
+ size: "small",
499
+ color: "error",
500
+ onClick: ()=>onChange(index, null),
501
+ children: '✕'
502
+ })
503
+ ]
504
+ });
505
+ });
506
+ /** Whether every rule must hold, or any one. */ const CollectionMatchSwitch = /*#__PURE__*/ memo(function CollectionMatchSwitch(props) {
507
+ const { matchAll, setDraft } = props;
508
+ return /*#__PURE__*/ _jsxs(Stack, {
509
+ direction: "row",
510
+ spacing: 1,
511
+ sx: {
512
+ alignItems: 'center'
513
+ },
514
+ children: [
515
+ /*#__PURE__*/ _jsx(Typography, {
516
+ variant: "body2",
517
+ children: 'Match'
518
+ }),
519
+ /*#__PURE__*/ _jsx(Switch, {
520
+ size: "small",
521
+ checked: matchAll,
522
+ onChange: (event)=>setDraft((prev)=>prev ? _extends({}, prev, {
523
+ matchAll: event.target.checked
524
+ }) : prev)
525
+ }),
526
+ /*#__PURE__*/ _jsx(Typography, {
527
+ variant: "body2",
528
+ children: matchAll ? 'all rules' : 'any rule'
529
+ })
530
+ ]
531
+ });
532
+ });
533
+ /** A smart collection's rules, whether all or any must hold, and the button that adds one. */ const CollectionRulesFields = /*#__PURE__*/ memo(function CollectionRulesFields(props) {
534
+ const { rules, matchAll, categories, setDraft, onRule } = props;
535
+ const categoryMenu = useMemo(()=>categories.map((category)=>/*#__PURE__*/ _jsx(MenuItem, {
536
+ value: category.$id,
537
+ children: category.name
538
+ }, category.$id)), [
539
+ categories
540
+ ]);
541
+ return /*#__PURE__*/ _jsxs(_Fragment, {
542
+ children: [
543
+ /*#__PURE__*/ _jsx(CollectionMatchSwitch, {
544
+ matchAll: matchAll,
545
+ setDraft: setDraft
546
+ }),
547
+ rules.map((rule, index)=>/*#__PURE__*/ _jsx(CollectionRuleRow, {
548
+ index: index,
549
+ rule: rule,
550
+ categoryMenu: categoryMenu,
551
+ onChange: onRule
552
+ }, index)),
553
+ /*#__PURE__*/ _jsx(Button, {
554
+ size: "small",
555
+ sx: {
556
+ alignSelf: 'flex-start'
557
+ },
558
+ onClick: ()=>onRule(rules.length, {}),
559
+ children: 'Add rule'
560
+ })
561
+ ]
562
+ });
563
+ });
564
+ /** The products a draft collection holds, the first few by name. */ const CollectionPreview = /*#__PURE__*/ memo(function CollectionPreview(props) {
565
+ const { matches, countPrefix } = props;
566
+ return /*#__PURE__*/ _jsxs(Stack, {
567
+ direction: "row",
568
+ spacing: 0.5,
569
+ sx: {
570
+ flexWrap: 'wrap'
571
+ },
572
+ children: [
573
+ /*#__PURE__*/ _jsx(Typography, {
574
+ variant: "caption",
575
+ color: "text.secondary",
576
+ children: `Matches ${countPrefix}${matches.length} products: `
577
+ }),
578
+ matches.slice(0, 6).map((product)=>/*#__PURE__*/ _jsx(Chip, {
579
+ label: product.name,
580
+ size: "small"
581
+ }, product.$id)),
582
+ matches.length > 6 ? /*#__PURE__*/ _jsx(Typography, {
583
+ variant: "caption",
584
+ color: "text.secondary",
585
+ children: `+${matches.length - 6} more`
586
+ }) : null
587
+ ]
588
+ });
589
+ });
590
+ /** The collection editor: a name, a mode, and the products or rules it holds. */ const CollectionDialog = /*#__PURE__*/ memo(function CollectionDialog(props) {
591
+ var _ref, _ref1, _ref2;
592
+ const { draft, error, setDraft } = props;
593
+ const close = useCallback(()=>setDraft(null), [
594
+ setDraft
595
+ ]);
596
+ const setName = useCallback((event)=>setDraft((prev)=>prev ? _extends({}, prev, {
597
+ name: event.target.value
598
+ }) : prev), [
599
+ setDraft
600
+ ]);
601
+ const setMode = useCallback((event)=>setDraft((prev)=>prev ? _extends({}, prev, {
602
+ mode: event.target.value
603
+ }) : prev), [
604
+ setDraft
605
+ ]);
606
+ return /*#__PURE__*/ _jsxs(Dialog, {
607
+ open: Boolean(draft),
608
+ onClose: close,
609
+ maxWidth: "sm",
610
+ fullWidth: true,
611
+ children: [
612
+ /*#__PURE__*/ _jsx(DialogTitle, {
613
+ children: (draft == null ? void 0 : draft.id) ? 'Edit collection' : 'New collection'
614
+ }),
615
+ /*#__PURE__*/ _jsxs(DialogContent, {
616
+ sx: {
617
+ display: 'flex',
618
+ flexDirection: 'column',
619
+ gap: 2
620
+ },
621
+ children: [
622
+ /*#__PURE__*/ _jsx(MemoTextField, {
623
+ label: "Name",
624
+ value: (_ref = draft == null ? void 0 : draft.name) != null ? _ref : '',
625
+ onChange: setName,
626
+ size: "small",
627
+ autoFocus: true,
628
+ sx: FIRST_FIELD,
629
+ error: props.slugTaken,
630
+ helperText: (draft == null ? void 0 : draft.name) ? `/collections/${props.slug}` : undefined
631
+ }),
632
+ /*#__PURE__*/ _jsx(MemoTextField, {
633
+ label: "Mode",
634
+ value: (_ref1 = draft == null ? void 0 : draft.mode) != null ? _ref1 : 'manual',
635
+ onChange: setMode,
636
+ size: "small",
637
+ select: true,
638
+ children: MODE_MENU
639
+ }),
640
+ (draft == null ? void 0 : draft.mode) === 'manual' ? /*#__PURE__*/ _jsx(CollectionProductsField, {
641
+ products: props.products,
642
+ value: props.picked,
643
+ onChange: props.onProducts
644
+ }) : /*#__PURE__*/ _jsx(CollectionRulesFields, {
645
+ rules: (_ref2 = draft == null ? void 0 : draft.rules) != null ? _ref2 : NO_RULES,
646
+ matchAll: (draft == null ? void 0 : draft.matchAll) !== false,
647
+ categories: props.categories,
648
+ setDraft: setDraft,
649
+ onRule: props.onRule
650
+ }),
651
+ error ? /*#__PURE__*/ _jsx(Alert, {
652
+ severity: "warning",
653
+ children: error
654
+ }) : null,
655
+ draft ? /*#__PURE__*/ _jsx(CollectionPreview, {
656
+ matches: props.matches,
657
+ countPrefix: props.countPrefix
658
+ }) : null
659
+ ]
660
+ }),
661
+ /*#__PURE__*/ _jsxs(DialogActions, {
662
+ children: [
663
+ /*#__PURE__*/ _jsx(Button, {
664
+ onClick: close,
665
+ children: 'Cancel'
666
+ }),
667
+ /*#__PURE__*/ _jsx(Button, {
668
+ variant: "contained",
669
+ color: "primary",
670
+ disabled: !(draft == null ? void 0 : draft.name.trim()) || Boolean(error),
671
+ onClick: props.onSave,
672
+ children: 'Save'
673
+ })
674
+ ]
675
+ })
676
+ ]
677
+ });
678
+ });
679
+ /** A collection with no rules yet, the same list on every render. */ const NO_RULES = [];
92
680
  /**
93
681
  * Categories & collections manager (AGL-280): category tree (parentId)
94
682
  * at `hosts/{hostId}/productCategories`, manual + smart collections at
95
683
  * `hosts/{hostId}/collections` with a live matched-product preview from
96
684
  * the same `matchesCollection` matcher the storefront uses.
97
685
  */ export function CatalogOrganizationCard(props) {
98
- var _CommerceModel_validateCollection, _ref, _ref1, _ref2, _ref3, _ref4;
686
+ var _CommerceModel_validateCollection;
99
687
  const { hostId } = props;
100
688
  const firestore = useFirestore();
101
689
  const { enqueueSnackbar } = useSnackbar();
@@ -421,14 +1009,35 @@ const RULE_OPS = [
421
1009
  const collectionError = collectionDraft ? collectionDraft.name ? (_CommerceModel_validateCollection = CommerceModel.validateCollection(_extends({}, collectionDraft, {
422
1010
  slug: draftSlug
423
1011
  }))) != null ? _CommerceModel_validateCollection : slugTaken ? `Another collection already serves /collections/${draftSlug}` : null : null : null;
1012
+ const collectionProductIds = collectionDraft == null ? void 0 : collectionDraft.productIds;
1013
+ const collectionProducts = useMemo(()=>products.filter((product)=>(collectionProductIds != null ? collectionProductIds : []).includes(product.$id)), [
1014
+ products,
1015
+ collectionProductIds
1016
+ ]);
1017
+ const setCollectionProducts = useCallback((picked)=>setCollectionDraft((prev)=>prev ? _extends({}, prev, {
1018
+ productIds: picked.map((item)=>item.$id)
1019
+ }) : prev), []);
1020
+ /*
1021
+ * Recomputed when what decides membership changes — the mode, the picked
1022
+ * products, the rules and whether all must hold — and not on a keystroke in
1023
+ * the name, which no match reads (AGL-3423).
1024
+ */ const draftMode = collectionDraft == null ? void 0 : collectionDraft.mode;
1025
+ const draftRules = collectionDraft == null ? void 0 : collectionDraft.rules;
1026
+ const draftMatchAll = collectionDraft == null ? void 0 : collectionDraft.matchAll;
424
1027
  const previewMatches = useMemo(()=>{
425
- if (!collectionDraft) return [];
426
- const candidate = _extends({}, collectionDraft, {
427
- slug: collectionDraft.slug || CommerceModel.commerceSlug(collectionDraft.name)
428
- });
1028
+ if (!draftMode) return [];
1029
+ const candidate = {
1030
+ mode: draftMode,
1031
+ productIds: collectionProductIds,
1032
+ rules: draftRules,
1033
+ matchAll: draftMatchAll
1034
+ };
429
1035
  return products.filter((product)=>CommerceModel.matchesCollection(product, candidate, product.$id));
430
1036
  }, [
431
- collectionDraft,
1037
+ draftMode,
1038
+ collectionProductIds,
1039
+ draftRules,
1040
+ draftMatchAll,
432
1041
  products
433
1042
  ]);
434
1043
  /**
@@ -568,7 +1177,7 @@ const RULE_OPS = [
568
1177
  // entries with it was the opposite of what happens, and the two
569
1178
  // blockers are the whole point of the feature — say them before the
570
1179
  // button is armed, not in a 409 afterwards.
571
- description: `Storefront blocks pointing at "${row.name}" go empty. The delete ` + 'is refused while the collection still has entries, or while a ' + 'screen uses it as a list or entry template — empty it and detach ' + 'those screens first. It also takes the site admin role.',
1180
+ description: `Storefront blocks pointing at "${row.name}" go empty. The delete ` + 'is refused while the collection still has entries, or while a ' + 'page uses it as a list or entry template — empty it and detach ' + 'those pages first. It also takes the site admin role.',
572
1181
  confirmationText: 'Delete',
573
1182
  confirmationButtonProps: {
574
1183
  color: 'error'
@@ -622,14 +1231,6 @@ const RULE_OPS = [
622
1231
  enqueueSnackbar,
623
1232
  restampMembership
624
1233
  ]);
625
- /**
626
- * How many products a collection holds — over the CATALOG WINDOW.
627
- *
628
- * Rendered with "at least" when the probe found a product past the ceiling,
629
- * because past that point this is a lower bound and printing it as a total
630
- * is the count-that-is-a-window-length defect. The number itself is honest
631
- * either way; only the claim around it changes.
632
- */ const collectionCount = (row)=>products.filter((product)=>CommerceModel.matchesCollection(product, row, product.$id)).length;
633
1234
  /*
634
1235
  * "at least", or nothing at all.
635
1236
  *
@@ -642,11 +1243,18 @@ const RULE_OPS = [
642
1243
  * because a catalog of exactly the ceiling is complete: nothing is missing,
643
1244
  * and a comparison would put "at least" on a number that is exact.
644
1245
  */ const countPrefix = productsTruncated ? 'at least ' : '';
645
- const updateRule = (index, patch)=>{
646
- var _collectionDraft_rules;
647
- if (!collectionDraft) return;
1246
+ /*
1247
+ * A rule's change, written over the collection draft as last rendered, from
1248
+ * a handler that keeps its identity so a rule row redraws only when its own
1249
+ * rule changes (AGL-3423).
1250
+ */ const latestCollectionDraft = useRef(collectionDraft);
1251
+ latestCollectionDraft.current = collectionDraft;
1252
+ const updateRule = useCallback((index, patch)=>{
1253
+ var _draft_rules;
1254
+ const draft = latestCollectionDraft.current;
1255
+ if (!draft) return;
648
1256
  const rules = [
649
- ...(_collectionDraft_rules = collectionDraft.rules) != null ? _collectionDraft_rules : []
1257
+ ...(_draft_rules = draft.rules) != null ? _draft_rules : []
650
1258
  ];
651
1259
  if (patch === null) rules.splice(index, 1);
652
1260
  else rules[index] = _extends({
@@ -654,10 +1262,10 @@ const RULE_OPS = [
654
1262
  op: 'eq',
655
1263
  value: ''
656
1264
  }, rules[index], patch);
657
- setCollectionDraft(_extends({}, collectionDraft, {
1265
+ setCollectionDraft(_extends({}, draft, {
658
1266
  rules
659
1267
  }));
660
- };
1268
+ }, []);
661
1269
  return /*#__PURE__*/ _jsxs(CardDisplay, {
662
1270
  header: 'Categories & collections',
663
1271
  help: pluginDocsHelp('catalog', {
@@ -669,459 +1277,57 @@ const RULE_OPS = [
669
1277
  /*#__PURE__*/ _jsxs(Stack, {
670
1278
  spacing: 1,
671
1279
  children: [
672
- /*#__PURE__*/ _jsx(Typography, {
673
- variant: "subtitle2",
674
- children: 'Categories'
675
- }),
676
- categories.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
677
- variant: "body2",
678
- color: "text.secondary",
679
- children: 'Group products into a browsable tree (e.g. Brakes → Pads).'
680
- }) : visibleCategories.map((category)=>/*#__PURE__*/ _jsxs(Stack, {
681
- direction: "row",
682
- spacing: 1,
683
- sx: {
684
- alignItems: 'center',
685
- pl: category.depth * 2
686
- },
687
- children: [
688
- /*#__PURE__*/ _jsxs(Typography, {
689
- variant: "body2",
690
- sx: {
691
- flex: 1
692
- },
693
- noWrap: true,
694
- children: [
695
- category.name,
696
- /*#__PURE__*/ _jsx(Typography, {
697
- component: "span",
698
- variant: "caption",
699
- color: "text.secondary",
700
- children: ` /${category.slug}`
701
- })
702
- ]
703
- }),
704
- /*#__PURE__*/ _jsx(Button, {
705
- size: "small",
706
- onClick: ()=>{
707
- var _category_parentId;
708
- return setCategoryDraft({
709
- id: category.$id,
710
- name: category.name,
711
- parentId: (_category_parentId = category.parentId) != null ? _category_parentId : ''
712
- });
713
- },
714
- children: 'Edit'
715
- }),
716
- /*#__PURE__*/ _jsx(Button, {
717
- size: "small",
718
- color: "error",
719
- onClick: handleCategoryDelete(category),
720
- children: 'Delete'
721
- })
722
- ]
723
- }, category.$id)),
724
- categories.length === 0 ? null : /*#__PURE__*/ _jsx(ListPagination, {
1280
+ /*#__PURE__*/ _jsx(CategoriesSection, {
1281
+ categories: categories,
1282
+ visible: visibleCategories,
725
1283
  page: categoryPage,
726
1284
  pageSize: categoryPageSize,
727
- rowCount: visibleCategories.length,
728
- // The categories the card HOLDS — a client slice of rows already
729
- // read, so the total is known exactly.
730
- count: categories.length,
1285
+ truncated: categoriesTruncated,
731
1286
  onPageChange: setCategoryPage,
732
- onPageSizeChange: setCategoryPageSize
733
- }),
734
- categoriesTruncated ? /*#__PURE__*/ _jsx(Alert, {
735
- severity: "info",
736
- children: `Showing ${CATEGORY_CEILING} categories, ordered by id. This ` + 'catalog has more — a category whose parent is past that ' + 'boundary is drawn at the top level here rather than under it.'
737
- }) : null,
738
- /*#__PURE__*/ _jsx(Button, {
739
- size: "small",
740
- sx: {
741
- alignSelf: 'flex-start'
742
- },
743
- onClick: ()=>setCategoryDraft({
744
- id: null,
745
- name: '',
746
- parentId: ''
747
- }),
748
- children: 'Add category'
1287
+ onPageSizeChange: setCategoryPageSize,
1288
+ onEdit: setCategoryDraft,
1289
+ onDelete: handleCategoryDelete
749
1290
  }),
750
1291
  /*#__PURE__*/ _jsx(Divider, {
751
1292
  sx: {
752
1293
  my: 1
753
1294
  }
754
1295
  }),
755
- /*#__PURE__*/ _jsx(Typography, {
756
- variant: "subtitle2",
757
- children: 'Collections'
758
- }),
759
- visibleCollections.map((row)=>{
760
- var _row_mode;
761
- return /*#__PURE__*/ _jsxs(Stack, {
762
- direction: "row",
763
- spacing: 1,
764
- sx: {
765
- alignItems: 'center'
766
- },
767
- children: [
768
- /*#__PURE__*/ _jsxs(Typography, {
769
- variant: "body2",
770
- sx: {
771
- flex: 1
772
- },
773
- noWrap: true,
774
- children: [
775
- row.name,
776
- /*#__PURE__*/ _jsx(Typography, {
777
- component: "span",
778
- variant: "caption",
779
- color: "text.secondary",
780
- children: ` · ${(_row_mode = row.mode) != null ? _row_mode : 'manual'} · ${countPrefix}${collectionCount(row)} products`
781
- })
782
- ]
783
- }),
784
- /*#__PURE__*/ _jsx(Button, {
785
- size: "small",
786
- onClick: ()=>{
787
- var _row_slug, _row_mode, _row_productIds, _row_rules;
788
- return setCollectionDraft({
789
- id: row.$id,
790
- name: row.name,
791
- slug: (_row_slug = row.slug) != null ? _row_slug : '',
792
- mode: (_row_mode = row.mode) != null ? _row_mode : 'manual',
793
- productIds: (_row_productIds = row.productIds) != null ? _row_productIds : [],
794
- rules: (_row_rules = row.rules) != null ? _row_rules : [],
795
- matchAll: row.matchAll !== false
796
- });
797
- },
798
- children: 'Edit'
799
- }),
800
- /*#__PURE__*/ _jsx(Button, {
801
- size: "small",
802
- color: "error",
803
- onClick: handleCollectionDelete(row),
804
- children: 'Delete'
805
- })
806
- ]
807
- }, row.$id);
808
- }),
809
- commerceCollections.length === 0 ? null : /*#__PURE__*/ _jsx(ListPagination, {
1296
+ /*#__PURE__*/ _jsx(CollectionsSection, {
1297
+ collections: commerceCollections,
1298
+ visible: visibleCollections,
1299
+ products: products,
1300
+ countPrefix: countPrefix,
810
1301
  page: collectionPage,
811
1302
  pageSize: collectionPageSize,
812
- rowCount: visibleCollections.length,
813
- // The CATALOG collections, which is what this list is. Not the
814
- // window's length: content collections share the subcollection
815
- // and are classified out before this count is taken.
816
- count: commerceCollections.length,
1303
+ truncated: collectionsTruncated,
817
1304
  onPageChange: setCollectionPage,
818
- onPageSizeChange: setCollectionPageSize
819
- }),
820
- collectionsTruncated ? /*#__PURE__*/ _jsx(Alert, {
821
- severity: "info",
822
- children: `Showing ${COLLECTION_CEILING} collections, ordered by id. This ` + 'site has more — the slug check below only covers the ones ' + 'listed here, so an address may already be taken by one that ' + 'is not.'
823
- }) : null,
824
- /*#__PURE__*/ _jsx(Button, {
825
- size: "small",
826
- sx: {
827
- alignSelf: 'flex-start'
828
- },
829
- onClick: ()=>setCollectionDraft({
830
- id: null,
831
- name: '',
832
- slug: '',
833
- mode: 'manual',
834
- productIds: [],
835
- rules: [],
836
- matchAll: true
837
- }),
838
- children: 'Add collection'
1305
+ onPageSizeChange: setCollectionPageSize,
1306
+ onEdit: setCollectionDraft,
1307
+ onDelete: handleCollectionDelete
839
1308
  })
840
1309
  ]
841
1310
  }),
842
- /*#__PURE__*/ _jsxs(Dialog, {
843
- open: Boolean(categoryDraft),
844
- onClose: ()=>setCategoryDraft(null),
845
- maxWidth: "xs",
846
- fullWidth: true,
847
- children: [
848
- /*#__PURE__*/ _jsx(DialogTitle, {
849
- children: (categoryDraft == null ? void 0 : categoryDraft.id) ? 'Edit category' : 'New category'
850
- }),
851
- /*#__PURE__*/ _jsxs(DialogContent, {
852
- sx: {
853
- display: 'flex',
854
- flexDirection: 'column',
855
- gap: 2
856
- },
857
- children: [
858
- /*#__PURE__*/ _jsx(TextField, {
859
- label: "Name",
860
- value: (_ref = categoryDraft == null ? void 0 : categoryDraft.name) != null ? _ref : '',
861
- onChange: (event)=>setCategoryDraft((prev)=>prev ? _extends({}, prev, {
862
- name: event.target.value
863
- }) : prev),
864
- size: "small",
865
- autoFocus: true,
866
- sx: {
867
- mt: 1
868
- }
869
- }),
870
- /*#__PURE__*/ _jsxs(TextField, {
871
- label: "Parent category",
872
- value: (_ref1 = categoryDraft == null ? void 0 : categoryDraft.parentId) != null ? _ref1 : '',
873
- onChange: (event)=>setCategoryDraft((prev)=>prev ? _extends({}, prev, {
874
- parentId: event.target.value
875
- }) : prev),
876
- size: "small",
877
- select: true,
878
- children: [
879
- /*#__PURE__*/ _jsx(MenuItem, {
880
- value: "",
881
- children: 'None (top level)'
882
- }),
883
- categories.filter((category)=>category.$id !== (categoryDraft == null ? void 0 : categoryDraft.id)).map((category)=>/*#__PURE__*/ _jsx(MenuItem, {
884
- value: category.$id,
885
- children: `${'— '.repeat(category.depth)}${category.name}`
886
- }, category.$id))
887
- ]
888
- })
889
- ]
890
- }),
891
- /*#__PURE__*/ _jsxs(DialogActions, {
892
- children: [
893
- /*#__PURE__*/ _jsx(Button, {
894
- onClick: ()=>setCategoryDraft(null),
895
- children: 'Cancel'
896
- }),
897
- /*#__PURE__*/ _jsx(Button, {
898
- variant: "contained",
899
- color: "primary",
900
- disabled: !(categoryDraft == null ? void 0 : categoryDraft.name.trim()),
901
- onClick: handleCategorySave,
902
- children: 'Save'
903
- })
904
- ]
905
- })
906
- ]
1311
+ /*#__PURE__*/ _jsx(CategoryDialog, {
1312
+ draft: categoryDraft,
1313
+ categories: categories,
1314
+ setDraft: setCategoryDraft,
1315
+ onSave: handleCategorySave
907
1316
  }),
908
- /*#__PURE__*/ _jsxs(Dialog, {
909
- open: Boolean(collectionDraft),
910
- onClose: ()=>setCollectionDraft(null),
911
- maxWidth: "sm",
912
- fullWidth: true,
913
- children: [
914
- /*#__PURE__*/ _jsx(DialogTitle, {
915
- children: (collectionDraft == null ? void 0 : collectionDraft.id) ? 'Edit collection' : 'New collection'
916
- }),
917
- /*#__PURE__*/ _jsxs(DialogContent, {
918
- sx: {
919
- display: 'flex',
920
- flexDirection: 'column',
921
- gap: 2
922
- },
923
- children: [
924
- /*#__PURE__*/ _jsx(TextField, {
925
- label: "Name",
926
- value: (_ref2 = collectionDraft == null ? void 0 : collectionDraft.name) != null ? _ref2 : '',
927
- onChange: (event)=>setCollectionDraft((prev)=>prev ? _extends({}, prev, {
928
- name: event.target.value
929
- }) : prev),
930
- size: "small",
931
- autoFocus: true,
932
- sx: {
933
- mt: 1
934
- },
935
- error: slugTaken,
936
- helperText: (collectionDraft == null ? void 0 : collectionDraft.name) ? `/collections/${draftSlug}` : undefined
937
- }),
938
- /*#__PURE__*/ _jsxs(TextField, {
939
- label: "Mode",
940
- value: (_ref3 = collectionDraft == null ? void 0 : collectionDraft.mode) != null ? _ref3 : 'manual',
941
- onChange: (event)=>setCollectionDraft((prev)=>prev ? _extends({}, prev, {
942
- mode: event.target.value
943
- }) : prev),
944
- size: "small",
945
- select: true,
946
- children: [
947
- /*#__PURE__*/ _jsx(MenuItem, {
948
- value: "manual",
949
- children: 'Manual — pick products'
950
- }),
951
- /*#__PURE__*/ _jsx(MenuItem, {
952
- value: "smart",
953
- children: 'Smart — rule based'
954
- })
955
- ]
956
- }),
957
- (collectionDraft == null ? void 0 : collectionDraft.mode) === 'manual' ? /*#__PURE__*/ _jsx(Autocomplete, {
958
- multiple: true,
959
- options: products,
960
- getOptionLabel: (product)=>product.name,
961
- isOptionEqualToValue: (option, value)=>option.$id === value.$id,
962
- value: products.filter((product)=>{
963
- var _collectionDraft_productIds;
964
- return ((_collectionDraft_productIds = collectionDraft.productIds) != null ? _collectionDraft_productIds : []).includes(product.$id);
965
- }),
966
- onChange: (_event, picked)=>setCollectionDraft((prev)=>prev ? _extends({}, prev, {
967
- productIds: picked.map((item)=>item.$id)
968
- }) : prev),
969
- renderInput: (params)=>/*#__PURE__*/ _jsx(TextField, _extends({}, params, {
970
- label: "Products",
971
- size: "small"
972
- }))
973
- }) : /*#__PURE__*/ _jsxs(_Fragment, {
974
- children: [
975
- /*#__PURE__*/ _jsxs(Stack, {
976
- direction: "row",
977
- spacing: 1,
978
- sx: {
979
- alignItems: 'center'
980
- },
981
- children: [
982
- /*#__PURE__*/ _jsx(Typography, {
983
- variant: "body2",
984
- children: 'Match'
985
- }),
986
- /*#__PURE__*/ _jsx(Switch, {
987
- size: "small",
988
- checked: (collectionDraft == null ? void 0 : collectionDraft.matchAll) !== false,
989
- onChange: (event)=>setCollectionDraft((prev)=>prev ? _extends({}, prev, {
990
- matchAll: event.target.checked
991
- }) : prev)
992
- }),
993
- /*#__PURE__*/ _jsx(Typography, {
994
- variant: "body2",
995
- children: (collectionDraft == null ? void 0 : collectionDraft.matchAll) !== false ? 'all rules' : 'any rule'
996
- })
997
- ]
998
- }),
999
- ((_ref4 = collectionDraft == null ? void 0 : collectionDraft.rules) != null ? _ref4 : []).map((rule, index)=>/*#__PURE__*/ _jsxs(Stack, {
1000
- direction: "row",
1001
- spacing: 1,
1002
- children: [
1003
- /*#__PURE__*/ _jsx(TextField, {
1004
- value: rule.field,
1005
- onChange: (event)=>updateRule(index, {
1006
- field: event.target.value
1007
- }),
1008
- size: "small",
1009
- select: true,
1010
- sx: {
1011
- width: 130
1012
- },
1013
- children: RULE_FIELDS.map((field)=>/*#__PURE__*/ _jsx(MenuItem, {
1014
- value: field.value,
1015
- children: field.label
1016
- }, field.value))
1017
- }),
1018
- /*#__PURE__*/ _jsx(TextField, {
1019
- value: rule.op,
1020
- onChange: (event)=>updateRule(index, {
1021
- op: event.target.value
1022
- }),
1023
- size: "small",
1024
- select: true,
1025
- sx: {
1026
- width: 110
1027
- },
1028
- children: RULE_OPS.map((op)=>/*#__PURE__*/ _jsx(MenuItem, {
1029
- value: op.value,
1030
- children: op.label
1031
- }, op.value))
1032
- }),
1033
- rule.field === 'categoryId' ? /*#__PURE__*/ _jsx(TextField, {
1034
- value: rule.value,
1035
- onChange: (event)=>updateRule(index, {
1036
- value: event.target.value
1037
- }),
1038
- size: "small",
1039
- select: true,
1040
- sx: {
1041
- flex: 1
1042
- },
1043
- children: categories.map((category)=>/*#__PURE__*/ _jsx(MenuItem, {
1044
- value: category.$id,
1045
- children: category.name
1046
- }, category.$id))
1047
- }) : /*#__PURE__*/ _jsx(TextField, {
1048
- value: rule.value,
1049
- onChange: (event)=>updateRule(index, {
1050
- value: rule.field === 'priceUsd' ? Number(event.target.value) : event.target.value
1051
- }),
1052
- size: "small",
1053
- sx: {
1054
- flex: 1
1055
- },
1056
- placeholder: rule.field === 'type' ? 'physical' : 'Value'
1057
- }),
1058
- /*#__PURE__*/ _jsx(Button, {
1059
- size: "small",
1060
- color: "error",
1061
- onClick: ()=>updateRule(index, null),
1062
- children: '✕'
1063
- })
1064
- ]
1065
- }, index)),
1066
- /*#__PURE__*/ _jsx(Button, {
1067
- size: "small",
1068
- sx: {
1069
- alignSelf: 'flex-start'
1070
- },
1071
- onClick: ()=>{
1072
- var _ref;
1073
- var _collectionDraft_rules;
1074
- return updateRule((_ref = collectionDraft == null ? void 0 : (_collectionDraft_rules = collectionDraft.rules) == null ? void 0 : _collectionDraft_rules.length) != null ? _ref : 0, {});
1075
- },
1076
- children: 'Add rule'
1077
- })
1078
- ]
1079
- }),
1080
- collectionError ? /*#__PURE__*/ _jsx(Alert, {
1081
- severity: "warning",
1082
- children: collectionError
1083
- }) : null,
1084
- collectionDraft ? /*#__PURE__*/ _jsxs(Stack, {
1085
- direction: "row",
1086
- spacing: 0.5,
1087
- sx: {
1088
- flexWrap: 'wrap'
1089
- },
1090
- children: [
1091
- /*#__PURE__*/ _jsx(Typography, {
1092
- variant: "caption",
1093
- color: "text.secondary",
1094
- children: `Matches ${countPrefix}${previewMatches.length} products: `
1095
- }),
1096
- previewMatches.slice(0, 6).map((product)=>/*#__PURE__*/ _jsx(Chip, {
1097
- label: product.name,
1098
- size: "small"
1099
- }, product.$id)),
1100
- previewMatches.length > 6 ? /*#__PURE__*/ _jsx(Typography, {
1101
- variant: "caption",
1102
- color: "text.secondary",
1103
- children: `+${previewMatches.length - 6} more`
1104
- }) : null
1105
- ]
1106
- }) : null
1107
- ]
1108
- }),
1109
- /*#__PURE__*/ _jsxs(DialogActions, {
1110
- children: [
1111
- /*#__PURE__*/ _jsx(Button, {
1112
- onClick: ()=>setCollectionDraft(null),
1113
- children: 'Cancel'
1114
- }),
1115
- /*#__PURE__*/ _jsx(Button, {
1116
- variant: "contained",
1117
- color: "primary",
1118
- disabled: !(collectionDraft == null ? void 0 : collectionDraft.name.trim()) || Boolean(collectionError),
1119
- onClick: handleCollectionSave,
1120
- children: 'Save'
1121
- })
1122
- ]
1123
- })
1124
- ]
1317
+ /*#__PURE__*/ _jsx(CollectionDialog, {
1318
+ draft: collectionDraft,
1319
+ slug: draftSlug,
1320
+ slugTaken: slugTaken,
1321
+ error: collectionError,
1322
+ products: products,
1323
+ picked: collectionProducts,
1324
+ categories: categories,
1325
+ matches: previewMatches,
1326
+ countPrefix: countPrefix,
1327
+ setDraft: setCollectionDraft,
1328
+ onProducts: setCollectionProducts,
1329
+ onRule: updateRule,
1330
+ onSave: handleCollectionSave
1125
1331
  })
1126
1332
  ]
1127
1333
  });