@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,133 +19,85 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
19
19
|
import * as CommerceModel from "../../model/index.js";
|
|
20
20
|
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
21
21
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
22
|
+
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
23
|
+
import { ListQueryNotices, listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
22
24
|
import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
23
25
|
import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
+
import { listFilterGridColumns, upsertListFilterClause } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
27
|
+
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
26
28
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
27
29
|
import { checkEntitlement, ORDERS_CUSTOMER_PARAM, ORDERS_ORDER_PARAM, pluginDocsHelp } from "@aglyn/aglyn";
|
|
28
30
|
import { Alert, AlertTitle, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField, Tooltip, Typography } from "@mui/material";
|
|
29
|
-
import { collection, doc, getDoc, limit, orderBy, query } from "firebase/firestore";
|
|
31
|
+
import { collection, doc, getDoc, getDocs, limit, orderBy, query, startAfter, where } from "firebase/firestore";
|
|
30
32
|
import { useSearchParams } from "next/navigation";
|
|
31
33
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
32
34
|
import { ceilingedWindow, collectionCeiling, useFirestore, useOrgPlan, useUser } from "@aglyn/tenant-feature-instance";
|
|
33
35
|
import { useFirestoreCollection } from "@aglyn/tenant-feature-instance";
|
|
36
|
+
import { listQueryConstraints, useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
34
37
|
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
38
|
+
import { OPEN_DISPUTE_CLAUSE, ORDER_CHANNEL_OPTIONS, ORDER_DISPUTE_OPTIONS, ORDER_LIST_FIELDS, ORDER_LIST_HEADERS, ORDER_LIST_QUERY, ORDER_LIST_SELECT_FIELDS, ORDER_STATUS_OPTIONS, ordersCustomerClause } from "../../constants/orders-list-query.js";
|
|
35
39
|
import CommerceStatTile from "./commerce-stat-tile.component.js";
|
|
36
40
|
import OrderDetailDialog, { DISPUTE_COLOR } from "./order-detail-dialog.component.js";
|
|
37
41
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* the
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
inMemoryListField('statusKey', 'select'),
|
|
62
|
-
inMemoryListField('createdAtMs', 'date'),
|
|
63
|
-
_extends({}, inMemoryListField('productIds', 'select'), {
|
|
64
|
-
tokensPath: 'productIds',
|
|
65
|
-
verbatimTokens: true
|
|
66
|
-
}),
|
|
67
|
-
inMemoryListField('disputeKey', 'select')
|
|
68
|
-
];
|
|
69
|
-
const ORDER_FILTER_HEADERS = {
|
|
70
|
-
orderLabel: 'Order',
|
|
71
|
-
customerEmail: 'Customer',
|
|
72
|
-
channelKey: 'Channel',
|
|
73
|
-
statusKey: 'Status',
|
|
74
|
-
createdAtMs: 'Date',
|
|
75
|
-
productIds: 'Product',
|
|
76
|
-
disputeKey: 'Disputes'
|
|
77
|
-
};
|
|
78
|
-
const ORDER_STATUS_OPTIONS = Object.keys(CommerceModel.ORDER_STATUS_LABELS).map((status)=>({
|
|
79
|
-
value: status,
|
|
80
|
-
label: CommerceModel.ORDER_STATUS_LABELS[status]
|
|
81
|
-
}));
|
|
82
|
-
const ORDER_CHANNEL_OPTIONS = Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).map((channel)=>({
|
|
83
|
-
value: channel,
|
|
84
|
-
label: CommerceModel.ORDER_CHANNEL_LABELS[channel]
|
|
85
|
-
}));
|
|
86
|
-
/*
|
|
87
|
-
* `lost` is the badge's tone, not the raw dispute status: a dispute that was
|
|
88
|
-
* WON also closes with money untouched, and listing it as charged back would
|
|
89
|
-
* tell a merchant they lost a case they won.
|
|
90
|
-
*/ const ORDER_DISPUTE_OPTIONS = [
|
|
91
|
-
{
|
|
92
|
-
value: 'open',
|
|
93
|
-
label: 'Open dispute'
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
value: 'lost',
|
|
97
|
-
label: 'Charged back'
|
|
98
|
-
}
|
|
99
|
-
];
|
|
100
|
-
/** What the quick search reads on an order row. */ const ORDER_SEARCH_FIELDS = [
|
|
101
|
-
'orderLabel',
|
|
102
|
-
'customerEmail',
|
|
103
|
-
'itemName'
|
|
104
|
-
];
|
|
105
|
-
/** The filter-only columns never show. */ const ORDER_HIDDEN_COLUMNS = hiddenFilterVisibility(ORDER_FILTER_FIELDS, [
|
|
42
|
+
* The products the card reads to NAME a legacy row and the CSV, to offer the
|
|
43
|
+
* Product filter's choices and to fill the draft dialog's picker. A picker's
|
|
44
|
+
* reach, not the list's: the orders themselves are paged by their query and
|
|
45
|
+
* the Product filter asks it for any product id, named here or not.
|
|
46
|
+
*/ const PRODUCT_NAME_WINDOW = 100;
|
|
47
|
+
/**
|
|
48
|
+
* How many open disputes the banner reads. Its own query, `disputeKey ==
|
|
49
|
+
* 'open'`, so a deadline is raised whatever the list is filtered to and
|
|
50
|
+
* however deep in the store the order sits; a store past this many open
|
|
51
|
+
* cases is told "more than".
|
|
52
|
+
*/ const OPEN_DISPUTE_CEILING = 50;
|
|
53
|
+
/**
|
|
54
|
+
* The money tiles read the last sixty days (thirty, and the thirty before it)
|
|
55
|
+
* on their own bounded query. They are analytics over a period, not a filter
|
|
56
|
+
* over the list, and share the analytics card's bound.
|
|
57
|
+
*/ const STATS_WINDOW_MS = 60 * 24 * 60 * 60 * 1000;
|
|
58
|
+
const STATS_ORDER_CEILING = 500;
|
|
59
|
+
/**
|
|
60
|
+
* Export CSV walks the list's own query, page by page, up to this many
|
|
61
|
+
* orders, and says so when the matches run past it.
|
|
62
|
+
*/ const ORDERS_EXPORT_CEILING = 5000;
|
|
63
|
+
const ORDERS_EXPORT_PAGE = 500;
|
|
64
|
+
/** The columns the grid draws; every other declared field is a hidden filter column. */ const ORDER_VISIBLE_COLUMNS = [
|
|
106
65
|
'orderLabel',
|
|
107
66
|
'customerEmail',
|
|
108
67
|
'channelKey',
|
|
109
68
|
'statusKey',
|
|
110
69
|
'createdAtMs'
|
|
111
|
-
]
|
|
112
|
-
|
|
113
|
-
field: 'disputeKey',
|
|
114
|
-
op: 'equals',
|
|
115
|
-
value: 'open'
|
|
116
|
-
};
|
|
70
|
+
];
|
|
71
|
+
const ORDER_HIDDEN_COLUMNS = hiddenFilterVisibility(ORDER_LIST_FIELDS, ORDER_VISIBLE_COLUMNS);
|
|
117
72
|
/**
|
|
118
73
|
* Orders console (AGL-287): filterable list over webhook-written order
|
|
119
74
|
* docs with a detail dialog (timeline, fulfill, refund, cancel, notes,
|
|
120
75
|
* packing slip) and draft orders that send the buyer a payment link.
|
|
76
|
+
*
|
|
77
|
+
* ## Every filter and the search are on the query (AGL-3321)
|
|
78
|
+
*
|
|
79
|
+
* The list used to read the newest two hundred orders and match its Filters
|
|
80
|
+
* panel and search over them, so a Status filter on a busy store answered
|
|
81
|
+
* "none" about every order past the two hundredth. Now each clause and the
|
|
82
|
+
* search word are predicates on ONE Firestore query (`ORDER_LIST_QUERY`),
|
|
83
|
+
* over the fields every writer stamps (`orderListFields`), and the footer
|
|
84
|
+
* pages that query's answer. A combination one query cannot hold is refused
|
|
85
|
+
* by name above the grid and never applied to some rows only.
|
|
121
86
|
*/ export function HostOrdersCard(props) {
|
|
122
|
-
var _ref,
|
|
87
|
+
var _ref, _loadedOrders_find, _ref1, _ref2, _ref3, _ref4;
|
|
123
88
|
var _draft_shipCountries, _draft_shipCountries1;
|
|
124
89
|
const { hostId } = props;
|
|
125
90
|
const firestore = useFirestore();
|
|
126
|
-
|
|
127
|
-
* The most recent orders, in the order the table shows them.
|
|
128
|
-
*
|
|
129
|
-
* `createdAtMs` is the field every order writer in the plugin stamps and the
|
|
130
|
-
* one `reconcile-stock` already walks the collection by. Ordering on it is
|
|
131
|
-
* what makes this window the RECENT orders: an unordered cap is answered in
|
|
132
|
-
* document-id order, and orders are keyed by generated ids, so it returns a
|
|
133
|
-
* pseudo-random sample that a client sort then dresses as newest-first.
|
|
134
|
-
*/ const { data: orderDocs } = useFirestoreCollection(()=>query(collection(firestore, 'hosts', hostId, 'orders'), orderBy('createdAtMs', 'desc'), limit(ORDERS_WINDOW + 1)), [
|
|
91
|
+
const ordersRef = useMemo(()=>collection(firestore, 'hosts', hostId, 'orders'), [
|
|
135
92
|
firestore,
|
|
136
93
|
hostId
|
|
137
|
-
]
|
|
138
|
-
idField: '$id'
|
|
139
|
-
});
|
|
94
|
+
]);
|
|
140
95
|
const { data: productDocs } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'products'), PRODUCT_NAME_WINDOW), [
|
|
141
96
|
firestore,
|
|
142
97
|
hostId
|
|
143
98
|
], {
|
|
144
99
|
idField: '$id'
|
|
145
100
|
});
|
|
146
|
-
const orderWindow = useMemo(()=>ceilingedWindow(orderDocs != null ? orderDocs : undefined, ORDERS_WINDOW), [
|
|
147
|
-
orderDocs
|
|
148
|
-
]);
|
|
149
101
|
const productWindow = useMemo(()=>ceilingedWindow(productDocs != null ? productDocs : undefined, PRODUCT_NAME_WINDOW), [
|
|
150
102
|
productDocs
|
|
151
103
|
]);
|
|
@@ -168,49 +120,90 @@ const ORDER_CHANNEL_OPTIONS = Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).ma
|
|
|
168
120
|
* a paid feature.
|
|
169
121
|
*/ const { org, ready: orgReady } = useOrgPlan(hostId);
|
|
170
122
|
const showStats = orgReady && checkEntitlement(org, 'commerceAnalytics');
|
|
171
|
-
// The query returns the window already newest-first; re-sorting it here
|
|
172
|
-
// would only restate the order it arrived in.
|
|
173
|
-
const orders = orderWindow.rows;
|
|
174
123
|
/*
|
|
175
124
|
* The grid's clauses (AGL-96, AGL-3317), held here so two things outside
|
|
176
125
|
* the grid can set one: the CRM's contact page links with the buyer's
|
|
177
|
-
* address to answer "what has this customer ordered" (AGL-2622)
|
|
178
|
-
*
|
|
179
|
-
*
|
|
126
|
+
* address to answer "what has this customer ordered" (AGL-2622) — a whole
|
|
127
|
+
* address is that buyer, a domain every buyer at a company
|
|
128
|
+
* (`ordersCustomerClause`) — and the dispute banner's "Show them".
|
|
180
129
|
*/ const searchParams = useSearchParams();
|
|
181
130
|
const [clauses, setClauses] = useState(()=>{
|
|
182
|
-
var
|
|
183
|
-
const customer = searchParams == null ? void 0 :
|
|
131
|
+
var _ref;
|
|
132
|
+
const customer = ordersCustomerClause((_ref = searchParams == null ? void 0 : searchParams.get(ORDERS_CUSTOMER_PARAM)) != null ? _ref : '');
|
|
184
133
|
return customer ? [
|
|
185
|
-
|
|
186
|
-
field: 'customerEmail',
|
|
187
|
-
op: 'contains',
|
|
188
|
-
value: customer
|
|
189
|
-
}
|
|
134
|
+
customer
|
|
190
135
|
] : [];
|
|
191
136
|
});
|
|
137
|
+
const gridFilter = useListGridFilter({
|
|
138
|
+
clauses,
|
|
139
|
+
onChange: setClauses,
|
|
140
|
+
selectFields: ORDER_LIST_SELECT_FIELDS
|
|
141
|
+
});
|
|
142
|
+
const filtering = clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
143
|
+
/*
|
|
144
|
+
* The open disputes, on their OWN query (AGL-1796). Raised whatever the
|
|
145
|
+
* list is filtered to — a merchant filtered to "delivered" still has a
|
|
146
|
+
* deadline running on a `paid` order, and the evidence window is days —
|
|
147
|
+
* and however far back the order is. Asked before the list so the list's
|
|
148
|
+
* plan is the one a spec reads last.
|
|
149
|
+
*/ const { rows: disputedRows, hasMore: moreDisputes } = useListQuery({
|
|
150
|
+
collection: ordersRef,
|
|
151
|
+
declaration: ORDER_LIST_QUERY,
|
|
152
|
+
request: {
|
|
153
|
+
clauses: [
|
|
154
|
+
OPEN_DISPUTE_CLAUSE
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
deps: [
|
|
158
|
+
firestore,
|
|
159
|
+
hostId
|
|
160
|
+
],
|
|
161
|
+
idField: '$id',
|
|
162
|
+
pageSize: OPEN_DISPUTE_CEILING,
|
|
163
|
+
// A dispute that closes leaves this query; confirm it before believing it.
|
|
164
|
+
confirmDisappearances: true
|
|
165
|
+
});
|
|
166
|
+
/*
|
|
167
|
+
* THE LIST: every clause and the search word on one query, newest first,
|
|
168
|
+
* paged by the footer. A status, a dispute or a paid draft's address can
|
|
169
|
+
* move an order out of the filtered query mid-session, so a disappearance
|
|
170
|
+
* is confirmed against the server before it is believed (AGL-1196).
|
|
171
|
+
*/ const { rows: orders, data: loadedOrders, hasMore, page, setPage, pageSize, setPageSize, plan, status: listStatus } = useListQuery({
|
|
172
|
+
collection: ordersRef,
|
|
173
|
+
declaration: ORDER_LIST_QUERY,
|
|
174
|
+
request: {
|
|
175
|
+
clauses,
|
|
176
|
+
search: gridFilter.searchWords
|
|
177
|
+
},
|
|
178
|
+
deps: [
|
|
179
|
+
firestore,
|
|
180
|
+
hostId
|
|
181
|
+
],
|
|
182
|
+
idField: '$id',
|
|
183
|
+
confirmDisappearances: true
|
|
184
|
+
});
|
|
192
185
|
const [selectedId, setSelectedId] = useState(null);
|
|
193
186
|
/*
|
|
194
187
|
* An order named in the URL opens in its dialog on arrival (AGL-2622). A
|
|
195
188
|
* contact's timeline names the order that made the person a customer,
|
|
196
|
-
* and the address it links to is this list with `?order={id}`. The
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
189
|
+
* and the address it links to is this list with `?order={id}`. The list
|
|
190
|
+
* shows one page, and an order from last year is not on it, so the
|
|
191
|
+
* document is read once by id and held beside the page for the dialog; an
|
|
192
|
+
* order that IS loaded is found there first and the read is skipped. Once
|
|
193
|
+
* per id — the ref keeps a re-render from reopening a dialog the merchant
|
|
194
|
+
* has since closed — and the landing read is judged against the ref
|
|
195
|
+
* rather than an effect cleanup, because the `setSelectedId` above
|
|
196
|
+
* re-renders before the read lands and a cleanup keyed on any dep would
|
|
197
|
+
* cancel the answer to the question just asked.
|
|
205
198
|
*/ const seededOrderId = (_ref = searchParams == null ? void 0 : searchParams.get(ORDERS_ORDER_PARAM)) != null ? _ref : null;
|
|
206
199
|
const [seededOrder, setSeededOrder] = useState(null);
|
|
207
200
|
const seededOpened = useRef(null);
|
|
208
201
|
useEffect(()=>{
|
|
209
202
|
if (!seededOrderId || seededOpened.current === seededOrderId) return;
|
|
210
|
-
if (
|
|
203
|
+
if (listStatus === 'loading') return;
|
|
211
204
|
seededOpened.current = seededOrderId;
|
|
212
205
|
setSelectedId(seededOrderId);
|
|
213
|
-
if (
|
|
206
|
+
if (loadedOrders.some((order)=>order.$id === seededOrderId)) return;
|
|
214
207
|
void getDoc(doc(firestore, 'hosts', hostId, 'orders', seededOrderId)).then((snapshot)=>{
|
|
215
208
|
if (seededOpened.current !== snapshot.id || !snapshot.exists()) return;
|
|
216
209
|
setSeededOrder(_extends({}, snapshot.data(), {
|
|
@@ -219,7 +212,8 @@ const ORDER_CHANNEL_OPTIONS = Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).ma
|
|
|
219
212
|
}).catch(()=>undefined);
|
|
220
213
|
}, [
|
|
221
214
|
seededOrderId,
|
|
222
|
-
|
|
215
|
+
loadedOrders,
|
|
216
|
+
listStatus,
|
|
223
217
|
firestore,
|
|
224
218
|
hostId
|
|
225
219
|
]);
|
|
@@ -227,30 +221,22 @@ const ORDER_CHANNEL_OPTIONS = Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).ma
|
|
|
227
221
|
const { data: user } = useUser();
|
|
228
222
|
const { enqueueSnackbar } = useSnackbar();
|
|
229
223
|
/**
|
|
230
|
-
* Each order with the values the grid
|
|
231
|
-
*
|
|
224
|
+
* Each order on the page with the values the grid DRAWS. What it filters
|
|
225
|
+
* by is on the query; this is presentation only. `liftLegacyOrder` names
|
|
226
|
+
* a status and channel on a row that predates them, and the item name
|
|
232
227
|
* reads the line items first (AGL-1747): the flat `productId` is written
|
|
233
228
|
* only by the two buy-now Stripe paths.
|
|
234
229
|
*/ const orderRows = useMemo(()=>orders.map((order)=>{
|
|
235
|
-
var
|
|
236
|
-
var _lifted_lineItems_,
|
|
230
|
+
var _ref, _ref1, _ref2, _lifted_channel;
|
|
231
|
+
var _lifted_lineItems_, _lifted_lineItems;
|
|
237
232
|
const lifted = CommerceModel.liftLegacyOrder(order);
|
|
238
|
-
const dispute = CommerceModel.describeOrderDispute(lifted);
|
|
239
|
-
const productIds = [
|
|
240
|
-
...new Set([
|
|
241
|
-
...((_lifted_lineItems = lifted.lineItems) != null ? _lifted_lineItems : []).map((line)=>line.productId),
|
|
242
|
-
order.productId
|
|
243
|
-
].filter((id)=>typeof id === 'string' && id !== ''))
|
|
244
|
-
];
|
|
245
233
|
return _extends({}, order, {
|
|
246
234
|
orderLabel: CommerceModel.formatOrderNumber(lifted, order.$id),
|
|
247
|
-
itemName: (_ref = (_ref1 = (_ref2 = (
|
|
235
|
+
itemName: (_ref = (_ref1 = (_ref2 = (_lifted_lineItems = lifted.lineItems) == null ? void 0 : (_lifted_lineItems_ = _lifted_lineItems[0]) == null ? void 0 : _lifted_lineItems_.name) != null ? _ref2 : productNames[order.productId]) != null ? _ref1 : order.productId) != null ? _ref : '',
|
|
248
236
|
statusKey: lifted.status,
|
|
249
237
|
channelKey: (_lifted_channel = lifted.channel) != null ? _lifted_channel : 'online',
|
|
250
238
|
netCents: CommerceModel.orderNetCents(lifted),
|
|
251
|
-
disputeBadge:
|
|
252
|
-
productIds,
|
|
253
|
-
disputeKey: CommerceModel.orderHasOpenDispute(lifted) ? 'open' : (dispute == null ? void 0 : dispute.tone) === 'lost' ? 'lost' : ''
|
|
239
|
+
disputeBadge: CommerceModel.describeOrderDispute(lifted)
|
|
254
240
|
});
|
|
255
241
|
}), [
|
|
256
242
|
orders,
|
|
@@ -273,52 +259,101 @@ const ORDER_CHANNEL_OPTIONS = Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).ma
|
|
|
273
259
|
}), [
|
|
274
260
|
productOptions
|
|
275
261
|
]);
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
});
|
|
285
|
-
const visibleOrders = listFilter.rows;
|
|
262
|
+
const refused = useMemo(()=>listQueryRefusals(plan.refused, {
|
|
263
|
+
fields: ORDER_LIST_FIELDS,
|
|
264
|
+
headers: ORDER_LIST_HEADERS,
|
|
265
|
+
options: filterOptions
|
|
266
|
+
}), [
|
|
267
|
+
plan,
|
|
268
|
+
filterOptions
|
|
269
|
+
]);
|
|
286
270
|
const showingOpenDisputes = clauses.some((clause)=>clause.field === OPEN_DISPUTE_CLAUSE.field && clause.op === OPEN_DISPUTE_CLAUSE.op && clause.value === OPEN_DISPUTE_CLAUSE.value);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
* merchant filtered to "delivered" still has a deadline running on a `paid`
|
|
290
|
-
* order, and the evidence window is days long.
|
|
291
|
-
*/ const openDisputes = useMemo(()=>CommerceModel.summariseOpenDisputes(orders.map((order)=>CommerceModel.liftLegacyOrder(order))), [
|
|
292
|
-
orders
|
|
271
|
+
const openDisputes = useMemo(()=>CommerceModel.summariseOpenDisputes(disputedRows.map((order)=>CommerceModel.liftLegacyOrder(order))), [
|
|
272
|
+
disputedRows
|
|
293
273
|
]);
|
|
294
|
-
|
|
295
|
-
* 30-day money summary with the prior 30 days behind it (AGL-2136).
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*/ const
|
|
274
|
+
/*
|
|
275
|
+
* 30-day money summary with the prior 30 days behind it (AGL-2136).
|
|
276
|
+
* ANALYTICS, NOT A FILTER: the tiles describe a period, not the list, so
|
|
277
|
+
* they keep their own bounded read — sixty days by `createdAtMs`, the
|
|
278
|
+
* newest `STATS_ORDER_CEILING` of them, read only when the tiles show.
|
|
279
|
+
* Range and order are the same field, so it needs no composite.
|
|
280
|
+
*/ const statsSince = useMemo(()=>Date.now() - STATS_WINDOW_MS, []);
|
|
281
|
+
const { data: statsDocs } = useFirestoreCollection(()=>showStats ? query(ordersRef, where('createdAtMs', '>=', statsSince), orderBy('createdAtMs', 'desc'), limit(STATS_ORDER_CEILING + 1)) : null, [
|
|
282
|
+
firestore,
|
|
283
|
+
hostId,
|
|
284
|
+
showStats,
|
|
285
|
+
statsSince
|
|
286
|
+
], {
|
|
287
|
+
idField: '$id'
|
|
288
|
+
});
|
|
289
|
+
const statsWindow = useMemo(()=>ceilingedWindow(statsDocs != null ? statsDocs : undefined, STATS_ORDER_CEILING), [
|
|
290
|
+
statsDocs
|
|
291
|
+
]);
|
|
292
|
+
const summary = useMemo(()=>CommerceModel.summarizeOrderWindow(statsWindow.rows, {
|
|
301
293
|
nowMs: Date.now()
|
|
302
294
|
}), [
|
|
303
|
-
|
|
295
|
+
statsWindow
|
|
304
296
|
]);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Export CSV writes what the QUERY matches — every clause and the search
|
|
299
|
+
* the grid is showing — not the page on screen. It walks the list's own
|
|
300
|
+
* plan in pages of `ORDERS_EXPORT_PAGE` up to `ORDERS_EXPORT_CEILING`, and
|
|
301
|
+
* says so when the matches run past it.
|
|
302
|
+
*/ const [exporting, setExporting] = useState(false);
|
|
303
|
+
const handleExportCsv = useCallback(async ()=>{
|
|
304
|
+
setExporting(true);
|
|
305
|
+
try {
|
|
306
|
+
const matched = [];
|
|
307
|
+
let cursor = null;
|
|
308
|
+
let more = true;
|
|
309
|
+
while(more && matched.length < ORDERS_EXPORT_CEILING){
|
|
310
|
+
var _docs_;
|
|
311
|
+
const take = Math.min(ORDERS_EXPORT_PAGE, ORDERS_EXPORT_CEILING - matched.length);
|
|
312
|
+
// One past the page: whether more match is a fact, not a guess.
|
|
313
|
+
const snapshot = await getDocs(query(ordersRef, ...listQueryConstraints(plan), ...cursor ? [
|
|
314
|
+
startAfter(cursor)
|
|
315
|
+
] : [], limit(take + 1)));
|
|
316
|
+
const docs = snapshot.docs.slice(0, take);
|
|
317
|
+
for (const entry of docs)matched.push(_extends({}, entry.data(), {
|
|
318
|
+
$id: entry.id
|
|
319
|
+
}));
|
|
320
|
+
more = snapshot.docs.length > take;
|
|
321
|
+
cursor = (_docs_ = docs[docs.length - 1]) != null ? _docs_ : null;
|
|
322
|
+
if (!cursor) more = false;
|
|
323
|
+
}
|
|
324
|
+
// The rows themselves are built by the pure model helper (AGL-1747) so
|
|
325
|
+
// the column-by-column arithmetic is unit-testable without Firestore.
|
|
326
|
+
const csv = CommerceModel.buildOrdersCsv(matched, productNames);
|
|
327
|
+
const url = URL.createObjectURL(new Blob([
|
|
328
|
+
csv
|
|
329
|
+
], {
|
|
330
|
+
type: 'text/csv'
|
|
331
|
+
}));
|
|
332
|
+
const anchor = document.createElement('a');
|
|
333
|
+
anchor.href = url;
|
|
334
|
+
anchor.download = 'orders.csv';
|
|
335
|
+
anchor.click();
|
|
336
|
+
URL.revokeObjectURL(url);
|
|
337
|
+
if (more) {
|
|
338
|
+
enqueueSnackbar(`Exported the ${ORDERS_EXPORT_CEILING.toLocaleString('en-US')} newest matching orders. ` + 'Narrow the filters to export the rest.', {
|
|
339
|
+
variant: 'warning',
|
|
340
|
+
allowDuplicate: true
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error('Orders export failed', error);
|
|
345
|
+
enqueueSnackbar('Export failed — try again', {
|
|
346
|
+
variant: 'error',
|
|
347
|
+
allowDuplicate: true
|
|
348
|
+
});
|
|
349
|
+
} finally{
|
|
350
|
+
setExporting(false);
|
|
351
|
+
}
|
|
319
352
|
}, [
|
|
320
|
-
|
|
321
|
-
|
|
353
|
+
ordersRef,
|
|
354
|
+
plan,
|
|
355
|
+
productNames,
|
|
356
|
+
enqueueSnackbar
|
|
322
357
|
]);
|
|
323
358
|
/**
|
|
324
359
|
* Idempotency key for ONE draft attempt (AGL-1697).
|
|
@@ -409,7 +444,7 @@ const ORDER_CHANNEL_OPTIONS = Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).ma
|
|
|
409
444
|
hostId,
|
|
410
445
|
enqueueSnackbar
|
|
411
446
|
]);
|
|
412
|
-
const selectedOrder = (
|
|
447
|
+
const selectedOrder = (_loadedOrders_find = loadedOrders.find((order)=>order.$id === selectedId)) != null ? _loadedOrders_find : seededOrder && seededOrder.$id === selectedId ? seededOrder : null;
|
|
413
448
|
/**
|
|
414
449
|
* Shared by the two triggers (AGL-1805) so the empty state and the toolbar
|
|
415
450
|
* cannot drift into opening the dialog with different starting fields.
|
|
@@ -584,6 +619,15 @@ const ORDER_CHANNEL_OPTIONS = Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).ma
|
|
|
584
619
|
}
|
|
585
620
|
}
|
|
586
621
|
], []);
|
|
622
|
+
const columns = useMemo(()=>listFilterGridColumns(orderColumns, ORDER_LIST_FIELDS, filterOptions, ORDER_LIST_HEADERS), [
|
|
623
|
+
orderColumns,
|
|
624
|
+
filterOptions
|
|
625
|
+
]);
|
|
626
|
+
/*
|
|
627
|
+
* The store is empty only when an UNFILTERED first page came back empty; a
|
|
628
|
+
* filter or search that matches nothing keeps the grid, its chips and its
|
|
629
|
+
* panel, so the reader can take the filter back off.
|
|
630
|
+
*/ const empty = listStatus === 'success' && !filtering && page === 0 && orders.length === 0;
|
|
587
631
|
return /*#__PURE__*/ _jsxs(CardDisplay, {
|
|
588
632
|
header: 'Orders',
|
|
589
633
|
help: pluginDocsHelp('commerce', {
|
|
@@ -593,7 +637,7 @@ const ORDER_CHANNEL_OPTIONS = Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).ma
|
|
|
593
637
|
contentGutterX: true,
|
|
594
638
|
contentGutterY: true,
|
|
595
639
|
children: [
|
|
596
|
-
|
|
640
|
+
empty ? /*
|
|
597
641
|
* An invitation, not a report (AGL-1805). The "Draft order" button
|
|
598
642
|
* used to live in the other arm of this ternary, so the one state a
|
|
599
643
|
* draft order exists for — no sales yet, invoice the customer you
|
|
@@ -658,6 +702,11 @@ const ORDER_CHANNEL_OPTIONS = Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).ma
|
|
|
658
702
|
})
|
|
659
703
|
]
|
|
660
704
|
}) : null,
|
|
705
|
+
showStats && statsWindow.truncated ? /*#__PURE__*/ _jsx(Typography, {
|
|
706
|
+
variant: "caption",
|
|
707
|
+
color: "text.secondary",
|
|
708
|
+
children: `Figures count the ${STATS_ORDER_CEILING} most recent orders of the last 60 days.`
|
|
709
|
+
}) : null,
|
|
661
710
|
openDisputes.count > 0 ? /*#__PURE__*/ _jsxs(Alert, {
|
|
662
711
|
severity: openDisputes.overdue ? 'error' : 'warning',
|
|
663
712
|
action: showingOpenDisputes ? undefined : /*#__PURE__*/ _jsx(Button, {
|
|
@@ -668,7 +717,7 @@ const ORDER_CHANNEL_OPTIONS = Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).ma
|
|
|
668
717
|
}),
|
|
669
718
|
children: [
|
|
670
719
|
/*#__PURE__*/ _jsx(AlertTitle, {
|
|
671
|
-
children: openDisputes.count === 1 ? 'A shopper has disputed a charge with their bank' : `${openDisputes.count} charges are disputed with the shopper’s bank`
|
|
720
|
+
children: moreDisputes ? `More than ${OPEN_DISPUTE_CEILING} charges are disputed with the shopper’s bank` : openDisputes.count === 1 ? 'A shopper has disputed a charge with their bank' : `${openDisputes.count} charges are disputed with the shopper’s bank`
|
|
672
721
|
}),
|
|
673
722
|
openDisputes.soonestDaysLeft === undefined ? 'Answer in the Stripe dashboard while the case is open — an unanswered dispute is decided for the shopper.' : openDisputes.soonestDaysLeft < 0 ? 'The evidence deadline has passed on at least one of these. Stripe decides an unanswered dispute for the shopper.' : `Evidence is due to Stripe in ${openDisputes.soonestDaysLeft} day${openDisputes.soonestDaysLeft === 1 ? '' : 's'} on the tightest of these. An unanswered dispute is decided for the shopper.`
|
|
674
723
|
]
|
|
@@ -683,7 +732,8 @@ const ORDER_CHANNEL_OPTIONS = Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).ma
|
|
|
683
732
|
children: [
|
|
684
733
|
/*#__PURE__*/ _jsx(Button, {
|
|
685
734
|
size: "small",
|
|
686
|
-
onClick: handleExportCsv,
|
|
735
|
+
onClick: ()=>void handleExportCsv(),
|
|
736
|
+
disabled: exporting,
|
|
687
737
|
children: 'Export CSV'
|
|
688
738
|
}),
|
|
689
739
|
/*#__PURE__*/ _jsx(Button, {
|
|
@@ -695,30 +745,50 @@ const ORDER_CHANNEL_OPTIONS = Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).ma
|
|
|
695
745
|
})
|
|
696
746
|
]
|
|
697
747
|
}),
|
|
698
|
-
/*#__PURE__*/ _jsx(ListFilterChips,
|
|
699
|
-
|
|
748
|
+
/*#__PURE__*/ _jsx(ListFilterChips, {
|
|
749
|
+
fields: ORDER_LIST_FIELDS,
|
|
750
|
+
headers: ORDER_LIST_HEADERS,
|
|
751
|
+
clauses: clauses,
|
|
752
|
+
onChange: setClauses,
|
|
753
|
+
options: filterOptions
|
|
754
|
+
}),
|
|
755
|
+
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
756
|
+
refused: refused,
|
|
757
|
+
notices: plan.notices
|
|
758
|
+
}),
|
|
759
|
+
/*#__PURE__*/ _jsx(ListTable, {
|
|
700
760
|
"aria-label": "Orders",
|
|
701
|
-
rows:
|
|
702
|
-
columns:
|
|
703
|
-
onOpen: (id)=>setSelectedId(id)
|
|
704
|
-
|
|
761
|
+
rows: orderRows,
|
|
762
|
+
columns: columns,
|
|
763
|
+
onOpen: (id)=>setSelectedId(id),
|
|
764
|
+
loading: listStatus === 'loading',
|
|
765
|
+
/*
|
|
766
|
+
* The grid must NOT also filter, search or sort: the query
|
|
767
|
+
* answers all three, and the list has one order — newest first —
|
|
768
|
+
* which the query already holds.
|
|
769
|
+
*/ filterMode: "server",
|
|
770
|
+
filterModel: gridFilter.filterModel,
|
|
771
|
+
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
772
|
+
quickFilter: true,
|
|
773
|
+
sortingMode: "server",
|
|
774
|
+
disableColumnSorting: true,
|
|
775
|
+
// `ListPagination` below pages the query.
|
|
776
|
+
hideFooter: true,
|
|
705
777
|
initialState: {
|
|
706
778
|
columns: {
|
|
707
779
|
columnVisibilityModel: ORDER_HIDDEN_COLUMNS
|
|
708
780
|
}
|
|
709
781
|
},
|
|
710
782
|
noRowsLabel: "No orders match these filters"
|
|
711
|
-
})
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
children: `Showing the ${ORDERS_WINDOW} most recent orders. Filters and Export CSV cover these.`
|
|
721
|
-
}) : null
|
|
783
|
+
}),
|
|
784
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
785
|
+
page: page,
|
|
786
|
+
pageSize: pageSize,
|
|
787
|
+
rowCount: orderRows.length,
|
|
788
|
+
hasMore: hasMore,
|
|
789
|
+
onPageChange: setPage,
|
|
790
|
+
onPageSizeChange: setPageSize
|
|
791
|
+
})
|
|
722
792
|
]
|
|
723
793
|
}),
|
|
724
794
|
selectedOrder ? /*#__PURE__*/ _jsx(OrderDetailDialog, {
|