@aglyn/plugins-commerce 1.0.0-beta.189 → 1.0.0-beta.191

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.
@@ -18,10 +18,14 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  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
- import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
21
+ import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
22
+ import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
23
+ import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
24
+ import { inMemoryListField, upsertListFilterClause } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
25
+ import { useListRowsFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-rows-filter";
22
26
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
23
27
  import { checkEntitlement, ORDERS_CUSTOMER_PARAM, ORDERS_ORDER_PARAM, pluginDocsHelp } from "@aglyn/aglyn";
24
- import { Alert, AlertTitle, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TableBody, TableCell, TableHead, TableRow, TextField, Tooltip, Typography } from "@mui/material";
28
+ import { Alert, AlertTitle, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField, Tooltip, Typography } from "@mui/material";
25
29
  import { collection, doc, getDoc, limit, orderBy, query } from "firebase/firestore";
26
30
  import { useSearchParams } from "next/navigation";
27
31
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -33,13 +37,83 @@ import OrderDetailDialog, { DISPUTE_COLOR } from "./order-detail-dialog.componen
33
37
  /**
34
38
  * How far back the table, its filters and its export reach.
35
39
  *
36
- * This window cannot be paged the way a plain list can. Five filters and the
37
- * CSV export all run over what was read, so a page of ten would quietly narrow
38
- * every one of them — a status filter would search a tenth of the orders and
39
- * report confidently on what it found. Bounding the read and saying where the
40
+ * This window cannot be paged the way a plain list can. The Filters panel,
41
+ * the search and the CSV export all run over what was read, so a page of ten
42
+ * would quietly narrow every one of them — a status filter would search a
43
+ * tenth of the orders and report confidently on what it found. Bounding the read and saying where the
40
44
  * bound falls keeps the filters honest about their own scope.
41
45
  */ const ORDERS_WINDOW = 200;
42
46
  /** A name map for the filter menu; it renders no product rows of its own. */ const PRODUCT_NAME_WINDOW = 100;
47
+ /*
48
+ * What the orders grid's Filters panel offers. The card holds its whole
49
+ * window, so the panel and the search answer over every order it read.
50
+ *
51
+ * Status and Channel are the order's own enumerations; Disputes is apart
52
+ * from Status on purpose, because the two are orthogonal: an OPEN dispute
53
+ * sits on an order that is still `paid`, and a lost one on `refunded`
54
+ * beside every ordinary refund. Product is the ids of every line item, so
55
+ * a cart, POS or draft order is found by any product it holds. The date is
56
+ * the field the query orders by, which every order writer stamps.
57
+ */ const ORDER_FILTER_FIELDS = [
58
+ inMemoryListField('orderLabel', 'text'),
59
+ inMemoryListField('customerEmail', 'text'),
60
+ inMemoryListField('channelKey', 'select'),
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, [
106
+ 'orderLabel',
107
+ 'customerEmail',
108
+ 'channelKey',
109
+ 'statusKey',
110
+ 'createdAtMs'
111
+ ]);
112
+ /** The clause the dispute banner's "Show them" sets. */ const OPEN_DISPUTE_CLAUSE = {
113
+ field: 'disputeKey',
114
+ op: 'equals',
115
+ value: 'open'
116
+ };
43
117
  /**
44
118
  * Orders console (AGL-287): filterable list over webhook-written order
45
119
  * docs with a detail dialog (timeline, fulfill, refund, cancel, notes,
@@ -97,28 +171,23 @@ import OrderDetailDialog, { DISPUTE_COLOR } from "./order-detail-dialog.componen
97
171
  // The query returns the window already newest-first; re-sorting it here
98
172
  // would only restate the order it arrived in.
99
173
  const orders = orderWindow.rows;
100
- // Filters + CSV export (AGL-96) over the loaded window.
101
- const [productFilter, setProductFilter] = useState('');
102
- const [dateFilter, setDateFilter] = useState('');
103
- const [statusFilter, setStatusFilter] = useState('');
104
- const [channelFilter, setChannelFilter] = useState('');
105
- /**
106
- * Disputes filter (AGL-1796), separate from Status on purpose: the two are
107
- * orthogonal. An OPEN dispute sits on an order that is still `paid`, and a
108
- * lost one sits on `refunded` beside every ordinary refund — so folding
109
- * either into the Status select would make that control lie about what it
110
- * selects.
111
- */ const [disputeFilter, setDisputeFilter] = useState('');
112
174
  /*
113
- * Customer filter (AGL-2622): the buyer's address, matched as a substring
114
- * of `customerEmail` so a domain finds every buyer at a company. Seeded
115
- * from the URL, because the CRM's contact page links here with the
116
- * person's address to answer "what has this customer ordered" — the
117
- * orders count on the record is the number, and this list is the rows.
175
+ * The grid's clauses (AGL-96, AGL-3317), held here so two things outside
176
+ * 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), seeded as
178
+ * a Customer clause matched as a substring so a domain finds every buyer at
179
+ * a company; and the dispute banner's "Show them".
118
180
  */ const searchParams = useSearchParams();
119
- const [customerFilter, setCustomerFilter] = useState(()=>{
120
- var _ref;
121
- return (_ref = searchParams == null ? void 0 : searchParams.get(ORDERS_CUSTOMER_PARAM)) != null ? _ref : '';
181
+ const [clauses, setClauses] = useState(()=>{
182
+ var _searchParams_get;
183
+ const customer = searchParams == null ? void 0 : (_searchParams_get = searchParams.get(ORDERS_CUSTOMER_PARAM)) == null ? void 0 : _searchParams_get.trim();
184
+ return customer ? [
185
+ {
186
+ field: 'customerEmail',
187
+ op: 'contains',
188
+ value: customer
189
+ }
190
+ ] : [];
122
191
  });
123
192
  const [selectedId, setSelectedId] = useState(null);
124
193
  /*
@@ -157,51 +226,64 @@ import OrderDetailDialog, { DISPUTE_COLOR } from "./order-detail-dialog.componen
157
226
  const [draft, setDraft] = useState(null);
158
227
  const { data: user } = useUser();
159
228
  const { enqueueSnackbar } = useSnackbar();
160
- const visibleOrders = useMemo(()=>{
161
- const now = Date.now() / 1000;
162
- return orders.filter((order)=>{
163
- var _order_customerEmail, _lifted_channel, _order_createdAtMs;
164
- var _CommerceModel_describeOrderDispute;
229
+ /**
230
+ * Each order with the values the grid shows and filters by. `liftLegacyOrder`
231
+ * names a status and channel on a row that predates them, and Product
232
+ * reads the line items first (AGL-1747): the flat `productId` is written
233
+ * only by the two buy-now Stripe paths.
234
+ */ const orderRows = useMemo(()=>orders.map((order)=>{
235
+ var _lifted_lineItems, _ref, _ref1, _ref2, _lifted_channel;
236
+ var _lifted_lineItems_, _lifted_lineItems1;
165
237
  const lifted = CommerceModel.liftLegacyOrder(order);
166
- // Line items first (AGL-1747): the flat `productId` is written only by
167
- // the two buy-now Stripe paths, so matching on it alone hid every cart,
168
- // POS and draft order behind the product filter.
169
- if (productFilter && !CommerceModel.orderContainsProduct(lifted, productFilter)) {
170
- return false;
171
- }
172
- if (statusFilter && lifted.status !== statusFilter) return false;
173
- if (customerFilter && !String((_order_customerEmail = order.customerEmail) != null ? _order_customerEmail : '').toLowerCase().includes(customerFilter.trim().toLowerCase())) {
174
- return false;
175
- }
176
- if (channelFilter && ((_lifted_channel = lifted.channel) != null ? _lifted_channel : 'online') !== channelFilter) {
177
- return false;
178
- }
179
- if (disputeFilter === 'open' && !CommerceModel.orderHasOpenDispute(lifted)) {
180
- return false;
181
- }
182
- // `lost` is the tone, not the raw status: a dispute that was WON also
183
- // closes with money untouched, and listing it under "charged back"
184
- // would tell a merchant they lost a case they won.
185
- if (disputeFilter === 'lost' && ((_CommerceModel_describeOrderDispute = CommerceModel.describeOrderDispute(lifted)) == null ? void 0 : _CommerceModel_describeOrderDispute.tone) !== 'lost') {
186
- return false;
187
- }
188
- // The same field the query orders by. `createdAt` is not universal
189
- // across order writers, and a row missing it read as epoch zero — which
190
- // both date filters excluded outright.
191
- const created = ((_order_createdAtMs = order.createdAtMs) != null ? _order_createdAtMs : 0) / 1000;
192
- if (dateFilter === '7d' && now - created > 7 * 86400) return false;
193
- if (dateFilter === '30d' && now - created > 30 * 86400) return false;
194
- return true;
195
- });
196
- }, [
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
+ return _extends({}, order, {
246
+ orderLabel: CommerceModel.formatOrderNumber(lifted, order.$id),
247
+ itemName: (_ref = (_ref1 = (_ref2 = (_lifted_lineItems1 = lifted.lineItems) == null ? void 0 : (_lifted_lineItems_ = _lifted_lineItems1[0]) == null ? void 0 : _lifted_lineItems_.name) != null ? _ref2 : productNames[order.productId]) != null ? _ref1 : order.productId) != null ? _ref : '',
248
+ statusKey: lifted.status,
249
+ channelKey: (_lifted_channel = lifted.channel) != null ? _lifted_channel : 'online',
250
+ netCents: CommerceModel.orderNetCents(lifted),
251
+ disputeBadge: dispute,
252
+ productIds,
253
+ disputeKey: CommerceModel.orderHasOpenDispute(lifted) ? 'open' : (dispute == null ? void 0 : dispute.tone) === 'lost' ? 'lost' : ''
254
+ });
255
+ }), [
197
256
  orders,
198
- productFilter,
199
- dateFilter,
200
- statusFilter,
201
- channelFilter,
202
- disputeFilter,
203
- customerFilter
257
+ productNames
258
+ ]);
259
+ const productOptions = useMemo(()=>productWindow.rows.map((product)=>{
260
+ var _product_name;
261
+ return {
262
+ value: String(product.$id),
263
+ label: String((_product_name = product.name) != null ? _product_name : product.$id)
264
+ };
265
+ }), [
266
+ productWindow
267
+ ]);
268
+ const filterOptions = useMemo(()=>({
269
+ statusKey: ORDER_STATUS_OPTIONS,
270
+ channelKey: ORDER_CHANNEL_OPTIONS,
271
+ productIds: productOptions,
272
+ disputeKey: ORDER_DISPUTE_OPTIONS
273
+ }), [
274
+ productOptions
204
275
  ]);
276
+ const listFilter = useListRowsFilter({
277
+ rows: orderRows,
278
+ fields: ORDER_FILTER_FIELDS,
279
+ options: filterOptions,
280
+ headers: ORDER_FILTER_HEADERS,
281
+ search: ORDER_SEARCH_FIELDS,
282
+ clauses,
283
+ onChange: setClauses
284
+ });
285
+ const visibleOrders = listFilter.rows;
286
+ const showingOpenDisputes = clauses.some((clause)=>clause.field === OPEN_DISPUTE_CLAUSE.field && clause.op === OPEN_DISPUTE_CLAUSE.op && clause.value === OPEN_DISPUTE_CLAUSE.value);
205
287
  /**
206
288
  * Raised over EVERY loaded order, not the visible ones (AGL-1796). A
207
289
  * merchant filtered to "delivered" still has a deadline running on a `paid`
@@ -340,6 +422,168 @@ import OrderDetailDialog, { DISPUTE_COLOR } from "./order-detail-dialog.componen
340
422
  /** What the draft dialog can actually offer. */ const selectableProducts = useMemo(()=>productWindow.rows.filter((product)=>!product.deletedAt), [
341
423
  productWindow
342
424
  ]);
425
+ /*
426
+ * The six columns `/product/commerce` advertises, in the order the mockup
427
+ * shows them — every fact scannable on its own, Channel included.
428
+ */ const orderColumns = useMemo(()=>[
429
+ {
430
+ field: 'orderLabel',
431
+ headerName: 'Order',
432
+ flex: 1,
433
+ minWidth: 160,
434
+ renderCell: ({ row })=>{
435
+ var _ref;
436
+ var _row_lineItems;
437
+ const extraItems = Math.max(0, ((_ref = (_row_lineItems = row.lineItems) == null ? void 0 : _row_lineItems.length) != null ? _ref : 0) - 1);
438
+ return /*#__PURE__*/ _jsxs(Stack, {
439
+ sx: {
440
+ minWidth: 0
441
+ },
442
+ children: [
443
+ /*#__PURE__*/ _jsx(Typography, {
444
+ variant: "body2",
445
+ noWrap: true,
446
+ children: row.orderLabel
447
+ }),
448
+ /*#__PURE__*/ _jsx(Typography, {
449
+ variant: "caption",
450
+ color: "text.secondary",
451
+ noWrap: true,
452
+ children: extraItems ? `${row.itemName} +${extraItems} more` : row.itemName
453
+ })
454
+ ]
455
+ });
456
+ }
457
+ },
458
+ {
459
+ field: 'customerEmail',
460
+ headerName: 'Customer',
461
+ flex: 1,
462
+ minWidth: 180,
463
+ renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
464
+ variant: "body2",
465
+ noWrap: true,
466
+ children: row.customerEmail || '—'
467
+ })
468
+ },
469
+ {
470
+ field: 'channelKey',
471
+ headerName: 'Channel',
472
+ width: 120,
473
+ renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
474
+ variant: "body2",
475
+ children: CommerceModel.orderChannelLabel(row.channelKey)
476
+ })
477
+ },
478
+ {
479
+ field: 'netCents',
480
+ headerName: 'Total',
481
+ type: 'number',
482
+ width: 150,
483
+ align: 'right',
484
+ headerAlign: 'right',
485
+ renderCell: ({ row })=>{
486
+ var _ref, _ref1;
487
+ var _row_totals;
488
+ return /*#__PURE__*/ _jsxs(Stack, {
489
+ sx: {
490
+ alignItems: 'flex-end',
491
+ minWidth: 0,
492
+ width: 1
493
+ },
494
+ children: [
495
+ /*#__PURE__*/ _jsx(Typography, {
496
+ variant: "body2",
497
+ children: `$${(row.netCents / 100).toFixed(2)}`
498
+ }),
499
+ row.refundedCents ? /*#__PURE__*/ _jsx(Typography, {
500
+ variant: "caption",
501
+ color: "text.secondary",
502
+ noWrap: true,
503
+ children: `$${(((_ref = (_ref1 = (_row_totals = row.totals) == null ? void 0 : _row_totals.totalCents) != null ? _ref1 : row.amountCents) != null ? _ref : 0) / 100).toFixed(2)} less refunds`
504
+ }) : null
505
+ ]
506
+ });
507
+ }
508
+ },
509
+ {
510
+ field: 'statusKey',
511
+ headerName: 'Status',
512
+ width: 230,
513
+ renderCell: ({ row })=>{
514
+ var _CommerceModel_ORDER_STATUS_LABELS_row_statusKey, _CommerceModel_ORDER_STATUS_COLOR_row_statusKey;
515
+ // A lost chargeback leaves `status: 'refunded'` (AGL-1787), so the
516
+ // status pill cannot tell the two apart either — which is why the
517
+ // dispute chip sits beside it rather than being folded into it.
518
+ const dispute = row.disputeBadge;
519
+ return /*#__PURE__*/ _jsxs(Stack, {
520
+ sx: {
521
+ minWidth: 0
522
+ },
523
+ children: [
524
+ /*#__PURE__*/ _jsxs(Stack, {
525
+ direction: "row",
526
+ spacing: 0.5,
527
+ sx: {
528
+ alignItems: 'center'
529
+ },
530
+ children: [
531
+ /*#__PURE__*/ _jsx(Chip, {
532
+ label: (_CommerceModel_ORDER_STATUS_LABELS_row_statusKey = CommerceModel.ORDER_STATUS_LABELS[row.statusKey]) != null ? _CommerceModel_ORDER_STATUS_LABELS_row_statusKey : row.statusKey,
533
+ size: "small",
534
+ color: (_CommerceModel_ORDER_STATUS_COLOR_row_statusKey = CommerceModel.ORDER_STATUS_COLOR[row.statusKey]) != null ? _CommerceModel_ORDER_STATUS_COLOR_row_statusKey : 'default',
535
+ variant: "outlined"
536
+ }),
537
+ dispute ? /*#__PURE__*/ _jsx(Tooltip, {
538
+ title: dispute.detail,
539
+ children: /*#__PURE__*/ _jsx(Chip, {
540
+ label: dispute.label,
541
+ size: "small",
542
+ color: DISPUTE_COLOR[dispute.tone],
543
+ variant: "filled"
544
+ })
545
+ }) : null
546
+ ]
547
+ }),
548
+ (dispute == null ? void 0 : dispute.evidenceDaysLeft) !== undefined ? /*#__PURE__*/ _jsx(Typography, {
549
+ variant: "caption",
550
+ component: "div",
551
+ color: dispute.evidenceDaysLeft < 0 ? 'error' : 'warning.main',
552
+ children: dispute.evidenceDaysLeft < 0 ? 'Evidence deadline passed' : `Evidence due in ${dispute.evidenceDaysLeft} day${dispute.evidenceDaysLeft === 1 ? '' : 's'}`
553
+ }) : null
554
+ ]
555
+ });
556
+ }
557
+ },
558
+ {
559
+ field: 'createdAtMs',
560
+ headerName: 'Date',
561
+ width: 130,
562
+ renderCell: ({ row })=>{
563
+ var _ref;
564
+ var _row_createdAt_toDate, _row_createdAt;
565
+ const createdAt = (_ref = (_row_createdAt = row.createdAt) == null ? void 0 : (_row_createdAt_toDate = _row_createdAt.toDate) == null ? void 0 : _row_createdAt_toDate.call(_row_createdAt)) != null ? _ref : row.createdAtMs ? new Date(row.createdAtMs) : null;
566
+ return /*#__PURE__*/ _jsxs(Stack, {
567
+ sx: {
568
+ minWidth: 0
569
+ },
570
+ children: [
571
+ /*#__PURE__*/ _jsx(Typography, {
572
+ variant: "body2",
573
+ noWrap: true,
574
+ children: createdAt ? createdAt.toLocaleDateString() : '—'
575
+ }),
576
+ /*#__PURE__*/ _jsx(Typography, {
577
+ variant: "caption",
578
+ color: "text.secondary",
579
+ noWrap: true,
580
+ children: createdAt ? createdAt.toLocaleTimeString() : ''
581
+ })
582
+ ]
583
+ });
584
+ }
585
+ }
586
+ ], []);
343
587
  return /*#__PURE__*/ _jsxs(CardDisplay, {
344
588
  header: 'Orders',
345
589
  help: pluginDocsHelp('commerce', {
@@ -416,10 +660,10 @@ import OrderDetailDialog, { DISPUTE_COLOR } from "./order-detail-dialog.componen
416
660
  }) : null,
417
661
  openDisputes.count > 0 ? /*#__PURE__*/ _jsxs(Alert, {
418
662
  severity: openDisputes.overdue ? 'error' : 'warning',
419
- action: disputeFilter === 'open' ? undefined : /*#__PURE__*/ _jsx(Button, {
663
+ action: showingOpenDisputes ? undefined : /*#__PURE__*/ _jsx(Button, {
420
664
  size: "small",
421
665
  color: "inherit",
422
- onClick: ()=>setDisputeFilter('open'),
666
+ onClick: ()=>setClauses(upsertListFilterClause(clauses, OPEN_DISPUTE_CLAUSE.field, OPEN_DISPUTE_CLAUSE)),
423
667
  children: 'Show them'
424
668
  }),
425
669
  children: [
@@ -433,130 +677,10 @@ import OrderDetailDialog, { DISPUTE_COLOR } from "./order-detail-dialog.componen
433
677
  direction: "row",
434
678
  spacing: 1,
435
679
  sx: {
436
- alignItems: 'center'
680
+ alignItems: 'center',
681
+ justifyContent: 'flex-end'
437
682
  },
438
683
  children: [
439
- /*#__PURE__*/ _jsxs(TextField, {
440
- select: true,
441
- size: "small",
442
- label: "Product",
443
- value: productFilter,
444
- onChange: (event)=>setProductFilter(event.target.value),
445
- sx: {
446
- minWidth: 160
447
- },
448
- children: [
449
- /*#__PURE__*/ _jsx(MenuItem, {
450
- value: "",
451
- children: 'All products'
452
- }),
453
- productWindow.rows.map((product)=>{
454
- var _product_name;
455
- return /*#__PURE__*/ _jsx(MenuItem, {
456
- value: product.$id,
457
- children: (_product_name = product.name) != null ? _product_name : product.$id
458
- }, product.$id);
459
- })
460
- ]
461
- }),
462
- /*#__PURE__*/ _jsxs(TextField, {
463
- select: true,
464
- size: "small",
465
- label: "Period",
466
- value: dateFilter,
467
- onChange: (event)=>setDateFilter(event.target.value),
468
- sx: {
469
- minWidth: 130
470
- },
471
- children: [
472
- /*#__PURE__*/ _jsx(MenuItem, {
473
- value: "",
474
- children: 'All time'
475
- }),
476
- /*#__PURE__*/ _jsx(MenuItem, {
477
- value: "7d",
478
- children: 'Last 7 days'
479
- }),
480
- /*#__PURE__*/ _jsx(MenuItem, {
481
- value: "30d",
482
- children: 'Last 30 days'
483
- })
484
- ]
485
- }),
486
- /*#__PURE__*/ _jsxs(TextField, {
487
- select: true,
488
- size: "small",
489
- label: "Status",
490
- value: statusFilter,
491
- onChange: (event)=>setStatusFilter(event.target.value),
492
- sx: {
493
- minWidth: 130
494
- },
495
- children: [
496
- /*#__PURE__*/ _jsx(MenuItem, {
497
- value: "",
498
- children: 'All statuses'
499
- }),
500
- Object.keys(CommerceModel.ORDER_STATUS_LABELS).map((status)=>/*#__PURE__*/ _jsx(MenuItem, {
501
- value: status,
502
- children: CommerceModel.ORDER_STATUS_LABELS[status]
503
- }, status))
504
- ]
505
- }),
506
- /*#__PURE__*/ _jsxs(TextField, {
507
- select: true,
508
- size: "small",
509
- label: "Channel",
510
- value: channelFilter,
511
- onChange: (event)=>setChannelFilter(event.target.value),
512
- sx: {
513
- minWidth: 120
514
- },
515
- children: [
516
- /*#__PURE__*/ _jsx(MenuItem, {
517
- value: "",
518
- children: 'All channels'
519
- }),
520
- Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).map((channel)=>/*#__PURE__*/ _jsx(MenuItem, {
521
- value: channel,
522
- children: CommerceModel.ORDER_CHANNEL_LABELS[channel]
523
- }, channel))
524
- ]
525
- }),
526
- /*#__PURE__*/ _jsx(TextField, {
527
- size: "small",
528
- label: "Customer",
529
- placeholder: "Email",
530
- value: customerFilter,
531
- onChange: (event)=>setCustomerFilter(event.target.value),
532
- sx: {
533
- minWidth: 180
534
- }
535
- }),
536
- /*#__PURE__*/ _jsxs(TextField, {
537
- select: true,
538
- size: "small",
539
- label: "Disputes",
540
- value: disputeFilter,
541
- onChange: (event)=>setDisputeFilter(event.target.value),
542
- sx: {
543
- minWidth: 140
544
- },
545
- children: [
546
- /*#__PURE__*/ _jsx(MenuItem, {
547
- value: "",
548
- children: 'All orders'
549
- }),
550
- /*#__PURE__*/ _jsx(MenuItem, {
551
- value: "open",
552
- children: 'Open dispute'
553
- }),
554
- /*#__PURE__*/ _jsx(MenuItem, {
555
- value: "lost",
556
- children: 'Charged back'
557
- })
558
- ]
559
- }),
560
684
  /*#__PURE__*/ _jsx(Button, {
561
685
  size: "small",
562
686
  onClick: handleExportCsv,
@@ -571,153 +695,25 @@ import OrderDetailDialog, { DISPUTE_COLOR } from "./order-detail-dialog.componen
571
695
  })
572
696
  ]
573
697
  }),
574
- /*#__PURE__*/ _jsxs(ScrollTable, {
575
- size: "small",
698
+ /*#__PURE__*/ _jsx(ListFilterChips, _extends({}, listFilter.chipsProps)),
699
+ /*#__PURE__*/ _jsx(ListTable, _extends({
576
700
  "aria-label": "Orders",
577
- children: [
578
- /*#__PURE__*/ _jsx(TableHead, {
579
- children: /*#__PURE__*/ _jsxs(TableRow, {
580
- children: [
581
- /*#__PURE__*/ _jsx(TableCell, {
582
- children: 'Order'
583
- }),
584
- /*#__PURE__*/ _jsx(TableCell, {
585
- children: 'Customer'
586
- }),
587
- /*#__PURE__*/ _jsx(TableCell, {
588
- children: 'Channel'
589
- }),
590
- /*#__PURE__*/ _jsx(TableCell, {
591
- align: "right",
592
- children: 'Total'
593
- }),
594
- /*#__PURE__*/ _jsx(TableCell, {
595
- children: 'Status'
596
- }),
597
- /*#__PURE__*/ _jsx(TableCell, {
598
- children: 'Date'
599
- })
600
- ]
601
- })
602
- }),
603
- /*#__PURE__*/ _jsx(TableBody, {
604
- children: visibleOrders.map((order)=>{
605
- var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _CommerceModel_ORDER_STATUS_LABELS_lifted_status, _CommerceModel_ORDER_STATUS_COLOR_lifted_status;
606
- var _order_createdAt_toDate, _order_createdAt, _lifted_lineItems_, _lifted_lineItems, _lifted_lineItems1, _lifted_totals;
607
- const lifted = CommerceModel.liftLegacyOrder(order);
608
- // A lost chargeback leaves `status: 'refunded'` (AGL-1787),
609
- // so the status pill on this row cannot tell the two apart
610
- // either — which is why the dispute chip sits beside it
611
- // rather than being folded into the status.
612
- const dispute = CommerceModel.describeOrderDispute(lifted);
613
- const createdAt = (_order_createdAt = order.createdAt) == null ? void 0 : (_order_createdAt_toDate = _order_createdAt.toDate) == null ? void 0 : _order_createdAt_toDate.call(_order_createdAt);
614
- const 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 : '';
615
- const extraItems = Math.max(0, ((_ref3 = (_lifted_lineItems1 = lifted.lineItems) == null ? void 0 : _lifted_lineItems1.length) != null ? _ref3 : 0) - 1);
616
- return /*#__PURE__*/ _jsxs(TableRow, {
617
- hover: true,
618
- onClick: ()=>setSelectedId(order.$id),
619
- sx: {
620
- cursor: 'pointer'
621
- },
622
- children: [
623
- /*#__PURE__*/ _jsxs(TableCell, {
624
- children: [
625
- /*#__PURE__*/ _jsx(Typography, {
626
- variant: "body2",
627
- children: CommerceModel.formatOrderNumber(lifted, order.$id)
628
- }),
629
- /*#__PURE__*/ _jsx(Typography, {
630
- variant: "caption",
631
- color: "text.secondary",
632
- children: extraItems ? `${itemName} +${extraItems} more` : itemName
633
- })
634
- ]
635
- }),
636
- /*#__PURE__*/ _jsx(TableCell, {
637
- children: /*#__PURE__*/ _jsx(Typography, {
638
- variant: "body2",
639
- noWrap: true,
640
- children: order.customerEmail || '—'
641
- })
642
- }),
643
- /*#__PURE__*/ _jsx(TableCell, {
644
- children: /*#__PURE__*/ _jsx(Typography, {
645
- variant: "body2",
646
- children: CommerceModel.orderChannelLabel(lifted.channel)
647
- })
648
- }),
649
- /*#__PURE__*/ _jsxs(TableCell, {
650
- align: "right",
651
- children: [
652
- /*#__PURE__*/ _jsx(Typography, {
653
- variant: "body2",
654
- children: `$${(CommerceModel.orderNetCents(lifted) / 100).toFixed(2)}`
655
- }),
656
- order.refundedCents ? /*#__PURE__*/ _jsx(Typography, {
657
- variant: "caption",
658
- color: "text.secondary",
659
- children: `$${(((_ref4 = (_ref5 = (_lifted_totals = lifted.totals) == null ? void 0 : _lifted_totals.totalCents) != null ? _ref5 : order.amountCents) != null ? _ref4 : 0) / 100).toFixed(2)} less refunds`
660
- }) : null
661
- ]
662
- }),
663
- /*#__PURE__*/ _jsxs(TableCell, {
664
- children: [
665
- /*#__PURE__*/ _jsxs(Stack, {
666
- direction: "row",
667
- spacing: 0.5,
668
- sx: {
669
- alignItems: 'center'
670
- },
671
- children: [
672
- /*#__PURE__*/ _jsx(Chip, {
673
- label: (_CommerceModel_ORDER_STATUS_LABELS_lifted_status = CommerceModel.ORDER_STATUS_LABELS[lifted.status]) != null ? _CommerceModel_ORDER_STATUS_LABELS_lifted_status : lifted.status,
674
- size: "small",
675
- color: (_CommerceModel_ORDER_STATUS_COLOR_lifted_status = CommerceModel.ORDER_STATUS_COLOR[lifted.status]) != null ? _CommerceModel_ORDER_STATUS_COLOR_lifted_status : 'default',
676
- variant: "outlined"
677
- }),
678
- dispute ? /*#__PURE__*/ _jsx(Tooltip, {
679
- title: dispute.detail,
680
- children: /*#__PURE__*/ _jsx(Chip, {
681
- label: dispute.label,
682
- size: "small",
683
- color: DISPUTE_COLOR[dispute.tone],
684
- variant: "filled"
685
- })
686
- }) : null
687
- ]
688
- }),
689
- (dispute == null ? void 0 : dispute.evidenceDaysLeft) !== undefined ? /*#__PURE__*/ _jsx(Typography, {
690
- variant: "caption",
691
- component: "div",
692
- color: dispute.evidenceDaysLeft < 0 ? 'error' : 'warning.main',
693
- children: dispute.evidenceDaysLeft < 0 ? 'Evidence deadline passed' : `Evidence due in ${dispute.evidenceDaysLeft} day${dispute.evidenceDaysLeft === 1 ? '' : 's'}`
694
- }) : null
695
- ]
696
- }),
697
- /*#__PURE__*/ _jsxs(TableCell, {
698
- children: [
699
- /*#__PURE__*/ _jsx(Typography, {
700
- variant: "body2",
701
- noWrap: true,
702
- children: createdAt ? createdAt.toLocaleDateString() : '—'
703
- }),
704
- /*#__PURE__*/ _jsx(Typography, {
705
- variant: "caption",
706
- color: "text.secondary",
707
- children: createdAt ? createdAt.toLocaleTimeString() : ''
708
- })
709
- ]
710
- })
711
- ]
712
- }, order.$id);
713
- })
714
- })
715
- ]
716
- }),
701
+ rows: visibleOrders,
702
+ columns: listFilter.filterColumns(orderColumns),
703
+ onOpen: (id)=>setSelectedId(id)
704
+ }, listFilter.gridProps, {
705
+ initialState: {
706
+ columns: {
707
+ columnVisibilityModel: ORDER_HIDDEN_COLUMNS
708
+ }
709
+ },
710
+ noRowsLabel: "No orders match these filters"
711
+ })),
717
712
  orderWindow.truncated ? /*
718
- * Where the filters and the export stop. Both run over what was
719
- * read, so a store past this window would otherwise see a status
720
- * filter return nothing and read it as having no such orders.
713
+ * Where the filters, the search and the export stop. All run over
714
+ * what was read, so a store past this window would otherwise see
715
+ * a status filter return nothing and read it as having no such
716
+ * orders.
721
717
  */ /*#__PURE__*/ _jsx(Typography, {
722
718
  variant: "caption",
723
719
  color: "text.secondary",