@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.
- package/package.json +13 -13
- package/src/lib/components/console/catalog-organization-card.component.js +48 -3
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
- package/src/lib/components/console/gift-cards-card.component.d.ts +9 -0
- package/src/lib/components/console/gift-cards-card.component.js +53 -28
- package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.d.ts +26 -0
- package/src/lib/components/console/host-orders-card.component.js +264 -194
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/order-detail-dialog.component.js +12 -0
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-page.component.js +86 -45
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/product-editor-dialog.component.js +30 -6
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-slugs.d.ts +43 -0
- package/src/lib/components/console/product-slugs.js +60 -0
- package/src/lib/components/console/product-slugs.js.map +1 -0
- package/src/lib/components/console/products-hub-card.component.d.ts +2 -2
- package/src/lib/components/console/products-hub-card.component.js +229 -282
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/products-hub-zone.component.d.ts +10 -5
- package/src/lib/components/console/products-hub-zone.component.js +32 -9
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
- package/src/lib/components/console/recovery-queue-card.component.d.ts +37 -5
- package/src/lib/components/console/recovery-queue-card.component.js +107 -86
- package/src/lib/components/console/recovery-queue-card.component.js.map +1 -1
- package/src/lib/components/console/reservations-card.component.js +15 -3
- package/src/lib/components/console/reservations-card.component.js.map +1 -1
- package/src/lib/components/console/smart-collections.d.ts +32 -0
- package/src/lib/components/console/smart-collections.js +57 -0
- package/src/lib/components/console/smart-collections.js.map +1 -0
- package/src/lib/components/console/stock-movements-card.component.d.ts +13 -4
- package/src/lib/components/console/stock-movements-card.component.js +142 -180
- package/src/lib/components/console/stock-movements-card.component.js.map +1 -1
- package/src/lib/components/product-grid.d.ts +5 -0
- package/src/lib/components/product-grid.js +95 -34
- package/src/lib/components/product-grid.js.map +1 -1
- package/src/lib/constants/orders-list-query.d.ts +40 -0
- package/src/lib/constants/orders-list-query.js +205 -0
- package/src/lib/constants/orders-list-query.js.map +1 -0
- package/src/lib/constants/product-list-query.d.ts +42 -0
- package/src/lib/constants/product-list-query.js +217 -0
- package/src/lib/constants/product-list-query.js.map +1 -0
- package/src/lib/constants/stock-movement-query.d.ts +27 -0
- package/src/lib/constants/stock-movement-query.js +110 -0
- package/src/lib/constants/stock-movement-query.js.map +1 -0
- package/src/lib/constants/storefront-catalog-query.d.ts +103 -0
- package/src/lib/constants/storefront-catalog-query.js +312 -0
- package/src/lib/constants/storefront-catalog-query.js.map +1 -0
- package/src/lib/model/checkout-recovery.d.ts +37 -0
- package/src/lib/model/checkout-recovery.js +44 -0
- package/src/lib/model/checkout-recovery.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +7 -0
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce.d.ts +80 -0
- package/src/lib/model/commerce.js +67 -6
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/gift-card-search.d.ts +24 -0
- package/src/lib/model/gift-card-search.js +48 -0
- package/src/lib/model/gift-card-search.js.map +1 -0
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/order-list-fields.d.ts +80 -0
- package/src/lib/model/order-list-fields.js +112 -0
- package/src/lib/model/order-list-fields.js.map +1 -0
- package/src/lib/server/billing-webhook.js +180 -99
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cancel-order.js +6 -6
- package/src/lib/server/cancel-order.js.map +1 -1
- package/src/lib/server/cart-checkout.js +9 -2
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/catalog.d.ts +46 -30
- package/src/lib/server/catalog.js +507 -170
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout.js +3 -1
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/collection-membership.d.ts +35 -0
- package/src/lib/server/collection-membership.js +172 -0
- package/src/lib/server/collection-membership.js.map +1 -0
- package/src/lib/server/draft-order.js +5 -3
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/gift-cards.js +7 -1
- package/src/lib/server/gift-cards.js.map +1 -1
- package/src/lib/server/member-name-search.d.ts +17 -0
- package/src/lib/server/member-name-search.js +23 -1
- package/src/lib/server/member-name-search.js.map +1 -1
- package/src/lib/server/membership-account.js +7 -3
- package/src/lib/server/membership-account.js.map +1 -1
- package/src/lib/server/membership-admin-password.js +8 -2
- package/src/lib/server/membership-admin-password.js.map +1 -1
- package/src/lib/server/membership-recover.js +4 -1
- package/src/lib/server/membership-recover.js.map +1 -1
- package/src/lib/server/membership-register.js +3 -1
- package/src/lib/server/membership-register.js.map +1 -1
- package/src/lib/server/pos-order.js +9 -5
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/process-abandoned.js +20 -7
- package/src/lib/server/process-abandoned.js.map +1 -1
- package/src/lib/server/process-restock.js +6 -2
- package/src/lib/server/process-restock.js.map +1 -1
- package/src/lib/server/reserve-stock.js +3 -4
- package/src/lib/server/reserve-stock.js.map +1 -1
- package/src/lib/server/reserve.js +3 -1
- package/src/lib/server/reserve.js.map +1 -1
- package/src/lib/server/site-page-enricher.js +3 -3
- package/src/lib/server/site-page-enricher.js.map +1 -1
- package/src/lib/server.js +4 -0
- 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 {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import { collection
|
|
28
|
-
import {
|
|
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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
'
|
|
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
|
|
83
|
-
* "Adjust stock" dialog, the sale
|
|
84
|
-
*
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
115
|
+
products
|
|
135
116
|
]);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
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
|
-
|
|
157
|
+
movementRows,
|
|
155
158
|
productNames
|
|
156
159
|
]);
|
|
157
|
-
|
|
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
|
|
275
|
-
return (
|
|
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
|
-
|
|
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__*/
|
|
246
|
+
children: /*#__PURE__*/ _jsx(Stack, {
|
|
302
247
|
spacing: 2,
|
|
303
|
-
children:
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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":"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"}
|
|
@@ -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>;
|