@aglyn/plugins-commerce 1.0.0-beta.188 → 1.0.0-beta.190
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -13
- package/src/lib/components/console/host-orders-card.component.js +333 -337
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +235 -215
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/stock-movements-card.component.js +166 -154
- package/src/lib/components/console/stock-movements-card.component.js.map +1 -1
|
@@ -14,12 +14,16 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
|
-
import {
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
19
|
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
20
|
+
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
19
21
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
22
|
+
import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
23
|
+
import { inMemoryListField } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
20
24
|
import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
21
|
-
import {
|
|
22
|
-
import { Alert,
|
|
25
|
+
import { useListRowsFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-rows-filter";
|
|
26
|
+
import { Alert, Stack, Typography } from "@mui/material";
|
|
23
27
|
import { collection, limit, orderBy, query } from "firebase/firestore";
|
|
24
28
|
import { useEffect, useMemo, useState } from "react";
|
|
25
29
|
import { useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
|
|
@@ -47,6 +51,30 @@ const movementsHelp = pluginDocsHelp('commerce', {
|
|
|
47
51
|
damage: 'Damaged',
|
|
48
52
|
cancellation: 'Order canceled'
|
|
49
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])=>({
|
|
69
|
+
value,
|
|
70
|
+
label
|
|
71
|
+
}));
|
|
72
|
+
/** What the quick search reads on a movement row. */ const MOVEMENT_SEARCH_FIELDS = [
|
|
73
|
+
'productName',
|
|
74
|
+
'variantId',
|
|
75
|
+
'orderId',
|
|
76
|
+
'locationId'
|
|
77
|
+
];
|
|
50
78
|
/**
|
|
51
79
|
* Stock movements (AGL-2341) — the adjustment history that had no history
|
|
52
80
|
* view.
|
|
@@ -73,8 +101,6 @@ const movementsHelp = pluginDocsHelp('commerce', {
|
|
|
73
101
|
var _ref;
|
|
74
102
|
const { hostId } = props;
|
|
75
103
|
const firestore = useFirestore();
|
|
76
|
-
const [productFilter, setProductFilter] = useState('all');
|
|
77
|
-
const [reasonFilter, setReasonFilter] = useState('all');
|
|
78
104
|
const { data: movementDocs } = useFirestoreCollection(()=>query(collection(firestore, 'hosts', hostId, 'inventoryAdjustments'), orderBy('atMs', 'desc'), /*
|
|
79
105
|
* `WINDOW + 1` is a PROBE (AGL-2501). One document beyond the ceiling
|
|
80
106
|
* turns "there are older movements than these" into a fact for the
|
|
@@ -111,7 +137,7 @@ const movementsHelp = pluginDocsHelp('commerce', {
|
|
|
111
137
|
/** The window itself, probe row removed. */ const windowRows = useMemo(()=>(movementDocs != null ? movementDocs : []).slice(0, WINDOW), [
|
|
112
138
|
movementDocs
|
|
113
139
|
]);
|
|
114
|
-
const
|
|
140
|
+
const windowMovements = useMemo(()=>[
|
|
115
141
|
...windowRows
|
|
116
142
|
]// The query already orders, but a cached snapshot can arrive before
|
|
117
143
|
// the server's and the sort is what makes "newest first" a promise
|
|
@@ -119,16 +145,53 @@ const movementsHelp = pluginDocsHelp('commerce', {
|
|
|
119
145
|
.sort((a, b)=>{
|
|
120
146
|
var _b_atMs, _a_atMs;
|
|
121
147
|
return Number((_b_atMs = b.atMs) != null ? _b_atMs : 0) - Number((_a_atMs = a.atMs) != null ? _a_atMs : 0);
|
|
122
|
-
}).
|
|
148
|
+
}).map((row)=>{
|
|
149
|
+
var _productNames_get;
|
|
150
|
+
return _extends({}, row, {
|
|
151
|
+
productName: (_productNames_get = productNames.get(row.productId)) != null ? _productNames_get : row.productId
|
|
152
|
+
});
|
|
153
|
+
}), [
|
|
154
|
+
windowRows,
|
|
155
|
+
productNames
|
|
156
|
+
]);
|
|
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
|
+
}, [
|
|
123
173
|
windowRows,
|
|
124
|
-
|
|
125
|
-
|
|
174
|
+
productNames
|
|
175
|
+
]);
|
|
176
|
+
const filterOptions = useMemo(()=>({
|
|
177
|
+
productId: productOptions,
|
|
178
|
+
reason: REASON_OPTIONS
|
|
179
|
+
}), [
|
|
180
|
+
productOptions
|
|
126
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;
|
|
127
190
|
/*
|
|
128
191
|
* The page is a SLICE of the window, and the window stays where it is.
|
|
129
192
|
*
|
|
130
193
|
* Paging the QUERY would be the cheaper read and the wrong control here,
|
|
131
|
-
* because
|
|
194
|
+
* because the grid's filters run in the browser: on a ten-row server page
|
|
132
195
|
* "Damaged" would search ten movements instead of a hundred and answer "no
|
|
133
196
|
* stock movements" about a ledger full of them. Moving those filters to the
|
|
134
197
|
* server is what would earn a query-level page, and it cannot be done
|
|
@@ -141,30 +204,94 @@ const movementsHelp = pluginDocsHelp('commerce', {
|
|
|
141
204
|
// unfiltered ledger is not a position in the filtered one — MUI renders an
|
|
142
205
|
// out-of-range page as an empty table with no explanation, which reads as
|
|
143
206
|
// the filter having matched nothing.
|
|
207
|
+
const searchKey = listFilter.gridFilter.searchWords.join(' ');
|
|
144
208
|
useEffect(()=>setPage(0), [
|
|
145
|
-
|
|
146
|
-
|
|
209
|
+
listFilter.gridFilter.clauses,
|
|
210
|
+
searchKey
|
|
147
211
|
]);
|
|
148
212
|
const shown = useMemo(()=>movements.slice(page * pageSize, page * pageSize + pageSize), [
|
|
149
213
|
movements,
|
|
150
214
|
page,
|
|
151
215
|
pageSize
|
|
152
216
|
]);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
217
|
+
const { filterColumns } = listFilter;
|
|
218
|
+
const columns = useMemo(()=>filterColumns([
|
|
219
|
+
{
|
|
220
|
+
field: 'atMs',
|
|
221
|
+
headerName: 'When',
|
|
222
|
+
width: 190,
|
|
223
|
+
renderCell: ({ row })=>row.atMs ? new Date(Number(row.atMs)).toLocaleString() : '—'
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
field: 'productId',
|
|
227
|
+
headerName: 'Product',
|
|
228
|
+
flex: 1,
|
|
229
|
+
minWidth: 180,
|
|
230
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsxs("span", {
|
|
231
|
+
children: [
|
|
232
|
+
row.productName,
|
|
233
|
+
row.variantId ? /*#__PURE__*/ _jsx(Typography, {
|
|
234
|
+
variant: "caption",
|
|
235
|
+
color: "text.secondary",
|
|
236
|
+
children: ` · ${row.variantId}`
|
|
237
|
+
}) : null
|
|
238
|
+
]
|
|
239
|
+
})
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
field: 'delta',
|
|
243
|
+
headerName: 'Change',
|
|
244
|
+
type: 'number',
|
|
245
|
+
width: 150,
|
|
246
|
+
align: 'right',
|
|
247
|
+
headerAlign: 'right',
|
|
248
|
+
renderCell: ({ row })=>{
|
|
249
|
+
var _row_delta, _row_appliedDelta;
|
|
250
|
+
const delta = Number((_row_delta = row.delta) != null ? _row_delta : 0);
|
|
251
|
+
const applied = Number((_row_appliedDelta = row.appliedDelta) != null ? _row_appliedDelta : delta);
|
|
252
|
+
return /*#__PURE__*/ _jsxs("span", {
|
|
253
|
+
children: [
|
|
254
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
255
|
+
variant: "body2",
|
|
256
|
+
color: delta < 0 ? 'error.main' : 'success.main',
|
|
257
|
+
component: "span",
|
|
258
|
+
children: delta > 0 ? `+${delta}` : String(delta)
|
|
259
|
+
}),
|
|
260
|
+
applied !== delta ? /*#__PURE__*/ _jsx(Typography, {
|
|
261
|
+
variant: "caption",
|
|
262
|
+
color: "text.secondary",
|
|
263
|
+
children: ` (${applied > 0 ? `+${applied}` : applied} applied)`
|
|
264
|
+
}) : null
|
|
265
|
+
]
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
field: 'reason',
|
|
271
|
+
headerName: 'Reason',
|
|
272
|
+
width: 150,
|
|
273
|
+
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;
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
field: 'source',
|
|
280
|
+
headerName: 'Source',
|
|
281
|
+
flex: 1,
|
|
282
|
+
minWidth: 160,
|
|
283
|
+
sortable: false,
|
|
284
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
|
|
285
|
+
variant: "caption",
|
|
286
|
+
color: "text.secondary",
|
|
287
|
+
children: [
|
|
288
|
+
row.orderId ? `order ${row.orderId}` : null,
|
|
289
|
+
row.locationId ? `at ${row.locationId}` : null
|
|
290
|
+
].filter(Boolean).join(' · ') || '—'
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
]), [
|
|
294
|
+
filterColumns
|
|
168
295
|
]);
|
|
169
296
|
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
170
297
|
header: "Stock movements",
|
|
@@ -174,137 +301,22 @@ const movementsHelp = pluginDocsHelp('commerce', {
|
|
|
174
301
|
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
175
302
|
spacing: 2,
|
|
176
303
|
children: [
|
|
177
|
-
/*#__PURE__*/
|
|
178
|
-
direction: "row",
|
|
179
|
-
spacing: 2,
|
|
180
|
-
children: [
|
|
181
|
-
/*#__PURE__*/ _jsxs(TextField, {
|
|
182
|
-
label: "Product",
|
|
183
|
-
size: "small",
|
|
184
|
-
select: true,
|
|
185
|
-
value: productFilter,
|
|
186
|
-
onChange: (event)=>setProductFilter(event.target.value),
|
|
187
|
-
sx: {
|
|
188
|
-
minWidth: 200
|
|
189
|
-
},
|
|
190
|
-
children: [
|
|
191
|
-
/*#__PURE__*/ _jsx(MenuItem, {
|
|
192
|
-
value: "all",
|
|
193
|
-
children: 'All products'
|
|
194
|
-
}),
|
|
195
|
-
filterableProducts.map((productId)=>{
|
|
196
|
-
var _productNames_get;
|
|
197
|
-
return /*#__PURE__*/ _jsx(MenuItem, {
|
|
198
|
-
value: productId,
|
|
199
|
-
children: (_productNames_get = productNames.get(productId)) != null ? _productNames_get : productId
|
|
200
|
-
}, productId);
|
|
201
|
-
})
|
|
202
|
-
]
|
|
203
|
-
}),
|
|
204
|
-
/*#__PURE__*/ _jsxs(TextField, {
|
|
205
|
-
label: "Reason",
|
|
206
|
-
size: "small",
|
|
207
|
-
select: true,
|
|
208
|
-
value: reasonFilter,
|
|
209
|
-
onChange: (event)=>setReasonFilter(event.target.value),
|
|
210
|
-
sx: {
|
|
211
|
-
minWidth: 180
|
|
212
|
-
},
|
|
213
|
-
children: [
|
|
214
|
-
/*#__PURE__*/ _jsx(MenuItem, {
|
|
215
|
-
value: "all",
|
|
216
|
-
children: 'Every reason'
|
|
217
|
-
}),
|
|
218
|
-
Object.entries(REASON_LABEL).map(([value, label])=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
219
|
-
value: value,
|
|
220
|
-
children: label
|
|
221
|
-
}, value))
|
|
222
|
-
]
|
|
223
|
-
})
|
|
224
|
-
]
|
|
225
|
-
}),
|
|
226
|
-
movements.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
|
|
304
|
+
windowMovements.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
|
|
227
305
|
variant: "body2",
|
|
228
306
|
color: "text.secondary",
|
|
229
307
|
children: 'No stock movements recorded yet. Sales, returns, cancellations ' + 'and hand adjustments all land here.'
|
|
230
|
-
}) : /*#__PURE__*/ _jsxs(
|
|
231
|
-
size: "small",
|
|
308
|
+
}) : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
232
309
|
children: [
|
|
233
|
-
/*#__PURE__*/ _jsx(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
align: "right",
|
|
244
|
-
children: 'Change'
|
|
245
|
-
}),
|
|
246
|
-
/*#__PURE__*/ _jsx(TableCell, {
|
|
247
|
-
children: 'Reason'
|
|
248
|
-
}),
|
|
249
|
-
/*#__PURE__*/ _jsx(TableCell, {
|
|
250
|
-
children: 'Source'
|
|
251
|
-
})
|
|
252
|
-
]
|
|
253
|
-
})
|
|
254
|
-
}),
|
|
255
|
-
/*#__PURE__*/ _jsx(TableBody, {
|
|
256
|
-
children: shown.map((row)=>{
|
|
257
|
-
var _row_delta, _row_appliedDelta, _productNames_get, _REASON_LABEL_row_reason;
|
|
258
|
-
const delta = Number((_row_delta = row.delta) != null ? _row_delta : 0);
|
|
259
|
-
const applied = Number((_row_appliedDelta = row.appliedDelta) != null ? _row_appliedDelta : delta);
|
|
260
|
-
return /*#__PURE__*/ _jsxs(TableRow, {
|
|
261
|
-
children: [
|
|
262
|
-
/*#__PURE__*/ _jsx(TableCell, {
|
|
263
|
-
children: row.atMs ? new Date(Number(row.atMs)).toLocaleString() : '—'
|
|
264
|
-
}),
|
|
265
|
-
/*#__PURE__*/ _jsxs(TableCell, {
|
|
266
|
-
children: [
|
|
267
|
-
(_productNames_get = productNames.get(row.productId)) != null ? _productNames_get : row.productId,
|
|
268
|
-
row.variantId ? /*#__PURE__*/ _jsx(Typography, {
|
|
269
|
-
variant: "caption",
|
|
270
|
-
color: "text.secondary",
|
|
271
|
-
children: ` · ${row.variantId}`
|
|
272
|
-
}) : null
|
|
273
|
-
]
|
|
274
|
-
}),
|
|
275
|
-
/*#__PURE__*/ _jsxs(TableCell, {
|
|
276
|
-
align: "right",
|
|
277
|
-
children: [
|
|
278
|
-
/*#__PURE__*/ _jsx(Typography, {
|
|
279
|
-
variant: "body2",
|
|
280
|
-
color: delta < 0 ? 'error.main' : 'success.main',
|
|
281
|
-
component: "span",
|
|
282
|
-
children: delta > 0 ? `+${delta}` : String(delta)
|
|
283
|
-
}),
|
|
284
|
-
applied !== delta ? /*#__PURE__*/ _jsx(Typography, {
|
|
285
|
-
variant: "caption",
|
|
286
|
-
color: "text.secondary",
|
|
287
|
-
children: ` (${applied > 0 ? `+${applied}` : applied} applied)`
|
|
288
|
-
}) : null
|
|
289
|
-
]
|
|
290
|
-
}),
|
|
291
|
-
/*#__PURE__*/ _jsx(TableCell, {
|
|
292
|
-
children: (_REASON_LABEL_row_reason = REASON_LABEL[row.reason]) != null ? _REASON_LABEL_row_reason : row.reason
|
|
293
|
-
}),
|
|
294
|
-
/*#__PURE__*/ _jsx(TableCell, {
|
|
295
|
-
children: /*#__PURE__*/ _jsx(Typography, {
|
|
296
|
-
variant: "caption",
|
|
297
|
-
color: "text.secondary",
|
|
298
|
-
children: [
|
|
299
|
-
row.orderId ? `order ${row.orderId}` : null,
|
|
300
|
-
row.locationId ? `at ${row.locationId}` : null
|
|
301
|
-
].filter(Boolean).join(' · ') || '—'
|
|
302
|
-
})
|
|
303
|
-
})
|
|
304
|
-
]
|
|
305
|
-
}, row.$id);
|
|
306
|
-
})
|
|
307
|
-
})
|
|
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
|
+
}))
|
|
308
320
|
]
|
|
309
321
|
}),
|
|
310
322
|
movements.length === 0 ? null : /*#__PURE__*/ _jsx(ListPagination, {
|
|
@@ -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 { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n MenuItem,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\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 * 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 const [productFilter, setProductFilter] = useState('all')\n const [reasonFilter, setReasonFilter] = useState('all')\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 movements: MovementRow[] = 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 .filter(\n (row) => productFilter === 'all' || row.productId === productFilter,\n )\n .filter((row) => reasonFilter === 'all' || row.reason === reasonFilter),\n [windowRows, productFilter, reasonFilter],\n )\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 both filters above 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 useEffect(() => setPage(0), [productFilter, reasonFilter])\n const shown = useMemo(\n () => movements.slice(page * pageSize, page * pageSize + pageSize),\n [movements, page, pageSize],\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 filterableProducts = 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].sort((a, b) =>\n (productNames.get(a) ?? a).localeCompare(productNames.get(b) ?? b),\n )\n }, [windowRows, productNames])\n\n return (\n <CardDisplay header=\"Stock movements\" help={movementsHelp} contentGutterX contentGutterY>\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={2}>\n <TextField\n label=\"Product\"\n size=\"small\"\n select\n value={productFilter}\n onChange={(event) => setProductFilter(event.target.value)}\n sx={{ minWidth: 200 }}\n >\n <MenuItem value=\"all\">{'All products'}</MenuItem>\n {filterableProducts.map((productId) => (\n <MenuItem key={productId} value={productId}>\n {productNames.get(productId) ?? productId}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n label=\"Reason\"\n size=\"small\"\n select\n value={reasonFilter}\n onChange={(event) => setReasonFilter(event.target.value)}\n sx={{ minWidth: 180 }}\n >\n <MenuItem value=\"all\">{'Every reason'}</MenuItem>\n {Object.entries(REASON_LABEL).map(([value, label]) => (\n <MenuItem key={value} value={value}>\n {label}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n {movements.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 <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'When'}</TableCell>\n <TableCell>{'Product'}</TableCell>\n <TableCell align=\"right\">{'Change'}</TableCell>\n <TableCell>{'Reason'}</TableCell>\n <TableCell>{'Source'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {shown.map((row) => {\n const delta = Number(row.delta ?? 0)\n const applied = Number(row.appliedDelta ?? delta)\n return (\n <TableRow key={row.$id}>\n <TableCell>\n {row.atMs\n ? new Date(Number(row.atMs)).toLocaleString()\n : '—'}\n </TableCell>\n <TableCell>\n {productNames.get(row.productId) ?? row.productId}\n {row.variantId ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {` · ${row.variantId}`}\n </Typography>\n ) : null}\n </TableCell>\n <TableCell align=\"right\">\n {/*\n * The sign is carried explicitly. \"3\" and \"-3\" are the\n * same width and opposite facts, and a merchant\n * scanning a column for the movement that broke their\n * count reads the sign 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 </TableCell>\n <TableCell>\n {REASON_LABEL[row.reason] ?? row.reason}\n </TableCell>\n <TableCell>\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 </TableCell>\n </TableRow>\n )\n })}\n </TableBody>\n </ScrollTable>\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","ListPagination","TABLE_PAGE_SIZE_DEFAULT","ScrollTable","Alert","MenuItem","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","collection","limit","orderBy","query","useEffect","useMemo","useState","useFirestore","useFirestoreCollection","pluginDocsHelp","WINDOW","movementsHelp","anchor","title","excerpt","REASON_LABEL","sale","refund","restock","correction","damage","cancellation","StockMovementsCard","props","hostId","firestore","productFilter","setProductFilter","reasonFilter","setReasonFilter","data","movementDocs","idField","productDocs","productNames","names","Map","product","set","$id","name","truncated","length","windowRows","slice","movements","sort","a","b","Number","atMs","filter","row","productId","reason","page","setPage","pageSize","setPageSize","shown","filterableProducts","ids","Set","add","get","localeCompare","header","help","contentGutterX","contentGutterY","spacing","direction","label","size","select","value","onChange","event","target","sx","minWidth","map","Object","entries","variant","color","align","delta","applied","appliedDelta","Date","toLocaleString","variantId","component","String","orderId","locationId","Boolean","join","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 { 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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"}
|