@aglyn/plugins-commerce 1.0.0-beta.288 → 1.0.0-beta.290
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/README.md +2 -2
- package/package.json +13 -13
- package/src/lib/components/commerce-console-page.js +29 -2
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/commerce-analytics-card.component.js +4 -2
- package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -1
- package/src/lib/components/console/commerce-pdf-download.d.ts +30 -0
- package/src/lib/components/console/commerce-pdf-download.js +53 -0
- package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
- package/src/lib/components/console/host-orders-card.component.js +5 -1
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/host-quotes-card.component.d.ts +34 -0
- package/src/lib/components/console/host-quotes-card.component.js +263 -0
- package/src/lib/components/console/host-quotes-card.component.js.map +1 -0
- package/src/lib/components/console/order-invoice.component.d.ts +6 -4
- package/src/lib/components/console/order-invoice.component.js +67 -11
- package/src/lib/components/console/order-invoice.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +33 -28
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -1
- package/src/lib/components/console/quote-editor-dialog.component.d.ts +45 -0
- package/src/lib/components/console/quote-editor-dialog.component.js +1317 -0
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -0
- package/src/lib/components/console/quote-pay-link.component.d.ts +38 -0
- package/src/lib/components/console/quote-pay-link.component.js +213 -0
- package/src/lib/components/console/quote-pay-link.component.js.map +1 -0
- package/src/lib/components/console/quote-proofs.component.d.ts +37 -0
- package/src/lib/components/console/quote-proofs.component.js +626 -0
- package/src/lib/components/console/quote-proofs.component.js.map +1 -0
- package/src/lib/components/console/quote-settings-card.component.d.ts +29 -0
- package/src/lib/components/console/quote-settings-card.component.js +121 -0
- package/src/lib/components/console/quote-settings-card.component.js.map +1 -0
- package/src/lib/components/console/tax-settings-card.component.js +4 -1
- package/src/lib/components/console/tax-settings-card.component.js.map +1 -1
- package/src/lib/components/quote-page-body.d.ts +33 -0
- package/src/lib/components/quote-page-body.js +724 -0
- package/src/lib/components/quote-page-body.js.map +1 -0
- package/src/lib/components/quote-page.d.ts +29 -0
- package/src/lib/components/quote-page.js +96 -0
- package/src/lib/components/quote-page.js.map +1 -0
- package/src/lib/components/quote-proof-panel.d.ts +42 -0
- package/src/lib/components/quote-proof-panel.js +436 -0
- package/src/lib/components/quote-proof-panel.js.map +1 -0
- package/src/lib/components/storefront-payment-element.d.ts +7 -1
- package/src/lib/components/storefront-payment-element.js +8 -3
- package/src/lib/components/storefront-payment-element.js.map +1 -1
- package/src/lib/constants/orders-list-query.js +6 -1
- package/src/lib/constants/orders-list-query.js.map +1 -1
- package/src/lib/constants/quote-page.d.ts +30 -0
- package/src/lib/constants/quote-page.js +35 -0
- package/src/lib/constants/quote-page.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +25 -2
- package/src/lib/model/commerce-orders.js +10 -1
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-quote-pay.d.ts +256 -0
- package/src/lib/model/commerce-quote-pay.js +426 -0
- package/src/lib/model/commerce-quote-pay.js.map +1 -0
- package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
- package/src/lib/model/commerce-quote-proofs.js +348 -0
- package/src/lib/model/commerce-quote-proofs.js.map +1 -0
- package/src/lib/model/commerce-quotes.d.ts +335 -0
- package/src/lib/model/commerce-quotes.js +522 -0
- package/src/lib/model/commerce-quotes.js.map +1 -0
- package/src/lib/model/index.d.ts +3 -0
- package/src/lib/model/index.js +3 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/plugin-permissions.d.ts +1 -1
- package/src/lib/model/plugin-permissions.js +20 -0
- package/src/lib/model/plugin-permissions.js.map +1 -1
- package/src/lib/server/api-v1/orders-and-products.js +6 -2
- package/src/lib/server/api-v1/orders-and-products.js.map +1 -1
- package/src/lib/server/billing-webhook.js +127 -52
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/checkout-session-expiry.d.ts +36 -0
- package/src/lib/server/checkout-session-expiry.js +63 -0
- package/src/lib/server/checkout-session-expiry.js.map +1 -0
- package/src/lib/server/commerce-pdf.d.ts +100 -0
- package/src/lib/server/commerce-pdf.js +283 -0
- package/src/lib/server/commerce-pdf.js.map +1 -0
- package/src/lib/server/draft-order.js +16 -8
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/order-print.d.ts +6 -0
- package/src/lib/server/order-print.js +22 -5
- package/src/lib/server/order-print.js.map +1 -1
- package/src/lib/server/pdf-response.d.ts +31 -0
- package/src/lib/server/pdf-response.js +30 -0
- package/src/lib/server/pdf-response.js.map +1 -0
- package/src/lib/server/person-eraser.d.ts +7 -0
- package/src/lib/server/person-eraser.js +24 -2
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/pos-reader-splash.d.ts +2 -18
- package/src/lib/server/pos-reader-splash.js +8 -93
- package/src/lib/server/pos-reader-splash.js.map +1 -1
- package/src/lib/server/pos-return.js +3 -1
- package/src/lib/server/pos-return.js.map +1 -1
- package/src/lib/server/quote-link-token.d.ts +39 -0
- package/src/lib/server/quote-link-token.js +97 -0
- package/src/lib/server/quote-link-token.js.map +1 -0
- package/src/lib/server/quote-pay.d.ts +110 -0
- package/src/lib/server/quote-pay.js +606 -0
- package/src/lib/server/quote-pay.js.map +1 -0
- package/src/lib/server/quote-proofs.d.ts +51 -0
- package/src/lib/server/quote-proofs.js +500 -0
- package/src/lib/server/quote-proofs.js.map +1 -0
- package/src/lib/server/quote-uploads.d.ts +33 -0
- package/src/lib/server/quote-uploads.js +29 -0
- package/src/lib/server/quote-uploads.js.map +1 -0
- package/src/lib/server/quote.d.ts +63 -0
- package/src/lib/server/quote.js +846 -0
- package/src/lib/server/quote.js.map +1 -0
- package/src/lib/server/related.js +2 -0
- package/src/lib/server/related.js.map +1 -1
- package/src/lib/server/site-image-source.d.ts +38 -0
- package/src/lib/server/site-image-source.js +130 -0
- package/src/lib/server/site-image-source.js.map +1 -0
- package/src/lib/server/site-page-resolver.d.ts +2 -0
- package/src/lib/server/site-page-resolver.js +75 -0
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +33 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/site.js +6 -0
- package/src/lib/site.js.map +1 -1
- package/src/lib/transfer/tracking.server.js +6 -0
- package/src/lib/transfer/tracking.server.js.map +1 -1
- package/src/lib/utils/commerce-document-pdf.d.ts +77 -0
- package/src/lib/utils/commerce-document-pdf.js +608 -0
- package/src/lib/utils/commerce-document-pdf.js.map +1 -0
- package/src/lib/utils/order-invoice.d.ts +52 -3
- package/src/lib/utils/order-invoice.js +116 -64
- package/src/lib/utils/order-invoice.js.map +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
19
|
+
import * as CommerceModel from "../../model/index.js";
|
|
20
|
+
import { posMoney } from "../../model/commerce-pos-ops.js";
|
|
21
|
+
import { commerceSitePermissionRefusal } from "../../model/plugin-permissions.js";
|
|
22
|
+
import { pluginDocsHelp } from "@aglyn/aglyn";
|
|
23
|
+
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
24
|
+
import { BlockedControl } from "@aglyn/shared-ui-jsx/components/blocked-control.component";
|
|
25
|
+
import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
26
|
+
import { ceilingedWindow, collectionCeiling, useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
|
|
27
|
+
import { Button, Chip, Stack, Typography } from "@mui/material";
|
|
28
|
+
import { collection, limit, query, where } from "firebase/firestore";
|
|
29
|
+
import { useEffect, useMemo, useState } from "react";
|
|
30
|
+
import QuoteEditorDialog from "./quote-editor-dialog.component.js";
|
|
31
|
+
/**
|
|
32
|
+
* How many quotes the list reads. The whole set is read and the grid sorts
|
|
33
|
+
* it by any column; a store past this many is told the list stops short.
|
|
34
|
+
*/ const QUOTE_CEILING = 500;
|
|
35
|
+
/** The products the editor's picker offers. */ const PRODUCT_WINDOW = 200;
|
|
36
|
+
const REFUSAL = commerceSitePermissionRefusal('commerce.quotes');
|
|
37
|
+
/** One quote as its row draws and sorts it. */ function quoteListRow(quote) {
|
|
38
|
+
var _quote_lineItems, _ref, _ref1, _ref2, _ref3;
|
|
39
|
+
var _quote_quote, _items_, _quote_totals, _quote_quote1, _CommerceModel_latestQuoteResponse;
|
|
40
|
+
const customer = (_quote_quote = quote.quote) == null ? void 0 : _quote_quote.customer;
|
|
41
|
+
const status = CommerceModel.quoteDisplayStatus(quote);
|
|
42
|
+
const items = (_quote_lineItems = quote.lineItems) != null ? _quote_lineItems : [];
|
|
43
|
+
return _extends({}, quote, {
|
|
44
|
+
customerLabel: (customer == null ? void 0 : customer.name) || (customer == null ? void 0 : customer.company) || (customer == null ? void 0 : customer.email) || '',
|
|
45
|
+
firstItem: (_ref = (_items_ = items[0]) == null ? void 0 : _items_.name) != null ? _ref : '',
|
|
46
|
+
moreItems: Math.max(0, items.length - 1),
|
|
47
|
+
statusKey: status.key,
|
|
48
|
+
statusLabel: status.label,
|
|
49
|
+
totalCents: Number((_ref1 = (_quote_totals = quote.totals) == null ? void 0 : _quote_totals.totalCents) != null ? _ref1 : 0),
|
|
50
|
+
// The customer's latest answer counts as an update (AGL-4096), so a quote
|
|
51
|
+
// they just approved or asked changes to sorts to the top.
|
|
52
|
+
updatedAtMs: Math.max(Number((_ref2 = (_quote_quote1 = quote.quote) == null ? void 0 : _quote_quote1.updatedAtMs) != null ? _ref2 : 0), Number((_ref3 = (_CommerceModel_latestQuoteResponse = CommerceModel.latestQuoteResponse(quote.quote)) == null ? void 0 : _CommerceModel_latestQuoteResponse.atMs) != null ? _ref3 : 0))
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Quotes (AGL-4093): prices put to a customer before they buy, beside the
|
|
57
|
+
* orders they become. A quote is an unpaid order on the `quote` channel, so
|
|
58
|
+
* this reads `hosts/{hostId}/orders` for that channel alone; the Orders list
|
|
59
|
+
* above never shows one until it is paid.
|
|
60
|
+
*/ export function HostQuotesCard(props) {
|
|
61
|
+
var _quotes_rows_find;
|
|
62
|
+
const { hostId, canEdit, canSend } = props;
|
|
63
|
+
const firestore = useFirestore();
|
|
64
|
+
const { data: quoteDocs, status } = useFirestoreCollection(()=>query(collection(firestore, 'hosts', hostId, 'orders'), where('channel', '==', 'quote'), limit(QUOTE_CEILING + 1)), [
|
|
65
|
+
firestore,
|
|
66
|
+
hostId
|
|
67
|
+
], {
|
|
68
|
+
idField: '$id'
|
|
69
|
+
});
|
|
70
|
+
const quotes = useMemo(()=>ceilingedWindow(quoteDocs != null ? quoteDocs : undefined, QUOTE_CEILING), [
|
|
71
|
+
quoteDocs
|
|
72
|
+
]);
|
|
73
|
+
const rows = useMemo(()=>Array.from(quotes.rows, quoteListRow), [
|
|
74
|
+
quotes
|
|
75
|
+
]);
|
|
76
|
+
const { data: productDocs } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'products'), PRODUCT_WINDOW), [
|
|
77
|
+
firestore,
|
|
78
|
+
hostId
|
|
79
|
+
], {
|
|
80
|
+
idField: '$id'
|
|
81
|
+
});
|
|
82
|
+
const products = useMemo(()=>ceilingedWindow(productDocs != null ? productDocs : undefined, PRODUCT_WINDOW).rows.filter((product)=>!product['deletedAt']), [
|
|
83
|
+
productDocs
|
|
84
|
+
]);
|
|
85
|
+
/** The quote open in the editor: its id, or `new`. */ const [openId, setOpenId] = useState(null);
|
|
86
|
+
const openQuote = openId && openId !== 'new' ? (_quotes_rows_find = quotes.rows.find((row)=>row.$id === openId)) != null ? _quotes_rows_find : null : null;
|
|
87
|
+
// A manager notice opens its quote (AGL-4096): `?quote=<id>` on the Orders
|
|
88
|
+
// page, once that quote has loaded.
|
|
89
|
+
const [linkedId, setLinkedId] = useState(()=>{
|
|
90
|
+
var _get;
|
|
91
|
+
return typeof window === 'undefined' ? '' : (_get = new URLSearchParams(window.location.search).get('quote')) != null ? _get : '';
|
|
92
|
+
});
|
|
93
|
+
useEffect(()=>{
|
|
94
|
+
if (!linkedId || !quotes.rows.some((row)=>row.$id === linkedId)) return;
|
|
95
|
+
setOpenId(linkedId);
|
|
96
|
+
setLinkedId('');
|
|
97
|
+
}, [
|
|
98
|
+
linkedId,
|
|
99
|
+
quotes.rows
|
|
100
|
+
]);
|
|
101
|
+
const columns = useMemo(()=>[
|
|
102
|
+
{
|
|
103
|
+
field: 'number',
|
|
104
|
+
headerName: 'Quote',
|
|
105
|
+
flex: 1,
|
|
106
|
+
minWidth: 160,
|
|
107
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsxs(Stack, {
|
|
108
|
+
sx: {
|
|
109
|
+
minWidth: 0
|
|
110
|
+
},
|
|
111
|
+
children: [
|
|
112
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
113
|
+
variant: "body2",
|
|
114
|
+
noWrap: true,
|
|
115
|
+
children: CommerceModel.formatQuoteNumber(row, row.$id)
|
|
116
|
+
}),
|
|
117
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
118
|
+
variant: "caption",
|
|
119
|
+
color: "text.secondary",
|
|
120
|
+
noWrap: true,
|
|
121
|
+
children: row.moreItems ? `${row.firstItem} and more` : row.firstItem
|
|
122
|
+
})
|
|
123
|
+
]
|
|
124
|
+
})
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
field: 'customerLabel',
|
|
128
|
+
headerName: 'Customer',
|
|
129
|
+
flex: 1,
|
|
130
|
+
minWidth: 160,
|
|
131
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
|
|
132
|
+
variant: "body2",
|
|
133
|
+
noWrap: true,
|
|
134
|
+
children: row.customerLabel || '—'
|
|
135
|
+
})
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
field: 'totalCents',
|
|
139
|
+
headerName: 'Total',
|
|
140
|
+
type: 'number',
|
|
141
|
+
width: 140,
|
|
142
|
+
align: 'right',
|
|
143
|
+
headerAlign: 'right',
|
|
144
|
+
renderCell: ({ row })=>{
|
|
145
|
+
var _row_quote_tax, _row_quote;
|
|
146
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
147
|
+
sx: {
|
|
148
|
+
alignItems: 'flex-end',
|
|
149
|
+
minWidth: 0,
|
|
150
|
+
width: 1
|
|
151
|
+
},
|
|
152
|
+
children: [
|
|
153
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
154
|
+
variant: "body2",
|
|
155
|
+
children: posMoney(row.totalCents)
|
|
156
|
+
}),
|
|
157
|
+
((_row_quote = row.quote) == null ? void 0 : (_row_quote_tax = _row_quote.tax) == null ? void 0 : _row_quote_tax.kind) === 'at-payment' && CommerceModel.isUnpaidQuote(row) ? /*#__PURE__*/ _jsx(Typography, {
|
|
158
|
+
variant: "caption",
|
|
159
|
+
color: "text.secondary",
|
|
160
|
+
noWrap: true,
|
|
161
|
+
children: 'plus tax at payment'
|
|
162
|
+
}) : null
|
|
163
|
+
]
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
field: 'statusLabel',
|
|
169
|
+
headerName: 'Status',
|
|
170
|
+
width: 180,
|
|
171
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(Chip, {
|
|
172
|
+
size: "small",
|
|
173
|
+
variant: "outlined",
|
|
174
|
+
label: row.statusLabel,
|
|
175
|
+
color: CommerceModel.QUOTE_STATUS_COLOR[row.statusKey]
|
|
176
|
+
})
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
field: 'updatedAtMs',
|
|
180
|
+
headerName: 'Updated',
|
|
181
|
+
type: 'number',
|
|
182
|
+
width: 140,
|
|
183
|
+
align: 'left',
|
|
184
|
+
headerAlign: 'left',
|
|
185
|
+
renderCell: ({ row })=>/*#__PURE__*/ _jsx(Typography, {
|
|
186
|
+
variant: "body2",
|
|
187
|
+
noWrap: true,
|
|
188
|
+
children: row.updatedAtMs ? new Date(row.updatedAtMs).toLocaleDateString() : '—'
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
], []);
|
|
192
|
+
const empty = status === 'success' && rows.length === 0;
|
|
193
|
+
return /*#__PURE__*/ _jsxs(CardDisplay, {
|
|
194
|
+
header: 'Quotes',
|
|
195
|
+
subheader: 'Prices for a customer’s order before they buy',
|
|
196
|
+
help: pluginDocsHelp('quotes', {
|
|
197
|
+
anchor: '#make-a-quote',
|
|
198
|
+
title: 'Quotes',
|
|
199
|
+
excerpt: 'Put a price to a customer with your products, items typed for the job and anything ' + 'included. A quote becomes an order when it is paid.'
|
|
200
|
+
}),
|
|
201
|
+
HeaderProps: {
|
|
202
|
+
action: /*#__PURE__*/ _jsx(BlockedControl, {
|
|
203
|
+
blocked: !canEdit,
|
|
204
|
+
reason: REFUSAL,
|
|
205
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
206
|
+
size: "small",
|
|
207
|
+
variant: "contained",
|
|
208
|
+
onClick: ()=>setOpenId('new'),
|
|
209
|
+
children: 'New quote'
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
},
|
|
213
|
+
contentGutterX: true,
|
|
214
|
+
contentGutterY: true,
|
|
215
|
+
children: [
|
|
216
|
+
empty ? /*#__PURE__*/ _jsx(Typography, {
|
|
217
|
+
variant: "body2",
|
|
218
|
+
color: "text.secondary",
|
|
219
|
+
children: 'No quotes yet. A quote prices a customer’s order before they buy: your products, ' + 'items typed for the job, and anything you include at no charge.'
|
|
220
|
+
}) : /*#__PURE__*/ _jsxs(Stack, {
|
|
221
|
+
spacing: 1,
|
|
222
|
+
children: [
|
|
223
|
+
quotes.truncated ? /*#__PURE__*/ _jsx(Typography, {
|
|
224
|
+
variant: "caption",
|
|
225
|
+
color: "text.secondary",
|
|
226
|
+
children: 'This store has more quotes than the list shows at once.'
|
|
227
|
+
}) : null,
|
|
228
|
+
/*#__PURE__*/ _jsx(ListTable, {
|
|
229
|
+
"aria-label": "Quotes",
|
|
230
|
+
rows: rows,
|
|
231
|
+
columns: columns,
|
|
232
|
+
loading: status === 'loading',
|
|
233
|
+
onOpen: (id)=>setOpenId(id),
|
|
234
|
+
quickFilter: true,
|
|
235
|
+
initialState: {
|
|
236
|
+
sorting: {
|
|
237
|
+
sortModel: [
|
|
238
|
+
{
|
|
239
|
+
field: 'updatedAtMs',
|
|
240
|
+
sort: 'desc'
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
noRowsLabel: "No quotes match"
|
|
246
|
+
})
|
|
247
|
+
]
|
|
248
|
+
}),
|
|
249
|
+
openId === 'new' || openQuote ? /*#__PURE__*/ _jsx(QuoteEditorDialog, {
|
|
250
|
+
hostId: hostId,
|
|
251
|
+
quote: openQuote,
|
|
252
|
+
products: products,
|
|
253
|
+
canEdit: canEdit,
|
|
254
|
+
canSend: canSend,
|
|
255
|
+
onClose: ()=>setOpenId(null)
|
|
256
|
+
}, openId) : null
|
|
257
|
+
]
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
HostQuotesCard.displayName = 'HostQuotesCard';
|
|
261
|
+
export default HostQuotesCard;
|
|
262
|
+
|
|
263
|
+
//# sourceMappingURL=host-quotes-card.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/host-quotes-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\n'use client'\n\nimport * as CommerceModel from '../../model'\nimport { posMoney } from '../../model/commerce-pos-ops'\nimport { commerceSitePermissionRefusal } from '../../model/plugin-permissions'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { BlockedControl } from '@aglyn/shared-ui-jsx/components/blocked-control.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, limit, query, where } from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport QuoteEditorDialog, { type QuoteRow } from './quote-editor-dialog.component'\n\nexport interface HostQuotesCardProps {\n hostId: string\n /** Holds \"Create and edit quotes\" (`commerce.quotes`). */\n canEdit: boolean\n /** Holds \"Send quotes and invoices to customers\" (`commerce.quotes.send`). */\n canSend: boolean\n}\n\n/**\n * How many quotes the list reads. The whole set is read and the grid sorts\n * it by any column; a store past this many is told the list stops short.\n */\nconst QUOTE_CEILING = 500\n\n/** The products the editor's picker offers. */\nconst PRODUCT_WINDOW = 200\n\nconst REFUSAL = commerceSitePermissionRefusal('commerce.quotes')\n\ninterface QuoteListRow extends QuoteRow {\n customerLabel: string\n firstItem: string\n moreItems: number\n statusKey: ReturnType<typeof CommerceModel.quoteDisplayStatus>['key']\n statusLabel: string\n totalCents: number\n updatedAtMs: number\n}\n\n/** One quote as its row draws and sorts it. */\nfunction quoteListRow(quote: QuoteRow): QuoteListRow {\n const customer = quote.quote?.customer\n const status = CommerceModel.quoteDisplayStatus(quote)\n const items = quote.lineItems ?? []\n return {\n ...quote,\n customerLabel: customer?.name || customer?.company || customer?.email || '',\n firstItem: items[0]?.name ?? '',\n moreItems: Math.max(0, items.length - 1),\n statusKey: status.key,\n statusLabel: status.label,\n totalCents: Number(quote.totals?.totalCents ?? 0),\n // The customer's latest answer counts as an update (AGL-4096), so a quote\n // they just approved or asked changes to sorts to the top.\n updatedAtMs: Math.max(\n Number(quote.quote?.updatedAtMs ?? 0),\n Number(CommerceModel.latestQuoteResponse(quote.quote)?.atMs ?? 0),\n ),\n }\n}\n\n/**\n * Quotes (AGL-4093): prices put to a customer before they buy, beside the\n * orders they become. A quote is an unpaid order on the `quote` channel, so\n * this reads `hosts/{hostId}/orders` for that channel alone; the Orders list\n * above never shows one until it is paid.\n */\nexport function HostQuotesCard(props: HostQuotesCardProps) {\n const { hostId, canEdit, canSend } = props\n const firestore = useFirestore()\n const { data: quoteDocs, status } = useFirestoreCollection<QuoteRow>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'orders'),\n where('channel', '==', 'quote'),\n limit(QUOTE_CEILING + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const quotes = useMemo(() => ceilingedWindow<QuoteRow>(quoteDocs ?? undefined, QUOTE_CEILING), [quoteDocs])\n const rows = useMemo(() => Array.from(quotes.rows, quoteListRow), [quotes])\n\n const { data: productDocs } = useFirestoreCollection<Record<string, any>>(\n () => collectionCeiling(collection(firestore, 'hosts', hostId, 'products'), PRODUCT_WINDOW),\n [firestore, hostId],\n { idField: '$id' },\n )\n const products = useMemo(\n () => ceilingedWindow<Record<string, any>>(productDocs ?? undefined, PRODUCT_WINDOW).rows.filter((product) => !product['deletedAt']),\n [productDocs],\n )\n\n /** The quote open in the editor: its id, or `new`. */\n const [openId, setOpenId] = useState<string | null>(null)\n const openQuote = openId && openId !== 'new' ? (quotes.rows.find((row) => row.$id === openId) ?? null) : null\n\n // A manager notice opens its quote (AGL-4096): `?quote=<id>` on the Orders\n // page, once that quote has loaded.\n const [linkedId, setLinkedId] = useState(() =>\n typeof window === 'undefined' ? '' : (new URLSearchParams(window.location.search).get('quote') ?? ''),\n )\n useEffect(() => {\n if (!linkedId || !quotes.rows.some((row) => row.$id === linkedId)) return\n setOpenId(linkedId)\n setLinkedId('')\n }, [linkedId, quotes.rows])\n\n const columns = useMemo<GridColDef[]>(\n () => [\n {\n field: 'number',\n headerName: 'Quote',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {CommerceModel.formatQuoteNumber(row, row.$id)}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {row.moreItems ? `${row.firstItem} and more` : row.firstItem}\n </Typography>\n </Stack>\n ),\n },\n {\n field: 'customerLabel',\n headerName: 'Customer',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Typography variant=\"body2\" noWrap>\n {row.customerLabel || '—'}\n </Typography>\n ),\n },\n {\n field: 'totalCents',\n headerName: 'Total',\n type: 'number',\n width: 140,\n align: 'right',\n headerAlign: 'right',\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Stack sx={{ alignItems: 'flex-end', minWidth: 0, width: 1 }}>\n <Typography variant=\"body2\">{posMoney(row.totalCents)}</Typography>\n {row.quote?.tax?.kind === 'at-payment' && CommerceModel.isUnpaidQuote(row) ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {'plus tax at payment'}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'statusLabel',\n headerName: 'Status',\n width: 180,\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={row.statusLabel}\n color={CommerceModel.QUOTE_STATUS_COLOR[row.statusKey]}\n />\n ),\n },\n {\n field: 'updatedAtMs',\n headerName: 'Updated',\n type: 'number',\n width: 140,\n align: 'left',\n headerAlign: 'left',\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Typography variant=\"body2\" noWrap>\n {row.updatedAtMs ? new Date(row.updatedAtMs).toLocaleDateString() : '—'}\n </Typography>\n ),\n },\n ],\n [],\n )\n\n const empty = status === 'success' && rows.length === 0\n\n return (\n <CardDisplay\n header={'Quotes'}\n subheader={'Prices for a customer’s order before they buy'}\n help={pluginDocsHelp('quotes', {\n anchor: '#make-a-quote',\n title: 'Quotes',\n excerpt:\n 'Put a price to a customer with your products, items typed for the job and anything ' +\n 'included. A quote becomes an order when it is paid.',\n })}\n HeaderProps={{\n action: (\n <BlockedControl blocked={!canEdit} reason={REFUSAL}>\n <Button size=\"small\" variant=\"contained\" onClick={() => setOpenId('new')}>\n {'New quote'}\n </Button>\n </BlockedControl>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n {empty ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No quotes yet. A quote prices a customer’s order before they buy: your products, ' +\n 'items typed for the job, and anything you include at no charge.'}\n </Typography>\n ) : (\n <Stack spacing={1}>\n {quotes.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'This store has more quotes than the list shows at once.'}\n </Typography>\n ) : null}\n <ListTable\n aria-label=\"Quotes\"\n rows={rows}\n columns={columns}\n loading={status === 'loading'}\n onOpen={(id) => setOpenId(id)}\n quickFilter\n initialState={{ sorting: { sortModel: [{ field: 'updatedAtMs', sort: 'desc' }] } }}\n noRowsLabel=\"No quotes match\"\n />\n </Stack>\n )}\n {/* A quote deleted while open closes its editor rather than turn it into a new one. */}\n {openId === 'new' || openQuote ? (\n <QuoteEditorDialog\n key={openId}\n hostId={hostId}\n quote={openQuote}\n products={products}\n canEdit={canEdit}\n canSend={canSend}\n onClose={() => setOpenId(null)}\n />\n ) : null}\n </CardDisplay>\n )\n}\nHostQuotesCard.displayName = 'HostQuotesCard'\n\nexport default HostQuotesCard\n"],"names":["CommerceModel","posMoney","commerceSitePermissionRefusal","pluginDocsHelp","CardDisplay","BlockedControl","ListTable","ceilingedWindow","collectionCeiling","useFirestore","useFirestoreCollection","Button","Chip","Stack","Typography","collection","limit","query","where","useEffect","useMemo","useState","QuoteEditorDialog","QUOTE_CEILING","PRODUCT_WINDOW","REFUSAL","quoteListRow","quote","items","customer","status","quoteDisplayStatus","lineItems","customerLabel","name","company","email","firstItem","moreItems","Math","max","length","statusKey","key","statusLabel","label","totalCents","Number","totals","updatedAtMs","latestQuoteResponse","atMs","HostQuotesCard","props","quotes","hostId","canEdit","canSend","firestore","data","quoteDocs","idField","undefined","rows","Array","from","productDocs","products","filter","product","openId","setOpenId","openQuote","find","row","$id","linkedId","setLinkedId","window","URLSearchParams","location","search","get","some","columns","field","headerName","flex","minWidth","renderCell","sx","variant","noWrap","formatQuoteNumber","color","type","width","align","headerAlign","alignItems","tax","kind","isUnpaidQuote","size","QUOTE_STATUS_COLOR","Date","toLocaleDateString","empty","header","subheader","help","anchor","title","excerpt","HeaderProps","action","blocked","reason","onClick","contentGutterX","contentGutterY","spacing","truncated","aria-label","loading","onOpen","id","quickFilter","initialState","sorting","sortModel","sort","noRowsLabel","onClose","displayName"],"mappings":"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"}
|
|
@@ -21,10 +21,12 @@ export interface OrderInvoiceButtonProps {
|
|
|
21
21
|
order: CommerceModel.HostOrder;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* "Invoice" (AGL-3611), beside "Packing slip" in the order dialog:
|
|
25
|
-
* as a printable bill,
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* "Invoice" (AGL-3611), beside "Packing slip" in the order dialog: a menu of
|
|
25
|
+
* "Print invoice", the order as a printable bill, and "Download PDF"
|
|
26
|
+
* (AGL-4095), the branded invoice the server makes in the site's colors with
|
|
27
|
+
* its logo (`commerce/order-print?kind=invoice-pdf`, "Manage orders"). The
|
|
28
|
+
* store's name, currency and footer are read when one is clicked, not while
|
|
29
|
+
* the dialog is open, so the dialog listens to nothing more.
|
|
28
30
|
*/
|
|
29
31
|
export declare function OrderInvoiceButton(props: OrderInvoiceButtonProps): import("react").JSX.Element;
|
|
30
32
|
export declare namespace OrderInvoiceButton {
|
|
@@ -14,22 +14,57 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
19
|
+
import { useFirestore, useUser } from "@aglyn/tenant-feature-instance";
|
|
20
|
+
import { Button, Menu, MenuItem } from "@mui/material";
|
|
20
21
|
import { doc, getDoc } from "firebase/firestore";
|
|
21
22
|
import { useCallback, useState } from "react";
|
|
22
23
|
import { buildOrderInvoiceHtml, invoiceStoreFrom } from "../../utils/order-invoice.js";
|
|
24
|
+
import { downloadCommercePdf } from "./commerce-pdf-download.js";
|
|
23
25
|
/**
|
|
24
|
-
* "Invoice" (AGL-3611), beside "Packing slip" in the order dialog:
|
|
25
|
-
* as a printable bill,
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* "Invoice" (AGL-3611), beside "Packing slip" in the order dialog: a menu of
|
|
27
|
+
* "Print invoice", the order as a printable bill, and "Download PDF"
|
|
28
|
+
* (AGL-4095), the branded invoice the server makes in the site's colors with
|
|
29
|
+
* its logo (`commerce/order-print?kind=invoice-pdf`, "Manage orders"). The
|
|
30
|
+
* store's name, currency and footer are read when one is clicked, not while
|
|
31
|
+
* the dialog is open, so the dialog listens to nothing more.
|
|
28
32
|
*/ export function OrderInvoiceButton(props) {
|
|
29
33
|
const { hostId, orderId, order } = props;
|
|
30
34
|
const firestore = useFirestore();
|
|
35
|
+
const { data: user } = useUser();
|
|
36
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
31
37
|
const [busy, setBusy] = useState(false);
|
|
38
|
+
const [anchor, setAnchor] = useState(null);
|
|
39
|
+
const handleDownload = useCallback(async ()=>{
|
|
40
|
+
setAnchor(null);
|
|
41
|
+
setBusy(true);
|
|
42
|
+
try {
|
|
43
|
+
var _order_number;
|
|
44
|
+
const query = new URLSearchParams({
|
|
45
|
+
hostId,
|
|
46
|
+
orderId,
|
|
47
|
+
kind: 'invoice-pdf'
|
|
48
|
+
});
|
|
49
|
+
const refused = await downloadCommercePdf(user, `/api/commerce/order-print?${query.toString()}`, {
|
|
50
|
+
method: 'GET'
|
|
51
|
+
}, `invoice-${(_order_number = order.number) != null ? _order_number : orderId.slice(-6)}.pdf`);
|
|
52
|
+
if (refused) enqueueSnackbar(refused, {
|
|
53
|
+
variant: 'error',
|
|
54
|
+
allowDuplicate: true
|
|
55
|
+
});
|
|
56
|
+
} finally{
|
|
57
|
+
setBusy(false);
|
|
58
|
+
}
|
|
59
|
+
}, [
|
|
60
|
+
user,
|
|
61
|
+
hostId,
|
|
62
|
+
orderId,
|
|
63
|
+
order.number,
|
|
64
|
+
enqueueSnackbar
|
|
65
|
+
]);
|
|
32
66
|
const handleInvoice = useCallback(async ()=>{
|
|
67
|
+
setAnchor(null);
|
|
33
68
|
// Opened before the reads, inside the click, so a popup blocker sees the
|
|
34
69
|
// user's gesture.
|
|
35
70
|
const win = window.open('', '_blank', 'width=720,height=900');
|
|
@@ -61,10 +96,31 @@ import { buildOrderInvoiceHtml, invoiceStoreFrom } from "../../utils/order-invoi
|
|
|
61
96
|
orderId,
|
|
62
97
|
order
|
|
63
98
|
]);
|
|
64
|
-
return /*#__PURE__*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
99
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
100
|
+
children: [
|
|
101
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
102
|
+
disabled: busy,
|
|
103
|
+
"aria-haspopup": "menu",
|
|
104
|
+
"aria-expanded": anchor ? 'true' : undefined,
|
|
105
|
+
onClick: (event)=>setAnchor(event.currentTarget),
|
|
106
|
+
children: 'Invoice'
|
|
107
|
+
}),
|
|
108
|
+
/*#__PURE__*/ _jsxs(Menu, {
|
|
109
|
+
anchorEl: anchor,
|
|
110
|
+
open: Boolean(anchor),
|
|
111
|
+
onClose: ()=>setAnchor(null),
|
|
112
|
+
children: [
|
|
113
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
114
|
+
onClick: ()=>void handleInvoice(),
|
|
115
|
+
children: 'Print invoice'
|
|
116
|
+
}),
|
|
117
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
118
|
+
onClick: ()=>void handleDownload(),
|
|
119
|
+
children: 'Download PDF'
|
|
120
|
+
})
|
|
121
|
+
]
|
|
122
|
+
})
|
|
123
|
+
]
|
|
68
124
|
});
|
|
69
125
|
}
|
|
70
126
|
OrderInvoiceButton.displayName = 'OrderInvoiceButton';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/order-invoice.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\n'use client'\n\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { Button } from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport type * as CommerceModel from '../../model'\nimport { buildOrderInvoiceHtml, invoiceStoreFrom } from '../../utils/order-invoice'\n\nexport interface OrderInvoiceButtonProps {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n}\n\n/**\n * \"Invoice\" (AGL-3611), beside \"Packing slip\" in the order dialog:
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/order-invoice.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\n'use client'\n\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { Button, Menu, MenuItem } from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport type * as CommerceModel from '../../model'\nimport { buildOrderInvoiceHtml, invoiceStoreFrom } from '../../utils/order-invoice'\nimport { downloadCommercePdf } from './commerce-pdf-download'\n\nexport interface OrderInvoiceButtonProps {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n}\n\n/**\n * \"Invoice\" (AGL-3611), beside \"Packing slip\" in the order dialog: a menu of\n * \"Print invoice\", the order as a printable bill, and \"Download PDF\"\n * (AGL-4095), the branded invoice the server makes in the site's colors with\n * its logo (`commerce/order-print?kind=invoice-pdf`, \"Manage orders\"). The\n * store's name, currency and footer are read when one is clicked, not while\n * the dialog is open, so the dialog listens to nothing more.\n */\nexport function OrderInvoiceButton(props: OrderInvoiceButtonProps) {\n const { hostId, orderId, order } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const [busy, setBusy] = useState(false)\n const [anchor, setAnchor] = useState<HTMLElement | null>(null)\n\n const handleDownload = useCallback(async () => {\n setAnchor(null)\n setBusy(true)\n try {\n const query = new URLSearchParams({ hostId, orderId, kind: 'invoice-pdf' })\n const refused = await downloadCommercePdf(\n user,\n `/api/commerce/order-print?${query.toString()}`,\n { method: 'GET' },\n `invoice-${order.number ?? orderId.slice(-6)}.pdf`,\n )\n if (refused) enqueueSnackbar(refused, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(false)\n }\n }, [user, hostId, orderId, order.number, enqueueSnackbar])\n\n const handleInvoice = useCallback(async () => {\n setAnchor(null)\n // Opened before the reads, inside the click, so a popup blocker sees the\n // user's gesture.\n const win = window.open('', '_blank', 'width=720,height=900')\n if (!win) return\n setBusy(true)\n try {\n const [host, store] = await Promise.all([\n getDoc(doc(firestore, 'hosts', hostId)).catch(() => null),\n getDoc(doc(firestore, 'hosts', hostId, 'settings', 'store')).catch(() => null),\n ])\n const html = buildOrderInvoiceHtml({\n order,\n orderId,\n store: invoiceStoreFrom(\n host?.data() as Record<string, unknown> | undefined,\n store?.data() as Record<string, unknown> | undefined,\n ),\n })\n // Every value in `html` is escaped by the builder (AGL-2283).\n win.document.write(html)\n win.document.close()\n // Print from the opener, not an injected script: the popup inherits\n // the console's CSP (AGL-523).\n win.focus()\n win.print()\n } finally {\n setBusy(false)\n }\n }, [firestore, hostId, orderId, order])\n\n return (\n <>\n <Button\n disabled={busy}\n aria-haspopup=\"menu\"\n aria-expanded={anchor ? 'true' : undefined}\n onClick={(event) => setAnchor(event.currentTarget)}\n >\n {'Invoice'}\n </Button>\n <Menu anchorEl={anchor} open={Boolean(anchor)} onClose={() => setAnchor(null)}>\n <MenuItem onClick={() => void handleInvoice()}>{'Print invoice'}</MenuItem>\n <MenuItem onClick={() => void handleDownload()}>{'Download PDF'}</MenuItem>\n </Menu>\n </>\n )\n}\nOrderInvoiceButton.displayName = 'OrderInvoiceButton'\n\nexport default OrderInvoiceButton\n"],"names":["useSnackbar","useFirestore","useUser","Button","Menu","MenuItem","doc","getDoc","useCallback","useState","buildOrderInvoiceHtml","invoiceStoreFrom","downloadCommercePdf","OrderInvoiceButton","props","hostId","orderId","order","firestore","data","user","enqueueSnackbar","busy","setBusy","anchor","setAnchor","handleDownload","query","URLSearchParams","kind","refused","toString","method","number","slice","variant","allowDuplicate","handleInvoice","win","window","open","host","store","Promise","all","catch","html","document","write","close","focus","print","disabled","aria-haspopup","aria-expanded","undefined","onClick","event","currentTarget","anchorEl","Boolean","onClose","displayName"],"mappings":"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"}
|
|
@@ -165,6 +165,39 @@ function PosCheckInsBody(props) {
|
|
|
165
165
|
minWidth: 150,
|
|
166
166
|
valueGetter: (_value, row)=>row.customerName || row.customerEmail || ''
|
|
167
167
|
},
|
|
168
|
+
{
|
|
169
|
+
// What a pass waiting at the window can still have done to it: a
|
|
170
|
+
// rain check, or one taken back; and a stay's guest count (AGL-4043).
|
|
171
|
+
// Not a value, so nothing to sort.
|
|
172
|
+
//
|
|
173
|
+
// Second, beside the name it acts on, rather than trailing. The grid
|
|
174
|
+
// has no column pinning, so a trailing column sat past the right edge
|
|
175
|
+
// of any window narrower than every column put together — on a
|
|
176
|
+
// laptop, and always on a phone — and the window could not see it.
|
|
177
|
+
field: 'rainCheckAction',
|
|
178
|
+
headerName: 'Actions',
|
|
179
|
+
minWidth: 150,
|
|
180
|
+
sortable: false,
|
|
181
|
+
filterable: false,
|
|
182
|
+
hideable: false,
|
|
183
|
+
disableColumnMenu: true,
|
|
184
|
+
renderCell: ({ row })=>row.source === 'reservation' && row.reservationId ? /*#__PURE__*/ _jsx(Button, {
|
|
185
|
+
size: "small",
|
|
186
|
+
disabled: !canManagePos,
|
|
187
|
+
onClick: ()=>setGuestsFor(row.reservationId),
|
|
188
|
+
children: 'Guests'
|
|
189
|
+
}) : row.source !== 'order' || row.status !== 'expected' ? null : row.rainCheck ? /*#__PURE__*/ _jsx(Button, {
|
|
190
|
+
size: "small",
|
|
191
|
+
disabled: !canManagePos,
|
|
192
|
+
onClick: ()=>void undoRainCheck(row),
|
|
193
|
+
children: 'Undo rain check'
|
|
194
|
+
}) : /*#__PURE__*/ _jsx(Button, {
|
|
195
|
+
size: "small",
|
|
196
|
+
disabled: !canManagePos || !todayKey,
|
|
197
|
+
onClick: ()=>setRainCheckRow(row),
|
|
198
|
+
children: 'Rain check'
|
|
199
|
+
})
|
|
200
|
+
},
|
|
168
201
|
{
|
|
169
202
|
field: 'itemName',
|
|
170
203
|
headerName: 'Item',
|
|
@@ -225,34 +258,6 @@ function PosCheckInsBody(props) {
|
|
|
225
258
|
headerName: 'Wristband',
|
|
226
259
|
minWidth: 120,
|
|
227
260
|
valueGetter: (_value, row)=>describeWristband(row.wristband)
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
// What a pass waiting at the window can still have done to it: a
|
|
231
|
-
// rain check, or one taken back; and a stay's guest count (AGL-4043).
|
|
232
|
-
// Not a value, so nothing to sort.
|
|
233
|
-
field: 'rainCheckAction',
|
|
234
|
-
headerName: '',
|
|
235
|
-
minWidth: 150,
|
|
236
|
-
sortable: false,
|
|
237
|
-
filterable: false,
|
|
238
|
-
hideable: false,
|
|
239
|
-
disableColumnMenu: true,
|
|
240
|
-
renderCell: ({ row })=>row.source === 'reservation' && row.reservationId ? /*#__PURE__*/ _jsx(Button, {
|
|
241
|
-
size: "small",
|
|
242
|
-
disabled: !canManagePos,
|
|
243
|
-
onClick: ()=>setGuestsFor(row.reservationId),
|
|
244
|
-
children: 'Guests'
|
|
245
|
-
}) : row.source !== 'order' || row.status !== 'expected' ? null : row.rainCheck ? /*#__PURE__*/ _jsx(Button, {
|
|
246
|
-
size: "small",
|
|
247
|
-
disabled: !canManagePos,
|
|
248
|
-
onClick: ()=>void undoRainCheck(row),
|
|
249
|
-
children: 'Undo rain check'
|
|
250
|
-
}) : /*#__PURE__*/ _jsx(Button, {
|
|
251
|
-
size: "small",
|
|
252
|
-
disabled: !canManagePos || !todayKey,
|
|
253
|
-
onClick: ()=>setRainCheckRow(row),
|
|
254
|
-
children: 'Rain check'
|
|
255
|
-
})
|
|
256
261
|
}
|
|
257
262
|
], [
|
|
258
263
|
list,
|