@aglyn/plugins-commerce 1.0.0-beta.199 → 1.0.0-beta.201

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 (111) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/console/catalog-organization-card.component.js +48 -3
  3. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  4. package/src/lib/components/console/gift-cards-card.component.d.ts +9 -0
  5. package/src/lib/components/console/gift-cards-card.component.js +53 -28
  6. package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
  7. package/src/lib/components/console/host-orders-card.component.d.ts +26 -0
  8. package/src/lib/components/console/host-orders-card.component.js +264 -194
  9. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  10. package/src/lib/components/console/order-detail-dialog.component.js +12 -0
  11. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  12. package/src/lib/components/console/pos-page.component.d.ts +14 -0
  13. package/src/lib/components/console/pos-page.component.js +86 -45
  14. package/src/lib/components/console/pos-page.component.js.map +1 -1
  15. package/src/lib/components/console/product-editor-dialog.component.js +30 -6
  16. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  17. package/src/lib/components/console/product-slugs.d.ts +43 -0
  18. package/src/lib/components/console/product-slugs.js +60 -0
  19. package/src/lib/components/console/product-slugs.js.map +1 -0
  20. package/src/lib/components/console/products-hub-card.component.d.ts +2 -2
  21. package/src/lib/components/console/products-hub-card.component.js +229 -282
  22. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  23. package/src/lib/components/console/products-hub-zone.component.d.ts +10 -5
  24. package/src/lib/components/console/products-hub-zone.component.js +32 -9
  25. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  26. package/src/lib/components/console/recovery-queue-card.component.d.ts +37 -5
  27. package/src/lib/components/console/recovery-queue-card.component.js +107 -86
  28. package/src/lib/components/console/recovery-queue-card.component.js.map +1 -1
  29. package/src/lib/components/console/reservations-card.component.js +15 -3
  30. package/src/lib/components/console/reservations-card.component.js.map +1 -1
  31. package/src/lib/components/console/smart-collections.d.ts +32 -0
  32. package/src/lib/components/console/smart-collections.js +57 -0
  33. package/src/lib/components/console/smart-collections.js.map +1 -0
  34. package/src/lib/components/console/stock-movements-card.component.d.ts +13 -4
  35. package/src/lib/components/console/stock-movements-card.component.js +142 -180
  36. package/src/lib/components/console/stock-movements-card.component.js.map +1 -1
  37. package/src/lib/components/product-grid.d.ts +5 -0
  38. package/src/lib/components/product-grid.js +95 -34
  39. package/src/lib/components/product-grid.js.map +1 -1
  40. package/src/lib/constants/orders-list-query.d.ts +40 -0
  41. package/src/lib/constants/orders-list-query.js +205 -0
  42. package/src/lib/constants/orders-list-query.js.map +1 -0
  43. package/src/lib/constants/product-list-query.d.ts +42 -0
  44. package/src/lib/constants/product-list-query.js +217 -0
  45. package/src/lib/constants/product-list-query.js.map +1 -0
  46. package/src/lib/constants/stock-movement-query.d.ts +27 -0
  47. package/src/lib/constants/stock-movement-query.js +110 -0
  48. package/src/lib/constants/stock-movement-query.js.map +1 -0
  49. package/src/lib/constants/storefront-catalog-query.d.ts +103 -0
  50. package/src/lib/constants/storefront-catalog-query.js +312 -0
  51. package/src/lib/constants/storefront-catalog-query.js.map +1 -0
  52. package/src/lib/model/checkout-recovery.d.ts +37 -0
  53. package/src/lib/model/checkout-recovery.js +44 -0
  54. package/src/lib/model/checkout-recovery.js.map +1 -0
  55. package/src/lib/model/commerce-orders.d.ts +7 -0
  56. package/src/lib/model/commerce-orders.js.map +1 -1
  57. package/src/lib/model/commerce.d.ts +80 -0
  58. package/src/lib/model/commerce.js +67 -6
  59. package/src/lib/model/commerce.js.map +1 -1
  60. package/src/lib/model/gift-card-search.d.ts +24 -0
  61. package/src/lib/model/gift-card-search.js +48 -0
  62. package/src/lib/model/gift-card-search.js.map +1 -0
  63. package/src/lib/model/index.d.ts +1 -0
  64. package/src/lib/model/index.js +1 -0
  65. package/src/lib/model/index.js.map +1 -1
  66. package/src/lib/model/order-list-fields.d.ts +80 -0
  67. package/src/lib/model/order-list-fields.js +112 -0
  68. package/src/lib/model/order-list-fields.js.map +1 -0
  69. package/src/lib/server/billing-webhook.js +180 -99
  70. package/src/lib/server/billing-webhook.js.map +1 -1
  71. package/src/lib/server/cancel-order.js +6 -6
  72. package/src/lib/server/cancel-order.js.map +1 -1
  73. package/src/lib/server/cart-checkout.js +9 -2
  74. package/src/lib/server/cart-checkout.js.map +1 -1
  75. package/src/lib/server/catalog.d.ts +46 -30
  76. package/src/lib/server/catalog.js +507 -170
  77. package/src/lib/server/catalog.js.map +1 -1
  78. package/src/lib/server/checkout.js +3 -1
  79. package/src/lib/server/checkout.js.map +1 -1
  80. package/src/lib/server/collection-membership.d.ts +35 -0
  81. package/src/lib/server/collection-membership.js +172 -0
  82. package/src/lib/server/collection-membership.js.map +1 -0
  83. package/src/lib/server/draft-order.js +5 -3
  84. package/src/lib/server/draft-order.js.map +1 -1
  85. package/src/lib/server/gift-cards.js +7 -1
  86. package/src/lib/server/gift-cards.js.map +1 -1
  87. package/src/lib/server/member-name-search.d.ts +17 -0
  88. package/src/lib/server/member-name-search.js +23 -1
  89. package/src/lib/server/member-name-search.js.map +1 -1
  90. package/src/lib/server/membership-account.js +7 -3
  91. package/src/lib/server/membership-account.js.map +1 -1
  92. package/src/lib/server/membership-admin-password.js +8 -2
  93. package/src/lib/server/membership-admin-password.js.map +1 -1
  94. package/src/lib/server/membership-recover.js +4 -1
  95. package/src/lib/server/membership-recover.js.map +1 -1
  96. package/src/lib/server/membership-register.js +3 -1
  97. package/src/lib/server/membership-register.js.map +1 -1
  98. package/src/lib/server/pos-order.js +9 -5
  99. package/src/lib/server/pos-order.js.map +1 -1
  100. package/src/lib/server/process-abandoned.js +20 -7
  101. package/src/lib/server/process-abandoned.js.map +1 -1
  102. package/src/lib/server/process-restock.js +6 -2
  103. package/src/lib/server/process-restock.js.map +1 -1
  104. package/src/lib/server/reserve-stock.js +3 -4
  105. package/src/lib/server/reserve-stock.js.map +1 -1
  106. package/src/lib/server/reserve.js +3 -1
  107. package/src/lib/server/reserve.js.map +1 -1
  108. package/src/lib/server/site-page-enricher.js +3 -3
  109. package/src/lib/server/site-page-enricher.js.map +1 -1
  110. package/src/lib/server.js +4 -0
  111. package/src/lib/server.js.map +1 -1
@@ -19,70 +19,48 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
19
19
  import { CardDisplay } from "@aglyn/shared-ui-jsx";
20
20
  import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
21
21
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
22
+ import { ListQueryNotices, listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
22
23
  import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
23
- import { inMemoryListField } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
24
- import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
25
- import { useListRowsFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-rows-filter";
26
- import { Alert, Stack, Typography } from "@mui/material";
27
- import { collection, limit, orderBy, query } from "firebase/firestore";
28
- import { useEffect, useMemo, useState } from "react";
29
- import { useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
24
+ import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
25
+ import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
26
+ import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
27
+ import { Stack, Typography } from "@mui/material";
28
+ import { collection } from "firebase/firestore";
29
+ import { useMemo } from "react";
30
+ import { ceilingedWindow, collectionCeiling, useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
31
+ import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
30
32
  import { pluginDocsHelp } from "@aglyn/aglyn";
33
+ import { STOCK_MOVEMENT_FILTER_FIELDS, STOCK_MOVEMENT_FILTER_HEADERS, STOCK_MOVEMENT_QUERY, STOCK_MOVEMENT_REASON_LABEL, STOCK_MOVEMENT_SELECT_FIELDS } from "../../constants/stock-movement-query.js";
31
34
  /**
32
- * How many rows the listener holds.
33
- *
34
- * Ordered by `atMs` descending, which is a SINGLE-FIELD index Firestore
35
- * maintains automatically — no composite, so nothing here can drift from
36
- * `cloud/firebase-firestore.indexes.json`. Filtering by product happens over
37
- * this window rather than in the query for exactly that reason: an equality
38
- * filter plus this ordering is a composite index, and an index that has to be
39
- * deployed by hand is an index the feature ships without.
40
- */ const WINDOW = 100;
35
+ * How many products the card reads to NAME its rows and offer the Product
36
+ * filter's choices. A picker's reach, not the ledger's: the ledger itself is
37
+ * paged by its query and reaches every movement whatever this holds.
38
+ */ const PRODUCT_NAMES_CEILING = 500;
39
+ /** A site's locations, which the plan caps well under this. */ const LOCATION_CEILING = 25;
41
40
  const movementsHelp = pluginDocsHelp('commerce', {
42
41
  anchor: '#stock-movements',
43
42
  title: 'Stock movements',
44
43
  excerpt: 'Every change to a tracked count — sales, returns, cancellations and ' + 'hand adjustments — newest first, with the reason each one was made.'
45
44
  });
46
- /** The stored key is a closed set; these are what a merchant reads. */ const REASON_LABEL = {
47
- sale: 'Sale',
48
- refund: 'Refund return',
49
- restock: 'Restock',
50
- correction: 'Correction',
51
- damage: 'Damaged',
52
- cancellation: 'Order canceled'
53
- };
54
- /*
55
- * What the movements grid's Filters panel offers. The card holds its whole
56
- * window, so the panel and the search answer over every movement it read.
57
- * Product is picked by id and shown by name; Reason is the stored closed set.
58
- */ const MOVEMENT_FILTER_FIELDS = [
59
- inMemoryListField('productId', 'select'),
60
- inMemoryListField('reason', 'select'),
61
- inMemoryListField('atMs', 'date')
62
- ];
63
- const MOVEMENT_FILTER_HEADERS = {
64
- productId: 'Product',
65
- reason: 'Reason',
66
- atMs: 'When'
67
- };
68
- const REASON_OPTIONS = Object.entries(REASON_LABEL).map(([value, label])=>({
45
+ const REASON_OPTIONS = Object.entries(STOCK_MOVEMENT_REASON_LABEL).map(([value, label])=>({
69
46
  value,
70
47
  label
71
48
  }));
72
- /** What the quick search reads on a movement row. */ const MOVEMENT_SEARCH_FIELDS = [
73
- 'productName',
74
- 'variantId',
75
- 'orderId',
76
- 'locationId'
49
+ /** The columns the grid draws; every other declared field is a hidden filter column. */ const VISIBLE_COLUMNS = [
50
+ 'atMs',
51
+ 'productId',
52
+ 'delta',
53
+ 'reason',
54
+ 'source'
77
55
  ];
78
56
  /**
79
57
  * Stock movements (AGL-2341) — the adjustment history that had no history
80
58
  * view.
81
59
  *
82
- * `hosts/{hostId}/inventoryAdjustments` has five writers: the products hub's
83
- * "Adjust stock" dialog, the sale path in `billing-webhook.ts`, the POS in
84
- * `pos-order.ts`, and the restock in `cancel-order.ts`. The hub's own comment
85
- * calls the collection "adjustment history".
60
+ * `hosts/{hostId}/inventoryAdjustments` has three writers: the products
61
+ * hub's "Adjust stock" dialog, `decrementVariantStock` (the sale, whether the
62
+ * checkout webhook or the POS sold it), and the restock in `cancel-order.ts`.
63
+ * The hub's own comment calls the collection "adjustment history".
86
64
  *
87
65
  * Its only reader was arithmetic: `cancel-order.ts` projects `appliedDelta`
88
66
  * off the `reason: 'sale'` rows to cap what a cancellation may put back. No
@@ -97,125 +75,89 @@ const REASON_OPTIONS = Object.entries(REASON_LABEL).map(([value, label])=>({
97
75
  * zero. They differ rarely and they differ importantly — three sold out of a
98
76
  * count of zero is exactly the state a merchant is trying to explain — so
99
77
  * both are shown when they disagree and only `delta` when they do not.
78
+ *
79
+ * ## Every filter is on the query (AGL-3321)
80
+ *
81
+ * The ledger used to read its hundred newest movements and filter those, so
82
+ * "Damaged" searched a hundred rows and answered "none" about a ledger full
83
+ * of older ones. Now the Filters panel's clauses are the query's `where`s —
84
+ * see `STOCK_MOVEMENT_QUERY` for what each one costs — and the footer pages
85
+ * the query's answer, so page three of "Damaged" is the third page of damage
86
+ * the ledger holds, however far back it goes.
100
87
  */ export function StockMovementsCard(props) {
101
- var _ref;
102
88
  const { hostId } = props;
103
89
  const firestore = useFirestore();
104
- const { data: movementDocs } = useFirestoreCollection(()=>query(collection(firestore, 'hosts', hostId, 'inventoryAdjustments'), orderBy('atMs', 'desc'), /*
105
- * `WINDOW + 1` is a PROBE (AGL-2501). One document beyond the ceiling
106
- * turns "there are older movements than these" into a fact for the
107
- * price of a single read. The comparison it replaces —
108
- * `length >= WINDOW` — is wrong at exactly the count that equals the
109
- * ceiling, which is the one ledger size where a merchant is told
110
- * rows are missing and none are. The probe row is sliced off below
111
- * and never rendered.
112
- */ limit(WINDOW + 1)), [
90
+ // Names, so a row reads as "Desk lamp" rather than a 20-character id, and
91
+ // the Product filter's choices.
92
+ const { data: productDocs } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'products'), PRODUCT_NAMES_CEILING), [
113
93
  firestore,
114
94
  hostId
115
95
  ], {
116
96
  idField: '$id'
117
97
  });
118
- // Names, so a row reads as "Desk lamp" rather than a 20-character id. The
119
- // products hub above runs this exact query, so the SDK shares one listener.
120
- const { data: productDocs } = useFirestoreCollection(()=>query(collection(firestore, 'hosts', hostId, 'products'), limit(500)), [
98
+ const { data: locationDocs } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'locations'), LOCATION_CEILING), [
121
99
  firestore,
122
100
  hostId
123
101
  ], {
124
102
  idField: '$id'
125
103
  });
104
+ const products = useMemo(()=>ceilingedWindow(productDocs != null ? productDocs : undefined, PRODUCT_NAMES_CEILING).rows, [
105
+ productDocs
106
+ ]);
126
107
  const productNames = useMemo(()=>{
127
108
  const names = new Map();
128
- for (const product of productDocs != null ? productDocs : []){
109
+ for (const product of products){
129
110
  var _product_name;
130
111
  names.set(product.$id, (_product_name = product.name) != null ? _product_name : product.$id);
131
112
  }
132
113
  return names;
133
114
  }, [
134
- productDocs
115
+ products
135
116
  ]);
136
- /** Read beyond the ceiling — so the ledger holds more than was read. */ const truncated = ((_ref = movementDocs == null ? void 0 : movementDocs.length) != null ? _ref : 0) > WINDOW;
137
- /** The window itself, probe row removed. */ const windowRows = useMemo(()=>(movementDocs != null ? movementDocs : []).slice(0, WINDOW), [
138
- movementDocs
117
+ const filterOptions = useMemo(()=>({
118
+ productId: [
119
+ ...productNames
120
+ ].map(([value, label])=>({
121
+ value,
122
+ label
123
+ })).sort((a, b)=>a.label.localeCompare(b.label)),
124
+ reason: REASON_OPTIONS,
125
+ locationId: ceilingedWindow(locationDocs != null ? locationDocs : undefined, LOCATION_CEILING).rows.map((location)=>{
126
+ var _location_name;
127
+ return {
128
+ value: location.$id,
129
+ label: (_location_name = location.name) != null ? _location_name : location.$id
130
+ };
131
+ })
132
+ }), [
133
+ productNames,
134
+ locationDocs
139
135
  ]);
140
- const windowMovements = useMemo(()=>[
141
- ...windowRows
142
- ]// The query already orders, but a cached snapshot can arrive before
143
- // the server's and the sort is what makes "newest first" a promise
144
- // rather than a hope.
145
- .sort((a, b)=>{
146
- var _b_atMs, _a_atMs;
147
- return Number((_b_atMs = b.atMs) != null ? _b_atMs : 0) - Number((_a_atMs = a.atMs) != null ? _a_atMs : 0);
148
- }).map((row)=>{
136
+ const gridFilter = useListGridFilter({
137
+ selectFields: STOCK_MOVEMENT_SELECT_FIELDS
138
+ });
139
+ const { rows: movementRows, hasMore, page, setPage, pageSize, setPageSize, plan, status } = useListQuery({
140
+ collection: collection(firestore, 'hosts', hostId, 'inventoryAdjustments'),
141
+ declaration: STOCK_MOVEMENT_QUERY,
142
+ request: {
143
+ clauses: gridFilter.clauses
144
+ },
145
+ deps: [
146
+ firestore,
147
+ hostId
148
+ ],
149
+ idField: '$id'
150
+ });
151
+ const movements = useMemo(()=>movementRows.map((row)=>{
149
152
  var _productNames_get;
150
153
  return _extends({}, row, {
151
154
  productName: (_productNames_get = productNames.get(row.productId)) != null ? _productNames_get : row.productId
152
155
  });
153
156
  }), [
154
- windowRows,
157
+ movementRows,
155
158
  productNames
156
159
  ]);
157
- /** Products that actually appear in the window — filtering to an empty
158
- * option is a dead end, and the whole catalog is not the answer here. */ const productOptions = useMemo(()=>{
159
- const ids = new Set();
160
- for (const row of windowRows){
161
- if (row.productId) ids.add(row.productId);
162
- }
163
- return [
164
- ...ids
165
- ].map((id)=>{
166
- var _productNames_get;
167
- return {
168
- value: id,
169
- label: (_productNames_get = productNames.get(id)) != null ? _productNames_get : id
170
- };
171
- }).sort((a, b)=>a.label.localeCompare(b.label));
172
- }, [
173
- windowRows,
174
- productNames
175
- ]);
176
- const filterOptions = useMemo(()=>({
177
- productId: productOptions,
178
- reason: REASON_OPTIONS
179
- }), [
180
- productOptions
181
- ]);
182
- const listFilter = useListRowsFilter({
183
- rows: windowMovements,
184
- fields: MOVEMENT_FILTER_FIELDS,
185
- options: filterOptions,
186
- headers: MOVEMENT_FILTER_HEADERS,
187
- search: MOVEMENT_SEARCH_FIELDS
188
- });
189
- const movements = listFilter.rows;
190
- /*
191
- * The page is a SLICE of the window, and the window stays where it is.
192
- *
193
- * Paging the QUERY would be the cheaper read and the wrong control here,
194
- * because the grid's filters run in the browser: on a ten-row server page
195
- * "Damaged" would search ten movements instead of a hundred and answer "no
196
- * stock movements" about a ledger full of them. Moving those filters to the
197
- * server is what would earn a query-level page, and it cannot be done
198
- * without composite indexes this feature would then ship without — see
199
- * `WINDOW`. So the read is unchanged and the wall of a hundred rows becomes
200
- * a footer over them.
201
- */ const [page, setPage] = useState(0);
202
- const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
203
- // A filter narrows the list under the reader's feet, and page four of the
204
- // unfiltered ledger is not a position in the filtered one — MUI renders an
205
- // out-of-range page as an empty table with no explanation, which reads as
206
- // the filter having matched nothing.
207
- const searchKey = listFilter.gridFilter.searchWords.join(' ');
208
- useEffect(()=>setPage(0), [
209
- listFilter.gridFilter.clauses,
210
- searchKey
211
- ]);
212
- const shown = useMemo(()=>movements.slice(page * pageSize, page * pageSize + pageSize), [
213
- movements,
214
- page,
215
- pageSize
216
- ]);
217
- const { filterColumns } = listFilter;
218
- const columns = useMemo(()=>filterColumns([
160
+ const columns = useMemo(()=>listFilterGridColumns([
219
161
  {
220
162
  field: 'atMs',
221
163
  headerName: 'When',
@@ -271,8 +213,8 @@ const REASON_OPTIONS = Object.entries(REASON_LABEL).map(([value, label])=>({
271
213
  headerName: 'Reason',
272
214
  width: 150,
273
215
  renderCell: ({ row })=>{
274
- var _REASON_LABEL_row_reason;
275
- return (_REASON_LABEL_row_reason = REASON_LABEL[row.reason]) != null ? _REASON_LABEL_row_reason : row.reason;
216
+ var _STOCK_MOVEMENT_REASON_LABEL_row_reason;
217
+ return (_STOCK_MOVEMENT_REASON_LABEL_row_reason = STOCK_MOVEMENT_REASON_LABEL[row.reason]) != null ? _STOCK_MOVEMENT_REASON_LABEL_row_reason : row.reason;
276
218
  }
277
219
  },
278
220
  {
@@ -280,7 +222,6 @@ const REASON_OPTIONS = Object.entries(REASON_LABEL).map(([value, label])=>({
280
222
  headerName: 'Source',
281
223
  flex: 1,
282
224
  minWidth: 160,
283
- sortable: false,
284
225
  renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
285
226
  variant: "caption",
286
227
  color: "text.secondary",
@@ -290,52 +231,73 @@ const REASON_OPTIONS = Object.entries(REASON_LABEL).map(([value, label])=>({
290
231
  ].filter(Boolean).join(' · ') || '—'
291
232
  })
292
233
  }
293
- ]), [
294
- filterColumns
234
+ ], STOCK_MOVEMENT_FILTER_FIELDS, filterOptions, STOCK_MOVEMENT_FILTER_HEADERS), [
235
+ filterOptions
295
236
  ]);
237
+ const filtering = gridFilter.clauses.length > 0;
238
+ // The ledger is empty only when an UNFILTERED first page came back empty;
239
+ // a filter that matches nothing keeps the grid, its chips and its panel.
240
+ const empty = status === 'success' && !filtering && page === 0 && movements.length === 0;
296
241
  return /*#__PURE__*/ _jsx(CardDisplay, {
297
242
  header: "Stock movements",
298
243
  help: movementsHelp,
299
244
  contentGutterX: true,
300
245
  contentGutterY: true,
301
- children: /*#__PURE__*/ _jsxs(Stack, {
246
+ children: /*#__PURE__*/ _jsx(Stack, {
302
247
  spacing: 2,
303
- children: [
304
- windowMovements.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
305
- variant: "body2",
306
- color: "text.secondary",
307
- children: 'No stock movements recorded yet. Sales, returns, cancellations ' + 'and hand adjustments all land here.'
308
- }) : /*#__PURE__*/ _jsxs(_Fragment, {
309
- children: [
310
- /*#__PURE__*/ _jsx(ListFilterChips, _extends({}, listFilter.chipsProps)),
311
- /*#__PURE__*/ _jsx(ListTable, _extends({
312
- "aria-label": "Stock movements",
313
- rows: shown,
314
- columns: columns
315
- }, listFilter.gridProps, {
316
- // `ListPagination` below pages the matches.
317
- hideFooter: true,
318
- noRowsLabel: "No stock movements match these filters"
319
- }))
320
- ]
321
- }),
322
- movements.length === 0 ? null : /*#__PURE__*/ _jsx(ListPagination, {
323
- page: page,
324
- pageSize: pageSize,
325
- rowCount: shown.length,
326
- // The movements matching the filters, which this card genuinely
327
- // holds. What it does not know is how many are older than the
328
- // window, and the notice below says so rather than letting the
329
- // count line imply a ledger total it cannot see.
330
- count: movements.length,
331
- onPageChange: setPage,
332
- onPageSizeChange: setPageSize
333
- }),
334
- truncated ? /*#__PURE__*/ _jsx(Alert, {
335
- severity: "info",
336
- children: `Paging the ${WINDOW} most recent movements. Older rows are ` + 'kept and reachable through a data export.'
337
- }) : null
338
- ]
248
+ children: empty ? /*#__PURE__*/ _jsx(Typography, {
249
+ variant: "body2",
250
+ color: "text.secondary",
251
+ children: 'No stock movements recorded yet. Sales, returns, cancellations ' + 'and hand adjustments all land here.'
252
+ }) : /*#__PURE__*/ _jsxs(_Fragment, {
253
+ children: [
254
+ /*#__PURE__*/ _jsx(ListFilterChips, {
255
+ fields: STOCK_MOVEMENT_FILTER_FIELDS,
256
+ headers: STOCK_MOVEMENT_FILTER_HEADERS,
257
+ clauses: gridFilter.clauses,
258
+ onChange: gridFilter.setClauses,
259
+ options: filterOptions
260
+ }),
261
+ /*#__PURE__*/ _jsx(ListQueryNotices, {
262
+ refused: listQueryRefusals(plan.refused, {
263
+ fields: STOCK_MOVEMENT_FILTER_FIELDS,
264
+ headers: STOCK_MOVEMENT_FILTER_HEADERS,
265
+ options: filterOptions
266
+ }),
267
+ notices: plan.notices
268
+ }),
269
+ /*#__PURE__*/ _jsx(ListTable, {
270
+ "aria-label": "Stock movements",
271
+ rows: movements,
272
+ columns: columns,
273
+ /*
274
+ * The grid must NOT also filter or sort. The query answers
275
+ * both, and the ledger has one order — newest first — which
276
+ * the query already holds.
277
+ */ filterMode: "server",
278
+ filterModel: gridFilter.filterModel,
279
+ onFilterModelChange: gridFilter.onFilterModelChange,
280
+ sortingMode: "server",
281
+ disableColumnSorting: true,
282
+ // `ListPagination` below pages the query.
283
+ hideFooter: true,
284
+ noRowsLabel: "No stock movements match these filters",
285
+ initialState: {
286
+ columns: {
287
+ columnVisibilityModel: hiddenFilterVisibility(STOCK_MOVEMENT_FILTER_FIELDS, VISIBLE_COLUMNS)
288
+ }
289
+ }
290
+ }),
291
+ /*#__PURE__*/ _jsx(ListPagination, {
292
+ page: page,
293
+ pageSize: pageSize,
294
+ rowCount: movements.length,
295
+ hasMore: hasMore,
296
+ onPageChange: setPage,
297
+ onPageSizeChange: setPageSize
298
+ })
299
+ ]
300
+ })
339
301
  })
340
302
  });
341
303
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/stock-movements-card.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 { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { inMemoryListField } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useListRowsFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-rows-filter'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, limit, orderBy, query } from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport * as CommerceModel from '../../model'\n\nexport interface StockMovementsCardProps {\n hostId: string\n}\n\n/**\n * How many rows the listener holds.\n *\n * Ordered by `atMs` descending, which is a SINGLE-FIELD index Firestore\n * maintains automatically — no composite, so nothing here can drift from\n * `cloud/firebase-firestore.indexes.json`. Filtering by product happens over\n * this window rather than in the query for exactly that reason: an equality\n * filter plus this ordering is a composite index, and an index that has to be\n * deployed by hand is an index the feature ships without.\n */\nconst WINDOW = 100\n\nconst movementsHelp = pluginDocsHelp('commerce', {\n anchor: '#stock-movements',\n title: 'Stock movements',\n excerpt:\n 'Every change to a tracked count — sales, returns, cancellations and ' +\n 'hand adjustments — newest first, with the reason each one was made.',\n})\n\n/** The stored key is a closed set; these are what a merchant reads. */\nconst REASON_LABEL: Record<CommerceModel.InventoryAdjustmentReason, string> = {\n sale: 'Sale',\n refund: 'Refund return',\n restock: 'Restock',\n correction: 'Correction',\n damage: 'Damaged',\n cancellation: 'Order canceled',\n}\n\ntype MovementRow = CommerceModel.InventoryAdjustment & { $id: string }\n\n/*\n * What the movements grid's Filters panel offers. The card holds its whole\n * window, so the panel and the search answer over every movement it read.\n * Product is picked by id and shown by name; Reason is the stored closed set.\n */\nconst MOVEMENT_FILTER_FIELDS = [\n inMemoryListField('productId', 'select'),\n inMemoryListField('reason', 'select'),\n inMemoryListField('atMs', 'date'),\n]\nconst MOVEMENT_FILTER_HEADERS: Readonly<Record<string, string>> = {\n productId: 'Product',\n reason: 'Reason',\n atMs: 'When',\n}\nconst REASON_OPTIONS = Object.entries(REASON_LABEL).map(([value, label]) => ({\n value,\n label,\n}))\n/** What the quick search reads on a movement row. */\nconst MOVEMENT_SEARCH_FIELDS = ['productName', 'variantId', 'orderId', 'locationId'] as const\n\n/**\n * Stock movements (AGL-2341) — the adjustment history that had no history\n * view.\n *\n * `hosts/{hostId}/inventoryAdjustments` has five writers: the products hub's\n * \"Adjust stock\" dialog, the sale path in `billing-webhook.ts`, the POS in\n * `pos-order.ts`, and the restock in `cancel-order.ts`. The hub's own comment\n * calls the collection \"adjustment history\".\n *\n * Its only reader was arithmetic: `cancel-order.ts` projects `appliedDelta`\n * off the `reason: 'sale'` rows to cap what a cancellation may put back. No\n * surface displayed a single one of them. A merchant whose count disagreed\n * with the shelf had no way to see what moved it, when, or why — and the one\n * number that reconciles the discrepancy was written on every adjustment and\n * unreachable.\n *\n * `delta` is what the merchant's history says moved; `appliedDelta` is what\n * the count could actually give up when the floor in `adjustVariantInventory`\n * absorbed part of it, which happens on a backorder product selling past\n * zero. They differ rarely and they differ importantly — three sold out of a\n * count of zero is exactly the state a merchant is trying to explain — so\n * both are shown when they disagree and only `delta` when they do not.\n */\nexport function StockMovementsCard(props: StockMovementsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n\n const { data: movementDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'inventoryAdjustments'),\n orderBy('atMs', 'desc'),\n /*\n * `WINDOW + 1` is a PROBE (AGL-2501). One document beyond the ceiling\n * turns \"there are older movements than these\" into a fact for the\n * price of a single read. The comparison it replaces —\n * `length >= WINDOW` — is wrong at exactly the count that equals the\n * ceiling, which is the one ledger size where a merchant is told\n * rows are missing and none are. The probe row is sliced off below\n * and never rendered.\n */\n limit(WINDOW + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n // Names, so a row reads as \"Desk lamp\" rather than a 20-character id. The\n // products hub above runs this exact query, so the SDK shares one listener.\n const { data: productDocs } = useFirestoreCollection<any>(\n () =>\n query(collection(firestore, 'hosts', hostId, 'products'), limit(500)),\n [firestore, hostId],\n { idField: '$id' },\n )\n\n const productNames = useMemo(() => {\n const names = new Map<string, string>()\n for (const product of productDocs ?? []) {\n names.set(product.$id, product.name ?? product.$id)\n }\n return names\n }, [productDocs])\n\n /** Read beyond the ceiling — so the ledger holds more than was read. */\n const truncated = (movementDocs?.length ?? 0) > WINDOW\n /** The window itself, probe row removed. */\n const windowRows = useMemo(\n () => ((movementDocs ?? []) as MovementRow[]).slice(0, WINDOW),\n [movementDocs],\n )\n\n const windowMovements = useMemo(\n () =>\n [...windowRows]\n // The query already orders, but a cached snapshot can arrive before\n // the server's and the sort is what makes \"newest first\" a promise\n // rather than a hope.\n .sort((a, b) => Number(b.atMs ?? 0) - Number(a.atMs ?? 0))\n .map((row) => ({\n ...row,\n productName: productNames.get(row.productId) ?? row.productId,\n })),\n [windowRows, productNames],\n )\n\n /** Products that actually appear in the window — filtering to an empty\n * option is a dead end, and the whole catalog is not the answer here. */\n const productOptions = useMemo(() => {\n const ids = new Set<string>()\n for (const row of windowRows) {\n if (row.productId) ids.add(row.productId)\n }\n return [...ids]\n .map((id) => ({ value: id, label: productNames.get(id) ?? id }))\n .sort((a, b) => a.label.localeCompare(b.label))\n }, [windowRows, productNames])\n const filterOptions = useMemo(\n () => ({ productId: productOptions, reason: REASON_OPTIONS }),\n [productOptions],\n )\n const listFilter = useListRowsFilter({\n rows: windowMovements,\n fields: MOVEMENT_FILTER_FIELDS,\n options: filterOptions,\n headers: MOVEMENT_FILTER_HEADERS,\n search: MOVEMENT_SEARCH_FIELDS,\n })\n const movements = listFilter.rows\n\n /*\n * The page is a SLICE of the window, and the window stays where it is.\n *\n * Paging the QUERY would be the cheaper read and the wrong control here,\n * because the grid's filters run in the browser: on a ten-row server page\n * \"Damaged\" would search ten movements instead of a hundred and answer \"no\n * stock movements\" about a ledger full of them. Moving those filters to the\n * server is what would earn a query-level page, and it cannot be done\n * without composite indexes this feature would then ship without — see\n * `WINDOW`. So the read is unchanged and the wall of a hundred rows becomes\n * a footer over them.\n */\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n // A filter narrows the list under the reader's feet, and page four of the\n // unfiltered ledger is not a position in the filtered one — MUI renders an\n // out-of-range page as an empty table with no explanation, which reads as\n // the filter having matched nothing.\n const searchKey = listFilter.gridFilter.searchWords.join(' ')\n useEffect(() => setPage(0), [listFilter.gridFilter.clauses, searchKey])\n const shown = useMemo(\n () => movements.slice(page * pageSize, page * pageSize + pageSize),\n [movements, page, pageSize],\n )\n\n const { filterColumns } = listFilter\n const columns = useMemo(\n () =>\n filterColumns([\n {\n field: 'atMs',\n headerName: 'When',\n width: 190,\n renderCell: ({ row }: { row: MovementRow }) =>\n row.atMs ? new Date(Number(row.atMs)).toLocaleString() : '—',\n },\n {\n field: 'productId',\n headerName: 'Product',\n flex: 1,\n minWidth: 180,\n renderCell: ({ row }: { row: MovementRow & { productName: string } }) => (\n <span>\n {row.productName}\n {row.variantId ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {` · ${row.variantId}`}\n </Typography>\n ) : null}\n </span>\n ),\n },\n {\n field: 'delta',\n headerName: 'Change',\n type: 'number',\n width: 150,\n align: 'right',\n headerAlign: 'right',\n renderCell: ({ row }: { row: MovementRow }) => {\n const delta = Number(row.delta ?? 0)\n const applied = Number(row.appliedDelta ?? delta)\n return (\n <span>\n {/*\n * The sign is carried explicitly. \"3\" and \"-3\" are the same\n * width and opposite facts, and a merchant scanning a column\n * for the movement that broke their count reads the sign\n * before the number.\n */}\n <Typography\n variant=\"body2\"\n color={delta < 0 ? 'error.main' : 'success.main'}\n component=\"span\"\n >\n {delta > 0 ? `+${delta}` : String(delta)}\n </Typography>\n {applied !== delta ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {` (${applied > 0 ? `+${applied}` : applied} applied)`}\n </Typography>\n ) : null}\n </span>\n )\n },\n },\n {\n field: 'reason',\n headerName: 'Reason',\n width: 150,\n renderCell: ({ row }: { row: MovementRow }) =>\n REASON_LABEL[row.reason] ?? row.reason,\n },\n {\n field: 'source',\n headerName: 'Source',\n flex: 1,\n minWidth: 160,\n sortable: false,\n renderCell: ({ row }: { row: MovementRow }) => (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {[\n row.orderId ? `order ${row.orderId}` : null,\n row.locationId ? `at ${row.locationId}` : null,\n ]\n .filter(Boolean)\n .join(' · ') || '—'}\n </Typography>\n ),\n },\n ] as GridColDef[]),\n [filterColumns],\n )\n\n return (\n <CardDisplay header=\"Stock movements\" help={movementsHelp} contentGutterX contentGutterY>\n <Stack spacing={2}>\n {windowMovements.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No stock movements recorded yet. Sales, returns, cancellations ' +\n 'and hand adjustments all land here.'}\n </Typography>\n ) : (\n <>\n <ListFilterChips {...listFilter.chipsProps} />\n <ListTable\n aria-label=\"Stock movements\"\n rows={shown}\n columns={columns}\n {...listFilter.gridProps}\n // `ListPagination` below pages the matches.\n hideFooter\n noRowsLabel=\"No stock movements match these filters\"\n />\n </>\n )}\n {movements.length === 0 ? null : (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={shown.length}\n // The movements matching the filters, which this card genuinely\n // holds. What it does not know is how many are older than the\n // window, and the notice below says so rather than letting the\n // count line imply a ledger total it cannot see.\n count={movements.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n {truncated ? (\n <Alert severity=\"info\">\n {`Paging the ${WINDOW} most recent movements. Older rows are ` +\n 'kept and reachable through a data export.'}\n </Alert>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nStockMovementsCard.displayName = 'StockMovementsCard'\n\nexport default StockMovementsCard\n"],"names":["CardDisplay","ListFilterChips","ListPagination","ListTable","inMemoryListField","TABLE_PAGE_SIZE_DEFAULT","useListRowsFilter","Alert","Stack","Typography","collection","limit","orderBy","query","useEffect","useMemo","useState","useFirestore","useFirestoreCollection","pluginDocsHelp","WINDOW","movementsHelp","anchor","title","excerpt","REASON_LABEL","sale","refund","restock","correction","damage","cancellation","MOVEMENT_FILTER_FIELDS","MOVEMENT_FILTER_HEADERS","productId","reason","atMs","REASON_OPTIONS","Object","entries","map","value","label","MOVEMENT_SEARCH_FIELDS","StockMovementsCard","props","hostId","firestore","data","movementDocs","idField","productDocs","productNames","names","Map","product","set","$id","name","truncated","length","windowRows","slice","windowMovements","sort","a","b","Number","row","productName","get","productOptions","ids","Set","add","id","localeCompare","filterOptions","listFilter","rows","fields","options","headers","search","movements","page","setPage","pageSize","setPageSize","searchKey","gridFilter","searchWords","join","clauses","shown","filterColumns","columns","field","headerName","width","renderCell","Date","toLocaleString","flex","minWidth","span","variantId","variant","color","type","align","headerAlign","delta","applied","appliedDelta","component","String","sortable","orderId","locationId","filter","Boolean","header","help","contentGutterX","contentGutterY","spacing","chipsProps","aria-label","gridProps","hideFooter","noRowsLabel","rowCount","count","onPageChange","onPageSizeChange","severity","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/stock-movements-card.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 { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListQueryNotices,\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport * as CommerceModel from '../../model'\nimport {\n STOCK_MOVEMENT_FILTER_FIELDS,\n STOCK_MOVEMENT_FILTER_HEADERS,\n STOCK_MOVEMENT_QUERY,\n STOCK_MOVEMENT_REASON_LABEL,\n STOCK_MOVEMENT_SELECT_FIELDS,\n} from '../../constants/stock-movement-query'\n\nexport interface StockMovementsCardProps {\n hostId: string\n}\n\n/**\n * How many products the card reads to NAME its rows and offer the Product\n * filter's choices. A picker's reach, not the ledger's: the ledger itself is\n * paged by its query and reaches every movement whatever this holds.\n */\nconst PRODUCT_NAMES_CEILING = 500\n/** A site's locations, which the plan caps well under this. */\nconst LOCATION_CEILING = 25\n\nconst movementsHelp = pluginDocsHelp('commerce', {\n anchor: '#stock-movements',\n title: 'Stock movements',\n excerpt:\n 'Every change to a tracked count — sales, returns, cancellations and ' +\n 'hand adjustments — newest first, with the reason each one was made.',\n})\n\nconst REASON_OPTIONS = Object.entries(STOCK_MOVEMENT_REASON_LABEL).map(([value, label]) => ({\n value,\n label,\n}))\n\n/** The columns the grid draws; every other declared field is a hidden filter column. */\nconst VISIBLE_COLUMNS = ['atMs', 'productId', 'delta', 'reason', 'source']\n\ntype MovementRow = CommerceModel.InventoryAdjustment & { $id: string }\n\n/**\n * Stock movements (AGL-2341) — the adjustment history that had no history\n * view.\n *\n * `hosts/{hostId}/inventoryAdjustments` has three writers: the products\n * hub's \"Adjust stock\" dialog, `decrementVariantStock` (the sale, whether the\n * checkout webhook or the POS sold it), and the restock in `cancel-order.ts`.\n * The hub's own comment calls the collection \"adjustment history\".\n *\n * Its only reader was arithmetic: `cancel-order.ts` projects `appliedDelta`\n * off the `reason: 'sale'` rows to cap what a cancellation may put back. No\n * surface displayed a single one of them. A merchant whose count disagreed\n * with the shelf had no way to see what moved it, when, or why — and the one\n * number that reconciles the discrepancy was written on every adjustment and\n * unreachable.\n *\n * `delta` is what the merchant's history says moved; `appliedDelta` is what\n * the count could actually give up when the floor in `adjustVariantInventory`\n * absorbed part of it, which happens on a backorder product selling past\n * zero. They differ rarely and they differ importantly — three sold out of a\n * count of zero is exactly the state a merchant is trying to explain — so\n * both are shown when they disagree and only `delta` when they do not.\n *\n * ## Every filter is on the query (AGL-3321)\n *\n * The ledger used to read its hundred newest movements and filter those, so\n * \"Damaged\" searched a hundred rows and answered \"none\" about a ledger full\n * of older ones. Now the Filters panel's clauses are the query's `where`s —\n * see `STOCK_MOVEMENT_QUERY` for what each one costs — and the footer pages\n * the query's answer, so page three of \"Damaged\" is the third page of damage\n * the ledger holds, however far back it goes.\n */\nexport function StockMovementsCard(props: StockMovementsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n\n // Names, so a row reads as \"Desk lamp\" rather than a 20-character id, and\n // the Product filter's choices.\n const { data: productDocs } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'products'),\n PRODUCT_NAMES_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { data: locationDocs } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'locations'),\n LOCATION_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const products = useMemo(\n () => ceilingedWindow<any>(productDocs ?? undefined, PRODUCT_NAMES_CEILING).rows,\n [productDocs],\n )\n const productNames = useMemo(() => {\n const names = new Map<string, string>()\n for (const product of products) {\n names.set(product.$id, product.name ?? product.$id)\n }\n return names\n }, [products])\n\n const filterOptions = useMemo(\n () => ({\n productId: [...productNames]\n .map(([value, label]) => ({ value, label }))\n .sort((a, b) => a.label.localeCompare(b.label)),\n reason: REASON_OPTIONS,\n locationId: ceilingedWindow<any>(locationDocs ?? undefined, LOCATION_CEILING).rows.map(\n (location: any) => ({ value: location.$id, label: location.name ?? location.$id }),\n ),\n }),\n [productNames, locationDocs],\n )\n\n const gridFilter = useListGridFilter({ selectFields: STOCK_MOVEMENT_SELECT_FIELDS })\n const {\n rows: movementRows,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n status,\n } = useListQuery<MovementRow>({\n collection: collection(firestore, 'hosts', hostId, 'inventoryAdjustments'),\n declaration: STOCK_MOVEMENT_QUERY,\n request: { clauses: gridFilter.clauses },\n deps: [firestore, hostId],\n idField: '$id',\n })\n const movements = useMemo(\n () =>\n movementRows.map((row) => ({\n ...row,\n productName: productNames.get(row.productId) ?? row.productId,\n })),\n [movementRows, productNames],\n )\n\n const columns = useMemo(\n () =>\n listFilterGridColumns(\n [\n {\n field: 'atMs',\n headerName: 'When',\n width: 190,\n renderCell: ({ row }: { row: MovementRow }) =>\n row.atMs ? new Date(Number(row.atMs)).toLocaleString() : '—',\n },\n {\n field: 'productId',\n headerName: 'Product',\n flex: 1,\n minWidth: 180,\n renderCell: ({ row }: { row: MovementRow & { productName: string } }) => (\n <span>\n {row.productName}\n {row.variantId ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {` · ${row.variantId}`}\n </Typography>\n ) : null}\n </span>\n ),\n },\n {\n field: 'delta',\n headerName: 'Change',\n type: 'number',\n width: 150,\n align: 'right',\n headerAlign: 'right',\n renderCell: ({ row }: { row: MovementRow }) => {\n const delta = Number(row.delta ?? 0)\n const applied = Number(row.appliedDelta ?? delta)\n return (\n <span>\n {/*\n * The sign is carried explicitly. \"3\" and \"-3\" are the same\n * width and opposite facts, and a merchant scanning a column\n * for the movement that broke their count reads the sign\n * before the number.\n */}\n <Typography\n variant=\"body2\"\n color={delta < 0 ? 'error.main' : 'success.main'}\n component=\"span\"\n >\n {delta > 0 ? `+${delta}` : String(delta)}\n </Typography>\n {applied !== delta ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {` (${applied > 0 ? `+${applied}` : applied} applied)`}\n </Typography>\n ) : null}\n </span>\n )\n },\n },\n {\n field: 'reason',\n headerName: 'Reason',\n width: 150,\n renderCell: ({ row }: { row: MovementRow }) =>\n STOCK_MOVEMENT_REASON_LABEL[row.reason] ?? row.reason,\n },\n {\n field: 'source',\n headerName: 'Source',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }: { row: MovementRow }) => (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {[\n row.orderId ? `order ${row.orderId}` : null,\n row.locationId ? `at ${row.locationId}` : null,\n ]\n .filter(Boolean)\n .join(' · ') || '—'}\n </Typography>\n ),\n },\n ] as GridColDef[],\n STOCK_MOVEMENT_FILTER_FIELDS,\n filterOptions,\n STOCK_MOVEMENT_FILTER_HEADERS,\n ),\n [filterOptions],\n )\n\n const filtering = gridFilter.clauses.length > 0\n // The ledger is empty only when an UNFILTERED first page came back empty;\n // a filter that matches nothing keeps the grid, its chips and its panel.\n const empty = status === 'success' && !filtering && page === 0 && movements.length === 0\n\n return (\n <CardDisplay header=\"Stock movements\" help={movementsHelp} contentGutterX contentGutterY>\n <Stack spacing={2}>\n {empty ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No stock movements recorded yet. Sales, returns, cancellations ' +\n 'and hand adjustments all land here.'}\n </Typography>\n ) : (\n <>\n <ListFilterChips\n fields={STOCK_MOVEMENT_FILTER_FIELDS}\n headers={STOCK_MOVEMENT_FILTER_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={filterOptions}\n />\n <ListQueryNotices\n refused={listQueryRefusals(plan.refused, {\n fields: STOCK_MOVEMENT_FILTER_FIELDS,\n headers: STOCK_MOVEMENT_FILTER_HEADERS,\n options: filterOptions,\n })}\n notices={plan.notices}\n />\n <ListTable\n aria-label=\"Stock movements\"\n rows={movements}\n columns={columns}\n /*\n * The grid must NOT also filter or sort. The query answers\n * both, and the ledger has one order — newest first — which\n * the query already holds.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n sortingMode=\"server\"\n disableColumnSorting\n // `ListPagination` below pages the query.\n hideFooter\n noRowsLabel=\"No stock movements match these filters\"\n initialState={{\n columns: {\n columnVisibilityModel: hiddenFilterVisibility(\n STOCK_MOVEMENT_FILTER_FIELDS,\n VISIBLE_COLUMNS,\n ),\n },\n }}\n />\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={movements.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nStockMovementsCard.displayName = 'StockMovementsCard'\n\nexport default StockMovementsCard\n"],"names":["CardDisplay","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","Stack","Typography","collection","useMemo","ceilingedWindow","collectionCeiling","useFirestore","useFirestoreCollection","useListQuery","pluginDocsHelp","STOCK_MOVEMENT_FILTER_FIELDS","STOCK_MOVEMENT_FILTER_HEADERS","STOCK_MOVEMENT_QUERY","STOCK_MOVEMENT_REASON_LABEL","STOCK_MOVEMENT_SELECT_FIELDS","PRODUCT_NAMES_CEILING","LOCATION_CEILING","movementsHelp","anchor","title","excerpt","REASON_OPTIONS","Object","entries","map","value","label","VISIBLE_COLUMNS","StockMovementsCard","props","hostId","firestore","data","productDocs","idField","locationDocs","products","undefined","rows","productNames","names","Map","product","set","$id","name","filterOptions","productId","sort","a","b","localeCompare","reason","locationId","location","gridFilter","selectFields","movementRows","hasMore","page","setPage","pageSize","setPageSize","plan","status","declaration","request","clauses","deps","movements","row","productName","get","columns","field","headerName","width","renderCell","atMs","Date","Number","toLocaleString","flex","minWidth","span","variantId","variant","color","type","align","headerAlign","delta","applied","appliedDelta","component","String","orderId","filter","Boolean","join","filtering","length","empty","header","help","contentGutterX","contentGutterY","spacing","fields","headers","onChange","setClauses","options","refused","notices","aria-label","filterMode","filterModel","onFilterModelChange","sortingMode","disableColumnSorting","hideFooter","noRowsLabel","initialState","columnVisibilityModel","rowCount","onPageChange","onPageSizeChange","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,WAAW,QAAQ,uBAAsB;AAClD,OAAOC,qBAAqB,8DAA6D;AACzF,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,SACEC,gBAAgB,EAChBC,iBAAiB,QACZ,+DAA8D;AACrE,SAASC,SAAS,QAAQ,uDAAsD;AAChF,SAASC,sBAAsB,QAAQ,yCAAwC;AAC/E,SAASC,qBAAqB,QAAQ,8CAA6C;AACnF,SAASC,iBAAiB,QAAQ,kDAAiD;AACnF,SAASC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAEjD,SAASC,UAAU,QAAQ,qBAAoB;AAC/C,SAASC,OAAO,QAAQ,QAAO;AAC/B,SACEC,eAAe,EACfC,iBAAiB,EACjBC,YAAY,EACZC,sBAAsB,QACjB,iCAAgC;AACvC,SAASC,YAAY,QAAQ,sDAAqD;AAClF,SAASC,cAAc,QAAQ,eAAc;AAE7C,SACEC,4BAA4B,EAC5BC,6BAA6B,EAC7BC,oBAAoB,EACpBC,2BAA2B,EAC3BC,4BAA4B,QACvB,0CAAsC;AAM7C;;;;CAIC,GACD,MAAMC,wBAAwB;AAC9B,6DAA6D,GAC7D,MAAMC,mBAAmB;AAEzB,MAAMC,gBAAgBR,eAAe,YAAY;IAC/CS,QAAQ;IACRC,OAAO;IACPC,SACE,yEACA;AACJ;AAEA,MAAMC,iBAAiBC,OAAOC,OAAO,CAACV,6BAA6BW,GAAG,CAAC,CAAC,CAACC,OAAOC,MAAM,GAAM,CAAA;QAC1FD;QACAC;IACF,CAAA;AAEA,sFAAsF,GACtF,MAAMC,kBAAkB;IAAC;IAAQ;IAAa;IAAS;IAAU;CAAS;AAI1E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BC,GACD,OAAO,SAASC,mBAAmBC,KAA8B;IAC/D,MAAM,EAAEC,MAAM,EAAE,GAAGD;IACnB,MAAME,YAAYzB;IAElB,0EAA0E;IAC1E,gCAAgC;IAChC,MAAM,EAAE0B,MAAMC,WAAW,EAAE,GAAG1B,uBAC5B,IACEF,kBACEH,WAAW6B,WAAW,SAASD,QAAQ,aACvCf,wBAEJ;QAACgB;QAAWD;KAAO,EACnB;QAAEI,SAAS;IAAM;IAEnB,MAAM,EAAEF,MAAMG,YAAY,EAAE,GAAG5B,uBAC7B,IACEF,kBACEH,WAAW6B,WAAW,SAASD,QAAQ,cACvCd,mBAEJ;QAACe;QAAWD;KAAO,EACnB;QAAEI,SAAS;IAAM;IAEnB,MAAME,WAAWjC,QACf,IAAMC,gBAAqB6B,sBAAAA,cAAeI,WAAWtB,uBAAuBuB,IAAI,EAChF;QAACL;KAAY;IAEf,MAAMM,eAAepC,QAAQ;QAC3B,MAAMqC,QAAQ,IAAIC;QAClB,KAAK,MAAMC,WAAWN,SAAU;gBACPM;YAAvBF,MAAMG,GAAG,CAACD,QAAQE,GAAG,GAAEF,gBAAAA,QAAQG,IAAI,YAAZH,gBAAgBA,QAAQE,GAAG;QACpD;QACA,OAAOJ;IACT,GAAG;QAACJ;KAAS;IAEb,MAAMU,gBAAgB3C,QACpB,IAAO,CAAA;YACL4C,WAAW;mBAAIR;aAAa,CACzBf,GAAG,CAAC,CAAC,CAACC,OAAOC,MAAM,GAAM,CAAA;oBAAED;oBAAOC;gBAAM,CAAA,GACxCsB,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEvB,KAAK,CAACyB,aAAa,CAACD,EAAExB,KAAK;YAC/C0B,QAAQ/B;YACRgC,YAAYjD,gBAAqB+B,uBAAAA,eAAgBE,WAAWrB,kBAAkBsB,IAAI,CAACd,GAAG,CACpF,CAAC8B;oBAAiDA;uBAA9B;oBAAE7B,OAAO6B,SAASV,GAAG;oBAAElB,KAAK,GAAE4B,iBAAAA,SAAST,IAAI,YAAbS,iBAAiBA,SAASV,GAAG;gBAAC;;QAEpF,CAAA,GACA;QAACL;QAAcJ;KAAa;IAG9B,MAAMoB,aAAaxD,kBAAkB;QAAEyD,cAAc1C;IAA6B;IAClF,MAAM,EACJwB,MAAMmB,YAAY,EAClBC,OAAO,EACPC,IAAI,EACJC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACXC,IAAI,EACJC,MAAM,EACP,GAAGxD,aAA0B;QAC5BN,YAAYA,WAAW6B,WAAW,SAASD,QAAQ;QACnDmC,aAAarD;QACbsD,SAAS;YAAEC,SAASZ,WAAWY,OAAO;QAAC;QACvCC,MAAM;YAACrC;YAAWD;SAAO;QACzBI,SAAS;IACX;IACA,MAAMmC,YAAYlE,QAChB,IACEsD,aAAajC,GAAG,CAAC,CAAC8C;gBAEH/B;mBAFY,aACtB+B;gBACHC,WAAW,GAAEhC,oBAAAA,aAAaiC,GAAG,CAACF,IAAIvB,SAAS,aAA9BR,oBAAmC+B,IAAIvB,SAAS;;YAEjE;QAACU;QAAclB;KAAa;IAG9B,MAAMkC,UAAUtE,QACd,IACEL,sBACE;YACE;gBACE4E,OAAO;gBACPC,YAAY;gBACZC,OAAO;gBACPC,YAAY,CAAC,EAAEP,GAAG,EAAwB,GACxCA,IAAIQ,IAAI,GAAG,IAAIC,KAAKC,OAAOV,IAAIQ,IAAI,GAAGG,cAAc,KAAK;YAC7D;YACA;gBACEP,OAAO;gBACPC,YAAY;gBACZO,MAAM;gBACNC,UAAU;gBACVN,YAAY,CAAC,EAAEP,GAAG,EAAkD,iBAClE,MAACc;;4BACEd,IAAIC,WAAW;4BACfD,IAAIe,SAAS,iBACZ,KAACpF;gCAAWqF,SAAQ;gCAAUC,OAAM;0CACjC,CAAC,GAAG,EAAEjB,IAAIe,SAAS,EAAE;iCAEtB;;;YAGV;YACA;gBACEX,OAAO;gBACPC,YAAY;gBACZa,MAAM;gBACNZ,OAAO;gBACPa,OAAO;gBACPC,aAAa;gBACbb,YAAY,CAAC,EAAEP,GAAG,EAAwB;wBACnBA,YACEA;oBADvB,MAAMqB,QAAQX,QAAOV,aAAAA,IAAIqB,KAAK,YAATrB,aAAa;oBAClC,MAAMsB,UAAUZ,QAAOV,oBAAAA,IAAIuB,YAAY,YAAhBvB,oBAAoBqB;oBAC3C,qBACE,MAACP;;0CAOC,KAACnF;gCACCqF,SAAQ;gCACRC,OAAOI,QAAQ,IAAI,eAAe;gCAClCG,WAAU;0CAETH,QAAQ,IAAI,CAAC,CAAC,EAAEA,OAAO,GAAGI,OAAOJ;;4BAEnCC,YAAYD,sBACX,KAAC1F;gCAAWqF,SAAQ;gCAAUC,OAAM;0CACjC,CAAC,EAAE,EAAEK,UAAU,IAAI,CAAC,CAAC,EAAEA,SAAS,GAAGA,QAAQ,SAAS,CAAC;iCAEtD;;;gBAGV;YACF;YACA;gBACElB,OAAO;gBACPC,YAAY;gBACZC,OAAO;gBACPC,YAAY,CAAC,EAAEP,GAAG,EAAwB;wBACxCzD;4BAAAA,0CAAAA,2BAA2B,CAACyD,IAAIlB,MAAM,CAAC,YAAvCvC,0CAA2CyD,IAAIlB,MAAM;;YACzD;YACA;gBACEsB,OAAO;gBACPC,YAAY;gBACZO,MAAM;gBACNC,UAAU;gBACVN,YAAY,CAAC,EAAEP,GAAG,EAAwB,iBACxC,KAACrE;wBAAWqF,SAAQ;wBAAUC,OAAM;kCACjC;4BACCjB,IAAI0B,OAAO,GAAG,CAAC,MAAM,EAAE1B,IAAI0B,OAAO,EAAE,GAAG;4BACvC1B,IAAIjB,UAAU,GAAG,CAAC,GAAG,EAAEiB,IAAIjB,UAAU,EAAE,GAAG;yBAC3C,CACE4C,MAAM,CAACC,SACPC,IAAI,CAAC,UAAU;;YAGxB;SACD,EACDzF,8BACAoC,eACAnC,gCAEJ;QAACmC;KAAc;IAGjB,MAAMsD,YAAY7C,WAAWY,OAAO,CAACkC,MAAM,GAAG;IAC9C,0EAA0E;IAC1E,yEAAyE;IACzE,MAAMC,QAAQtC,WAAW,aAAa,CAACoC,aAAazC,SAAS,KAAKU,UAAUgC,MAAM,KAAK;IAEvF,qBACE,KAAC9G;QAAYgH,QAAO;QAAkBC,MAAMvF;QAAewF,cAAc;QAACC,cAAc;kBACtF,cAAA,KAAC1G;YAAM2G,SAAS;sBACbL,sBACC,KAACrG;gBAAWqF,SAAQ;gBAAQC,OAAM;0BAC/B,oEACC;+BAGJ;;kCACE,KAAC/F;wBACCoH,QAAQlG;wBACRmG,SAASlG;wBACTwD,SAASZ,WAAWY,OAAO;wBAC3B2C,UAAUvD,WAAWwD,UAAU;wBAC/BC,SAASlE;;kCAEX,KAACpD;wBACCuH,SAAStH,kBAAkBoE,KAAKkD,OAAO,EAAE;4BACvCL,QAAQlG;4BACRmG,SAASlG;4BACTqG,SAASlE;wBACX;wBACAoE,SAASnD,KAAKmD,OAAO;;kCAEvB,KAACtH;wBACCuH,cAAW;wBACX7E,MAAM+B;wBACNI,SAASA;wBACT;;;;eAIC,GACD2C,YAAW;wBACXC,aAAa9D,WAAW8D,WAAW;wBACnCC,qBAAqB/D,WAAW+D,mBAAmB;wBACnDC,aAAY;wBACZC,oBAAoB;wBACpB,0CAA0C;wBAC1CC,UAAU;wBACVC,aAAY;wBACZC,cAAc;4BACZlD,SAAS;gCACPmD,uBAAuB/H,uBACrBa,8BACAiB;4BAEJ;wBACF;;kCAEF,KAAClC;wBACCkE,MAAMA;wBACNE,UAAUA;wBACVgE,UAAUxD,UAAUgC,MAAM;wBAC1B3C,SAASA;wBACToE,cAAclE;wBACdmE,kBAAkBjE;;;;;;AAOhC;AACAlC,mBAAmBoG,WAAW,GAAG;AAEjC,eAAepG,mBAAkB"}
@@ -60,6 +60,11 @@ export interface ProductGridProps {
60
60
  * optional debounced search, category chips, type chips, a sort
61
61
  * select, and Load more paging — all resolved server-side by the
62
62
  * catalog handler so large catalogs stay cheap.
63
+ *
64
+ * Nothing is narrowed here (AGL-3321): every control is a parameter of the
65
+ * catalog query, and the rows shown are the rows it returned. A control the
66
+ * query could not combine with another comes back in `refused` and is said
67
+ * above the grid, rather than applied to the cards that happen to be loaded.
63
68
  */
64
69
  declare const ProductGrid: import("react").ForwardRefExoticComponent<ProductGridProps & import("react").RefAttributes<HTMLDivElement>>;
65
70
  export declare const schema: Aglyn.ComponentSchema<ProductGridProps>;