@aglyn/plugins-commerce 1.0.0-beta.288 → 1.0.0-beta.289

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.
Files changed (87) hide show
  1. package/README.md +2 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/commerce-console-page.js +29 -2
  4. package/src/lib/components/commerce-console-page.js.map +1 -1
  5. package/src/lib/components/console/commerce-analytics-card.component.js +4 -2
  6. package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -1
  7. package/src/lib/components/console/host-orders-card.component.js +5 -1
  8. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  9. package/src/lib/components/console/host-quotes-card.component.d.ts +34 -0
  10. package/src/lib/components/console/host-quotes-card.component.js +247 -0
  11. package/src/lib/components/console/host-quotes-card.component.js.map +1 -0
  12. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +33 -28
  13. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -1
  14. package/src/lib/components/console/quote-editor-dialog.component.d.ts +45 -0
  15. package/src/lib/components/console/quote-editor-dialog.component.js +1266 -0
  16. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -0
  17. package/src/lib/components/console/quote-pay-link.component.d.ts +38 -0
  18. package/src/lib/components/console/quote-pay-link.component.js +213 -0
  19. package/src/lib/components/console/quote-pay-link.component.js.map +1 -0
  20. package/src/lib/components/console/quote-settings-card.component.d.ts +29 -0
  21. package/src/lib/components/console/quote-settings-card.component.js +121 -0
  22. package/src/lib/components/console/quote-settings-card.component.js.map +1 -0
  23. package/src/lib/components/console/tax-settings-card.component.js +4 -1
  24. package/src/lib/components/console/tax-settings-card.component.js.map +1 -1
  25. package/src/lib/components/quote-page-body.d.ts +33 -0
  26. package/src/lib/components/quote-page-body.js +696 -0
  27. package/src/lib/components/quote-page-body.js.map +1 -0
  28. package/src/lib/components/quote-page.d.ts +29 -0
  29. package/src/lib/components/quote-page.js +96 -0
  30. package/src/lib/components/quote-page.js.map +1 -0
  31. package/src/lib/components/storefront-payment-element.d.ts +7 -1
  32. package/src/lib/components/storefront-payment-element.js +8 -3
  33. package/src/lib/components/storefront-payment-element.js.map +1 -1
  34. package/src/lib/constants/orders-list-query.js +6 -1
  35. package/src/lib/constants/orders-list-query.js.map +1 -1
  36. package/src/lib/constants/quote-page.d.ts +30 -0
  37. package/src/lib/constants/quote-page.js +35 -0
  38. package/src/lib/constants/quote-page.js.map +1 -0
  39. package/src/lib/model/commerce-orders.d.ts +25 -2
  40. package/src/lib/model/commerce-orders.js +10 -1
  41. package/src/lib/model/commerce-orders.js.map +1 -1
  42. package/src/lib/model/commerce-quote-pay.d.ts +229 -0
  43. package/src/lib/model/commerce-quote-pay.js +393 -0
  44. package/src/lib/model/commerce-quote-pay.js.map +1 -0
  45. package/src/lib/model/commerce-quotes.d.ts +279 -0
  46. package/src/lib/model/commerce-quotes.js +522 -0
  47. package/src/lib/model/commerce-quotes.js.map +1 -0
  48. package/src/lib/model/index.d.ts +2 -0
  49. package/src/lib/model/index.js +2 -0
  50. package/src/lib/model/index.js.map +1 -1
  51. package/src/lib/model/plugin-permissions.d.ts +1 -1
  52. package/src/lib/model/plugin-permissions.js +20 -0
  53. package/src/lib/model/plugin-permissions.js.map +1 -1
  54. package/src/lib/server/api-v1/orders-and-products.js +6 -2
  55. package/src/lib/server/api-v1/orders-and-products.js.map +1 -1
  56. package/src/lib/server/billing-webhook.js +127 -52
  57. package/src/lib/server/billing-webhook.js.map +1 -1
  58. package/src/lib/server/checkout-session-expiry.d.ts +36 -0
  59. package/src/lib/server/checkout-session-expiry.js +63 -0
  60. package/src/lib/server/checkout-session-expiry.js.map +1 -0
  61. package/src/lib/server/draft-order.js +16 -8
  62. package/src/lib/server/draft-order.js.map +1 -1
  63. package/src/lib/server/person-eraser.d.ts +7 -0
  64. package/src/lib/server/person-eraser.js +24 -2
  65. package/src/lib/server/person-eraser.js.map +1 -1
  66. package/src/lib/server/pos-return.js +3 -1
  67. package/src/lib/server/pos-return.js.map +1 -1
  68. package/src/lib/server/quote-link-token.d.ts +39 -0
  69. package/src/lib/server/quote-link-token.js +97 -0
  70. package/src/lib/server/quote-link-token.js.map +1 -0
  71. package/src/lib/server/quote-pay.d.ts +83 -0
  72. package/src/lib/server/quote-pay.js +606 -0
  73. package/src/lib/server/quote-pay.js.map +1 -0
  74. package/src/lib/server/quote.d.ts +49 -0
  75. package/src/lib/server/quote.js +513 -0
  76. package/src/lib/server/quote.js.map +1 -0
  77. package/src/lib/server/related.js +2 -0
  78. package/src/lib/server/related.js.map +1 -1
  79. package/src/lib/server/site-page-resolver.d.ts +2 -0
  80. package/src/lib/server/site-page-resolver.js +75 -0
  81. package/src/lib/server/site-page-resolver.js.map +1 -1
  82. package/src/lib/server.js +14 -0
  83. package/src/lib/server.js.map +1 -1
  84. package/src/lib/site.js +6 -0
  85. package/src/lib/site.js.map +1 -1
  86. package/src/lib/transfer/tracking.server.js +6 -0
  87. package/src/lib/transfer/tracking.server.js.map +1 -1
@@ -0,0 +1,1266 @@
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 { BlockedControl } from "@aglyn/shared-ui-jsx/components/blocked-control.component";
23
+ import { PrerequisiteNotice } from "@aglyn/shared-ui-jsx/components/prerequisite-notice.component";
24
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
25
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
26
+ import { useFirestore, useFirestoreDoc, useStoreSettingsHref, useUser } from "@aglyn/tenant-feature-instance";
27
+ import { Alert, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, FormControlLabel, List, ListItemButton, ListItemText, MenuItem, Paper, Stack, Switch, TextField, Typography } from "@mui/material";
28
+ import { doc } from "firebase/firestore";
29
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
30
+ import { QuotePayLinkSection } from "./quote-pay-link.component.js";
31
+ const REFUSAL = commerceSitePermissionRefusal('commerce.quotes');
32
+ const SEARCH_DEBOUNCE_MS = 250;
33
+ const money = (cents)=>posMoney(cents);
34
+ /** Field widths on a wide screen, from the theme's spacing; full width on a phone. */ const narrowField = (theme)=>({
35
+ maxWidth: {
36
+ sm: theme.spacing(16)
37
+ }
38
+ });
39
+ const mediumField = (theme)=>({
40
+ minWidth: {
41
+ sm: theme.spacing(20)
42
+ },
43
+ maxWidth: {
44
+ sm: theme.spacing(24)
45
+ }
46
+ });
47
+ /** Dollars typed into a field, as whole cents, or `null` when it is not an amount. */ function centsOf(text) {
48
+ const trimmed = text.trim();
49
+ if (!trimmed) return 0;
50
+ if (!/^\d+(\.\d{0,2})?$/.test(trimmed)) return null;
51
+ return Math.round(Number(trimmed) * 100);
52
+ }
53
+ const dollarsOf = (cents)=>cents ? (cents / 100).toFixed(2).replace(/\.00$/, '') : '';
54
+ function addressForm(address) {
55
+ var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6;
56
+ return {
57
+ name: (_ref = address == null ? void 0 : address.name) != null ? _ref : '',
58
+ line1: (_ref1 = address == null ? void 0 : address.line1) != null ? _ref1 : '',
59
+ line2: (_ref2 = address == null ? void 0 : address.line2) != null ? _ref2 : '',
60
+ city: (_ref3 = address == null ? void 0 : address.city) != null ? _ref3 : '',
61
+ state: (_ref4 = address == null ? void 0 : address.state) != null ? _ref4 : '',
62
+ postalCode: (_ref5 = address == null ? void 0 : address.postalCode) != null ? _ref5 : '',
63
+ country: (_ref6 = address == null ? void 0 : address.country) != null ? _ref6 : ''
64
+ };
65
+ }
66
+ let lineSequence = 0;
67
+ const nextLineKey = ()=>`line-${++lineSequence}`;
68
+ function formFromQuote(quote, requireProofApproval) {
69
+ var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9;
70
+ var _section_customer, _section_customer1, _section_customer2, _section_customer3, _section_customer4, _section_shipping;
71
+ const section = quote == null ? void 0 : quote.quote;
72
+ const discount = (_ref = section == null ? void 0 : section.discount) != null ? _ref : null;
73
+ return {
74
+ lines: ((_ref1 = quote == null ? void 0 : quote.lineItems) != null ? _ref1 : []).map((line)=>{
75
+ var _request_description, _request_sku, _request_variantId, _request_modifiers, _request_description1;
76
+ const request = CommerceModel.quoteLineRequestFromLine(line);
77
+ return request.kind === 'custom' ? {
78
+ key: nextLineKey(),
79
+ kind: 'custom',
80
+ name: request.name,
81
+ description: (_request_description = request.description) != null ? _request_description : '',
82
+ sku: (_request_sku = request.sku) != null ? _request_sku : '',
83
+ price: dollarsOf(request.unitAmountCents),
84
+ quantity: String(request.quantity),
85
+ ships: request.ships !== false,
86
+ included: request.included === true
87
+ } : {
88
+ key: nextLineKey(),
89
+ kind: 'catalog',
90
+ productId: request.productId,
91
+ variantId: (_request_variantId = request.variantId) != null ? _request_variantId : '',
92
+ quantity: String(request.quantity),
93
+ modifiers: (_request_modifiers = request.modifiers) != null ? _request_modifiers : [],
94
+ description: (_request_description1 = request.description) != null ? _request_description1 : '',
95
+ included: request.included === true
96
+ };
97
+ }),
98
+ customer: {
99
+ record: (_ref2 = section == null ? void 0 : (_section_customer = section.customer) == null ? void 0 : _section_customer.record) != null ? _ref2 : null,
100
+ name: (_ref3 = section == null ? void 0 : (_section_customer1 = section.customer) == null ? void 0 : _section_customer1.name) != null ? _ref3 : '',
101
+ company: (_ref4 = section == null ? void 0 : (_section_customer2 = section.customer) == null ? void 0 : _section_customer2.company) != null ? _ref4 : '',
102
+ email: (_ref5 = section == null ? void 0 : (_section_customer3 = section.customer) == null ? void 0 : _section_customer3.email) != null ? _ref5 : '',
103
+ phone: (_ref6 = section == null ? void 0 : (_section_customer4 = section.customer) == null ? void 0 : _section_customer4.phone) != null ? _ref6 : ''
104
+ },
105
+ shipTo: addressForm(section == null ? void 0 : section.shipTo),
106
+ billTo: addressForm(section == null ? void 0 : section.billTo),
107
+ discountKind: (_ref7 = discount == null ? void 0 : discount.kind) != null ? _ref7 : 'none',
108
+ discountValue: (discount == null ? void 0 : discount.kind) === 'amount' ? dollarsOf(discount.amountCents) : (discount == null ? void 0 : discount.kind) === 'percent' ? String(discount.pct) : '',
109
+ shipping: dollarsOf(section == null ? void 0 : (_section_shipping = section.shipping) == null ? void 0 : _section_shipping.amountCents),
110
+ notes: (_ref8 = section == null ? void 0 : section.notes) != null ? _ref8 : '',
111
+ terms: (_ref9 = section == null ? void 0 : section.terms) != null ? _ref9 : '',
112
+ requireProofApproval: section ? section.requireProofApproval === true : requireProofApproval
113
+ };
114
+ }
115
+ function lineRequest(line) {
116
+ var _centsOf;
117
+ const quantity = Number(line.quantity);
118
+ if (line.kind === 'catalog') {
119
+ return _extends({
120
+ kind: 'catalog',
121
+ productId: line.productId
122
+ }, line.variantId ? {
123
+ variantId: line.variantId
124
+ } : {}, {
125
+ quantity
126
+ }, line.modifiers.length ? {
127
+ modifiers: line.modifiers
128
+ } : {}, line.included ? {
129
+ included: true
130
+ } : {}, line.description.trim() ? {
131
+ description: line.description.trim()
132
+ } : {});
133
+ }
134
+ return _extends({
135
+ kind: 'custom',
136
+ name: line.name.trim()
137
+ }, line.description.trim() ? {
138
+ description: line.description.trim()
139
+ } : {}, line.sku.trim() ? {
140
+ sku: line.sku.trim()
141
+ } : {}, {
142
+ unitAmountCents: line.included ? 0 : (_centsOf = centsOf(line.price)) != null ? _centsOf : Number.NaN,
143
+ quantity,
144
+ ships: line.ships
145
+ }, line.included ? {
146
+ included: true
147
+ } : {});
148
+ }
149
+ function addressBody(address) {
150
+ const entries = Object.entries(address).filter(([, value])=>value.trim());
151
+ return entries.length ? Object.fromEntries(entries.map(([key, value])=>[
152
+ key,
153
+ value.trim()
154
+ ])) : null;
155
+ }
156
+ /** What the server is sent: choices, never what a product costs. */ function quoteBody(form) {
157
+ var _centsOf;
158
+ const discountCents = centsOf(form.discountValue);
159
+ return {
160
+ lines: form.lines.map(lineRequest),
161
+ customer: _extends({}, form.customer.record ? {
162
+ record: form.customer.record
163
+ } : {}, {
164
+ name: form.customer.name,
165
+ company: form.customer.company,
166
+ email: form.customer.email,
167
+ phone: form.customer.phone
168
+ }),
169
+ shipTo: addressBody(form.shipTo),
170
+ billTo: addressBody(form.billTo),
171
+ discount: form.discountKind === 'amount' ? {
172
+ kind: 'amount',
173
+ amountCents: discountCents != null ? discountCents : Number.NaN
174
+ } : form.discountKind === 'percent' ? {
175
+ kind: 'percent',
176
+ pct: Number(form.discountValue || 0)
177
+ } : null,
178
+ shipping: form.shipping.trim() ? {
179
+ amountCents: (_centsOf = centsOf(form.shipping)) != null ? _centsOf : Number.NaN
180
+ } : null,
181
+ notes: form.notes,
182
+ terms: form.terms,
183
+ requireProofApproval: form.requireProofApproval
184
+ };
185
+ }
186
+ /**
187
+ * The quote editor (AGL-4093): who it is for, its lines (products with their
188
+ * options and add-ons, typed items, things included at no charge), discount,
189
+ * shipping, notes and terms, with the totals worked out as it is edited. The
190
+ * server prices and saves it; the totals here are the same function's answer
191
+ * over the same inputs, and a connected tax service's figure arrives on save.
192
+ */ export function QuoteEditorDialog(props) {
193
+ var _centsOf, _ref, _ref1;
194
+ var _quote_quote_tax, _quote_quote, _quote_quote1, _quote_quote2;
195
+ const { hostId, quote, products, canEdit, canSend, onClose } = props;
196
+ const firestore = useFirestore();
197
+ const { data: user } = useUser();
198
+ const { enqueueSnackbar } = useSnackbar();
199
+ const taxesHref = useStoreSettingsHref(hostId, 'taxes');
200
+ const { data: store } = useFirestoreDoc(()=>doc(firestore, 'hosts', hostId, 'settings', 'store'), [
201
+ firestore,
202
+ hostId
203
+ ]);
204
+ const storeDefault = CommerceModel.quoteRequiresProofApprovalByDefault(store == null ? void 0 : store.quotes);
205
+ const [form, setForm] = useState(()=>formFromQuote(quote, storeDefault));
206
+ // The version this form was opened on. A change saved elsewhere while it is
207
+ // open arrives on `quote`; the save still names this one, so the server
208
+ // refuses rather than overwrite that change.
209
+ const [baseUpdatedAtMs] = useState(()=>{
210
+ var _quote_quote;
211
+ return quote == null ? void 0 : (_quote_quote = quote.quote) == null ? void 0 : _quote_quote.updatedAtMs;
212
+ });
213
+ const [busy, setBusy] = useState(false);
214
+ const [confirmingDelete, setConfirmingDelete] = useState(false);
215
+ /** Whether anything has been changed since the dialog opened. */ const [edited, setEdited] = useState(false);
216
+ /** Whether the proof-approval switch was set by hand. */ const [approvalSet, setApprovalSet] = useState(false);
217
+ // A new quote starts with the store's default, which may arrive after the
218
+ // dialog opens; a switch set by hand keeps its setting.
219
+ const requireProofApproval = quote || approvalSet ? form.requireProofApproval : storeDefault;
220
+ const update = useCallback((patch)=>{
221
+ setEdited(true);
222
+ if ('requireProofApproval' in patch) setApprovalSet(true);
223
+ setForm((prev)=>_extends({}, prev, patch));
224
+ }, []);
225
+ const updateLine = useCallback((key, patch)=>{
226
+ setEdited(true);
227
+ setForm((prev)=>_extends({}, prev, {
228
+ lines: prev.lines.map((line)=>line.key === key ? _extends({}, line, patch) : line)
229
+ }));
230
+ }, []);
231
+ /*
232
+ * One attempt per change (AGL-1697's rule): a retried Save presents the same
233
+ * key and is answered from the first, an edited quote is a new attempt.
234
+ */ const attemptKey = useRef('');
235
+ useEffect(()=>{
236
+ attemptKey.current = '';
237
+ }, [
238
+ form
239
+ ]);
240
+ const productsById = useMemo(()=>new Map(products.map((product)=>[
241
+ String(product.$id),
242
+ product
243
+ ])), [
244
+ products
245
+ ]);
246
+ /** Each line priced as the server will price it, or why it cannot be. */ const priced = useMemo(()=>form.lines.map((line)=>{
247
+ const request = lineRequest(line);
248
+ if (request.kind === 'custom') {
249
+ if (!request.name) return {
250
+ error: 'Name the item.'
251
+ };
252
+ if (!Number.isInteger(request.unitAmountCents)) return {
253
+ error: 'Enter a price.'
254
+ };
255
+ if (!(request.quantity >= 1)) return {
256
+ error: 'Enter a quantity.'
257
+ };
258
+ return {
259
+ line: CommerceModel.priceQuoteCustomLine(request)
260
+ };
261
+ }
262
+ if (!request.productId) return {
263
+ error: 'Choose a product.'
264
+ };
265
+ if (!(request.quantity >= 1)) return {
266
+ error: 'Enter a quantity.'
267
+ };
268
+ const answer = CommerceModel.priceQuoteCatalogLine(productsById.get(request.productId), request);
269
+ return 'error' in answer ? {
270
+ error: answer.error
271
+ } : {
272
+ line: answer.line
273
+ };
274
+ }), [
275
+ form.lines,
276
+ productsById
277
+ ]);
278
+ const discountCents = centsOf(form.discountValue);
279
+ const discount = form.discountKind === 'amount' && discountCents ? {
280
+ kind: 'amount',
281
+ amountCents: discountCents
282
+ } : form.discountKind === 'percent' && Number(form.discountValue) > 0 ? {
283
+ kind: 'percent',
284
+ pct: Math.min(100, Number(form.discountValue))
285
+ } : null;
286
+ const shippingCents = (_centsOf = centsOf(form.shipping)) != null ? _centsOf : 0;
287
+ const taxBasis = CommerceModel.quoteTaxBasisFromSettings(store == null ? void 0 : store.tax, {
288
+ country: form.shipTo.country.trim().toUpperCase(),
289
+ state: form.shipTo.state.trim()
290
+ });
291
+ const pricedLines = priced.flatMap((entry)=>'line' in entry && entry.line ? [
292
+ entry.line
293
+ ] : []);
294
+ const totals = CommerceModel.computeQuoteTotals({
295
+ lines: pricedLines,
296
+ discount,
297
+ shippingCents,
298
+ tax: taxBasis
299
+ });
300
+ const problems = priced.some((entry)=>'error' in entry);
301
+ const savedService = (quote == null ? void 0 : (_quote_quote = quote.quote) == null ? void 0 : (_quote_quote_tax = _quote_quote.tax) == null ? void 0 : _quote_quote_tax.kind) === 'service' ? quote.quote.tax : null;
302
+ const save = useCallback(async ()=>{
303
+ setBusy(true);
304
+ try {
305
+ var _payload_number;
306
+ if (!attemptKey.current) {
307
+ var _ref;
308
+ var _globalThis_crypto_randomUUID, _globalThis_crypto;
309
+ attemptKey.current = (_ref = (_globalThis_crypto = globalThis.crypto) == null ? void 0 : (_globalThis_crypto_randomUUID = _globalThis_crypto.randomUUID) == null ? void 0 : _globalThis_crypto_randomUUID.call(_globalThis_crypto)) != null ? _ref : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
310
+ }
311
+ const response = await authorizedFetch(user, '/api/commerce/quote', {
312
+ method: 'POST',
313
+ headers: {
314
+ 'Content-Type': 'application/json',
315
+ 'Idempotency-Key': attemptKey.current
316
+ },
317
+ body: JSON.stringify(_extends({
318
+ action: quote ? 'update' : 'create',
319
+ hostId
320
+ }, quote ? {
321
+ quoteId: quote.$id,
322
+ baseUpdatedAtMs
323
+ } : {}, {
324
+ quote: quoteBody(_extends({}, form, {
325
+ requireProofApproval
326
+ }))
327
+ }))
328
+ });
329
+ const payload = await response.json().catch(()=>({}));
330
+ if (!response.ok) {
331
+ var _ref1;
332
+ return void enqueueSnackbar((_ref1 = payload == null ? void 0 : payload.error) != null ? _ref1 : 'The quote could not be saved', {
333
+ variant: 'error',
334
+ allowDuplicate: true
335
+ });
336
+ }
337
+ enqueueSnackbar(`${CommerceModel.formatQuoteNumber({
338
+ number: (_payload_number = payload.number) != null ? _payload_number : undefined
339
+ }, payload.quoteId)} saved`, {
340
+ variant: 'success',
341
+ persist: false
342
+ });
343
+ onClose();
344
+ } finally{
345
+ setBusy(false);
346
+ }
347
+ }, [
348
+ user,
349
+ quote,
350
+ hostId,
351
+ baseUpdatedAtMs,
352
+ form,
353
+ requireProofApproval,
354
+ enqueueSnackbar,
355
+ onClose
356
+ ]);
357
+ const remove = useCallback(async ()=>{
358
+ if (!quote) return;
359
+ setBusy(true);
360
+ try {
361
+ const response = await authorizedFetch(user, '/api/commerce/quote', {
362
+ method: 'POST',
363
+ headers: {
364
+ 'Content-Type': 'application/json'
365
+ },
366
+ body: JSON.stringify({
367
+ action: 'delete',
368
+ hostId,
369
+ quoteId: quote.$id
370
+ })
371
+ });
372
+ const payload = await response.json().catch(()=>({}));
373
+ if (!response.ok) {
374
+ var _ref;
375
+ return void enqueueSnackbar((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'The quote could not be deleted', {
376
+ variant: 'error',
377
+ allowDuplicate: true
378
+ });
379
+ }
380
+ enqueueSnackbar(`${CommerceModel.formatQuoteNumber(quote, quote.$id)} deleted`, {
381
+ variant: 'success',
382
+ persist: false
383
+ });
384
+ setConfirmingDelete(false);
385
+ onClose();
386
+ } finally{
387
+ setBusy(false);
388
+ }
389
+ }, [
390
+ user,
391
+ quote,
392
+ hostId,
393
+ enqueueSnackbar,
394
+ onClose
395
+ ]);
396
+ const readOnly = !canEdit || busy;
397
+ const paid = quote ? !CommerceModel.isUnpaidQuote(quote) : false;
398
+ const title = quote ? CommerceModel.formatQuoteNumber(quote, quote.$id) : 'New quote';
399
+ const status = quote ? CommerceModel.quoteDisplayStatus(quote) : null;
400
+ const deletable = Boolean(quote) && !paid && ((_ref = quote == null ? void 0 : (_quote_quote1 = quote.quote) == null ? void 0 : _quote_quote1.stage) != null ? _ref : 'draft') === 'draft';
401
+ return /*#__PURE__*/ _jsxs(Dialog, {
402
+ open: true,
403
+ onClose: busy ? undefined : onClose,
404
+ maxWidth: "md",
405
+ fullWidth: true,
406
+ scroll: "paper",
407
+ "aria-labelledby": "quote-editor-title",
408
+ children: [
409
+ /*#__PURE__*/ _jsx(DialogTitle, {
410
+ id: "quote-editor-title",
411
+ children: /*#__PURE__*/ _jsxs(Stack, {
412
+ direction: "row",
413
+ spacing: 1,
414
+ sx: {
415
+ alignItems: 'center'
416
+ },
417
+ children: [
418
+ /*#__PURE__*/ _jsx("span", {
419
+ children: title
420
+ }),
421
+ status ? /*#__PURE__*/ _jsx(Chip, {
422
+ size: "small",
423
+ variant: "outlined",
424
+ label: status.label,
425
+ color: CommerceModel.QUOTE_STATUS_COLOR[status.key]
426
+ }) : null
427
+ ]
428
+ })
429
+ }),
430
+ /*#__PURE__*/ _jsx(DialogContent, {
431
+ dividers: true,
432
+ children: /*#__PURE__*/ _jsxs(Stack, {
433
+ spacing: 3,
434
+ children: [
435
+ paid ? /*#__PURE__*/ _jsx(Alert, {
436
+ severity: "success",
437
+ children: (quote == null ? void 0 : (_quote_quote2 = quote.quote) == null ? void 0 : _quote_quote2.paidAtMs) ? `Paid ${new Date(quote.quote.paidAtMs).toLocaleDateString()}. It is an order now and can no longer be changed.` : 'This quote has been paid, so it is an order now and can no longer be changed.'
438
+ }) : null,
439
+ quote ? /*#__PURE__*/ _jsx(QuotePayLinkSection, {
440
+ hostId: hostId,
441
+ quoteId: quote.$id,
442
+ quote: quote,
443
+ canSend: canSend,
444
+ edited: edited
445
+ }) : null,
446
+ taxBasis.kind === 'none' && taxBasis.reason === 'undecided' ? /*#__PURE__*/ _jsx(PrerequisiteNotice, {
447
+ href: taxesHref != null ? taxesHref : undefined,
448
+ actionLabel: "Open Taxes",
449
+ children: 'This store has not chosen how it handles sales tax, so this quote shows no tax. Choose it in Taxes.'
450
+ }) : null,
451
+ /*#__PURE__*/ _jsx(QuoteCustomerSection, {
452
+ hostId: hostId,
453
+ form: form,
454
+ disabled: readOnly || paid,
455
+ onChange: (customer)=>update({
456
+ customer
457
+ })
458
+ }),
459
+ /*#__PURE__*/ _jsxs(Stack, {
460
+ spacing: 1.5,
461
+ children: [
462
+ /*#__PURE__*/ _jsx(Typography, {
463
+ variant: "subtitle2",
464
+ children: 'Items'
465
+ }),
466
+ form.lines.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
467
+ variant: "body2",
468
+ color: "text.secondary",
469
+ children: 'Add products from your catalog, or type an item that is made to order.'
470
+ }) : null,
471
+ form.lines.map((line, index)=>/*#__PURE__*/ _jsx(QuoteLineEditor, {
472
+ line: line,
473
+ priced: priced[index],
474
+ products: products,
475
+ disabled: readOnly || paid,
476
+ onChange: (patch)=>updateLine(line.key, patch),
477
+ onRemove: ()=>update({
478
+ lines: form.lines.filter((entry)=>entry.key !== line.key)
479
+ })
480
+ }, line.key)),
481
+ /*#__PURE__*/ _jsxs(Stack, {
482
+ direction: "row",
483
+ spacing: 1,
484
+ useFlexGap: true,
485
+ sx: {
486
+ flexWrap: 'wrap'
487
+ },
488
+ children: [
489
+ /*#__PURE__*/ _jsx(Button, {
490
+ size: "small",
491
+ disabled: readOnly || paid || products.length === 0,
492
+ onClick: ()=>update({
493
+ lines: [
494
+ ...form.lines,
495
+ {
496
+ key: nextLineKey(),
497
+ kind: 'catalog',
498
+ productId: '',
499
+ variantId: '',
500
+ quantity: '1',
501
+ modifiers: [],
502
+ description: '',
503
+ included: false
504
+ }
505
+ ]
506
+ }),
507
+ children: 'Add product'
508
+ }),
509
+ /*#__PURE__*/ _jsx(Button, {
510
+ size: "small",
511
+ disabled: readOnly || paid,
512
+ onClick: ()=>update({
513
+ lines: [
514
+ ...form.lines,
515
+ {
516
+ key: nextLineKey(),
517
+ kind: 'custom',
518
+ name: '',
519
+ description: '',
520
+ sku: '',
521
+ price: '',
522
+ quantity: '1',
523
+ ships: true,
524
+ included: false
525
+ }
526
+ ]
527
+ }),
528
+ children: 'Add custom item'
529
+ })
530
+ ]
531
+ })
532
+ ]
533
+ }),
534
+ /*#__PURE__*/ _jsxs(Stack, {
535
+ direction: {
536
+ xs: 'column',
537
+ sm: 'row'
538
+ },
539
+ spacing: 2,
540
+ children: [
541
+ /*#__PURE__*/ _jsxs(TextField, {
542
+ label: "Discount",
543
+ select: true,
544
+ size: "small",
545
+ value: form.discountKind,
546
+ disabled: readOnly || paid,
547
+ onChange: (event)=>update({
548
+ discountKind: event.target.value
549
+ }),
550
+ sx: mediumField,
551
+ children: [
552
+ /*#__PURE__*/ _jsx(MenuItem, {
553
+ value: "none",
554
+ children: 'None'
555
+ }),
556
+ /*#__PURE__*/ _jsx(MenuItem, {
557
+ value: "amount",
558
+ children: 'Amount off'
559
+ }),
560
+ /*#__PURE__*/ _jsx(MenuItem, {
561
+ value: "percent",
562
+ children: 'Percent off'
563
+ })
564
+ ]
565
+ }),
566
+ form.discountKind !== 'none' ? /*#__PURE__*/ _jsx(TextField, {
567
+ label: form.discountKind === 'amount' ? 'Amount ($)' : 'Percent (%)',
568
+ size: "small",
569
+ value: form.discountValue,
570
+ disabled: readOnly || paid,
571
+ onChange: (event)=>update({
572
+ discountValue: event.target.value
573
+ }),
574
+ slotProps: {
575
+ htmlInput: {
576
+ inputMode: 'decimal'
577
+ }
578
+ },
579
+ error: form.discountKind === 'amount' ? centsOf(form.discountValue) === null : !(Number(form.discountValue) >= 0 && Number(form.discountValue) <= 100)
580
+ }) : null,
581
+ /*#__PURE__*/ _jsx(TextField, {
582
+ label: "Shipping ($)",
583
+ size: "small",
584
+ value: form.shipping,
585
+ disabled: readOnly || paid,
586
+ onChange: (event)=>update({
587
+ shipping: event.target.value
588
+ }),
589
+ slotProps: {
590
+ htmlInput: {
591
+ inputMode: 'decimal'
592
+ }
593
+ },
594
+ error: centsOf(form.shipping) === null,
595
+ helperText: "Leave empty when nothing ships."
596
+ })
597
+ ]
598
+ }),
599
+ /*#__PURE__*/ _jsx(QuoteAddressFields, {
600
+ label: "Ship to",
601
+ value: form.shipTo,
602
+ disabled: readOnly || paid,
603
+ helperText: "Where the order goes. Store tax rates use its state and country.",
604
+ onChange: (shipTo)=>update({
605
+ shipTo
606
+ })
607
+ }),
608
+ /*#__PURE__*/ _jsx(QuoteAddressFields, {
609
+ label: "Bill to",
610
+ value: form.billTo,
611
+ disabled: readOnly || paid,
612
+ onChange: (billTo)=>update({
613
+ billTo
614
+ })
615
+ }),
616
+ /*#__PURE__*/ _jsxs(Stack, {
617
+ spacing: 2,
618
+ children: [
619
+ /*#__PURE__*/ _jsx(TextField, {
620
+ label: "Notes for the customer",
621
+ multiline: true,
622
+ minRows: 2,
623
+ value: form.notes,
624
+ disabled: readOnly || paid,
625
+ onChange: (event)=>update({
626
+ notes: event.target.value
627
+ })
628
+ }),
629
+ /*#__PURE__*/ _jsx(TextField, {
630
+ label: "Terms",
631
+ multiline: true,
632
+ minRows: 2,
633
+ value: form.terms,
634
+ disabled: readOnly || paid,
635
+ onChange: (event)=>update({
636
+ terms: event.target.value
637
+ })
638
+ }),
639
+ /*#__PURE__*/ _jsx(FormControlLabel, {
640
+ control: /*#__PURE__*/ _jsx(Switch, {
641
+ checked: requireProofApproval,
642
+ disabled: readOnly || paid,
643
+ onChange: (event)=>update({
644
+ requireProofApproval: event.target.checked
645
+ })
646
+ }),
647
+ label: "Customers approve the proof before they can pay"
648
+ })
649
+ ]
650
+ }),
651
+ /*#__PURE__*/ _jsx(Divider, {}),
652
+ /*#__PURE__*/ _jsx(QuoteTotalsSummary, {
653
+ totals: totals,
654
+ tax: taxBasis,
655
+ savedService: edited ? null : savedService,
656
+ savedTotals: edited ? null : (_ref1 = quote == null ? void 0 : quote.totals) != null ? _ref1 : null
657
+ })
658
+ ]
659
+ })
660
+ }),
661
+ /*#__PURE__*/ _jsxs(DialogActions, {
662
+ sx: {
663
+ flexWrap: 'wrap',
664
+ rowGap: 1
665
+ },
666
+ children: [
667
+ deletable ? /*#__PURE__*/ _jsx(BlockedControl, {
668
+ blocked: !canEdit,
669
+ reason: REFUSAL,
670
+ children: /*#__PURE__*/ _jsx(Button, {
671
+ color: "error",
672
+ disabled: busy,
673
+ onClick: ()=>setConfirmingDelete(true),
674
+ sx: {
675
+ mr: 'auto'
676
+ },
677
+ children: 'Delete'
678
+ })
679
+ }) : null,
680
+ /*#__PURE__*/ _jsx(Button, {
681
+ onClick: onClose,
682
+ disabled: busy,
683
+ children: paid || !canEdit ? 'Close' : 'Cancel'
684
+ }),
685
+ paid ? null : /*#__PURE__*/ _jsx(BlockedControl, {
686
+ blocked: !canEdit,
687
+ reason: REFUSAL,
688
+ children: /*#__PURE__*/ _jsx(Button, {
689
+ variant: "contained",
690
+ disabled: busy || form.lines.length === 0 || problems,
691
+ onClick: ()=>void save(),
692
+ children: quote ? 'Save' : 'Create quote'
693
+ })
694
+ })
695
+ ]
696
+ }),
697
+ /*#__PURE__*/ _jsxs(Dialog, {
698
+ open: confirmingDelete,
699
+ onClose: ()=>setConfirmingDelete(false),
700
+ maxWidth: "xs",
701
+ fullWidth: true,
702
+ children: [
703
+ /*#__PURE__*/ _jsx(DialogTitle, {
704
+ children: `Delete ${title}?`
705
+ }),
706
+ /*#__PURE__*/ _jsx(DialogContent, {
707
+ children: /*#__PURE__*/ _jsx(DialogContentText, {
708
+ children: 'The quote and everything on it is removed. This can’t be undone.'
709
+ })
710
+ }),
711
+ /*#__PURE__*/ _jsxs(DialogActions, {
712
+ children: [
713
+ /*#__PURE__*/ _jsx(Button, {
714
+ onClick: ()=>setConfirmingDelete(false),
715
+ disabled: busy,
716
+ children: 'Cancel'
717
+ }),
718
+ /*#__PURE__*/ _jsx(Button, {
719
+ color: "error",
720
+ variant: "contained",
721
+ disabled: busy,
722
+ onClick: ()=>void remove(),
723
+ children: 'Delete'
724
+ })
725
+ ]
726
+ })
727
+ ]
728
+ })
729
+ ]
730
+ });
731
+ }
732
+ QuoteEditorDialog.displayName = 'QuoteEditorDialog';
733
+ /** Who the quote is for: a person the workspace keeps, or details typed here. */ function QuoteCustomerSection(props) {
734
+ const { hostId, form, disabled, onChange } = props;
735
+ const { data: user } = useUser();
736
+ const [text, setText] = useState('');
737
+ const [found, setFound] = useState([]);
738
+ const [available, setAvailable] = useState(true);
739
+ const ticket = useRef(0);
740
+ const customer = form.customer;
741
+ useEffect(()=>{
742
+ const typed = text.trim();
743
+ if (typed.length < 2 || !user) {
744
+ // Only when something is listed, so an effect that re-runs settles.
745
+ setFound((prev)=>prev.length ? [] : prev);
746
+ return;
747
+ }
748
+ const mine = ++ticket.current;
749
+ const timer = window.setTimeout(()=>{
750
+ void authorizedFetch(user, '/api/commerce/quote', {
751
+ method: 'POST',
752
+ headers: {
753
+ 'Content-Type': 'application/json'
754
+ },
755
+ body: JSON.stringify({
756
+ action: 'search-customers',
757
+ hostId,
758
+ text: typed
759
+ })
760
+ }).then((response)=>response.json().catch(()=>({}))).then((payload)=>{
761
+ if (mine !== ticket.current) return;
762
+ setAvailable((payload == null ? void 0 : payload.available) !== false);
763
+ setFound(Array.isArray(payload == null ? void 0 : payload.customers) ? payload.customers : []);
764
+ }).catch(()=>undefined);
765
+ }, SEARCH_DEBOUNCE_MS);
766
+ return ()=>window.clearTimeout(timer);
767
+ }, [
768
+ text,
769
+ user,
770
+ hostId
771
+ ]);
772
+ const pick = (person)=>{
773
+ var _person_email, _person_phone;
774
+ onChange({
775
+ record: person.record,
776
+ name: person.name,
777
+ company: person.company,
778
+ email: (_person_email = person.email) != null ? _person_email : '',
779
+ phone: (_person_phone = person.phone) != null ? _person_phone : ''
780
+ });
781
+ setText('');
782
+ setFound([]);
783
+ };
784
+ return /*#__PURE__*/ _jsxs(Stack, {
785
+ spacing: 1.5,
786
+ children: [
787
+ /*#__PURE__*/ _jsx(Typography, {
788
+ variant: "subtitle2",
789
+ children: 'Customer'
790
+ }),
791
+ customer.record ? /*#__PURE__*/ _jsxs(Stack, {
792
+ direction: "row",
793
+ spacing: 1,
794
+ sx: {
795
+ alignItems: 'center'
796
+ },
797
+ children: [
798
+ /*#__PURE__*/ _jsx(Chip, {
799
+ color: "primary",
800
+ variant: "outlined",
801
+ label: customer.name || customer.email || 'Contact',
802
+ onDelete: disabled ? undefined : ()=>onChange(_extends({}, customer, {
803
+ record: null
804
+ }))
805
+ }),
806
+ /*#__PURE__*/ _jsx(Typography, {
807
+ variant: "caption",
808
+ color: "text.secondary",
809
+ children: 'From your contacts'
810
+ })
811
+ ]
812
+ }) : available ? /*#__PURE__*/ _jsxs(Box, {
813
+ children: [
814
+ /*#__PURE__*/ _jsx(TextField, {
815
+ size: "small",
816
+ fullWidth: true,
817
+ label: "Find a contact",
818
+ placeholder: "Name, email or phone",
819
+ value: text,
820
+ disabled: disabled,
821
+ onChange: (event)=>setText(event.target.value)
822
+ }),
823
+ found.length ? /*#__PURE__*/ _jsx(Paper, {
824
+ variant: "outlined",
825
+ sx: {
826
+ mt: 0.5
827
+ },
828
+ children: /*#__PURE__*/ _jsx(List, {
829
+ dense: true,
830
+ disablePadding: true,
831
+ children: found.map((person)=>/*#__PURE__*/ _jsx(ListItemButton, {
832
+ onClick: ()=>pick(person),
833
+ children: /*#__PURE__*/ _jsx(ListItemText, {
834
+ primary: person.name || person.email || 'Contact',
835
+ secondary: [
836
+ person.company,
837
+ person.email,
838
+ person.phone
839
+ ].filter(Boolean).join(' · ')
840
+ })
841
+ }, `${person.record.kind}:${person.record.id}`))
842
+ })
843
+ }) : null
844
+ ]
845
+ }) : null,
846
+ /*#__PURE__*/ _jsxs(Stack, {
847
+ direction: {
848
+ xs: 'column',
849
+ sm: 'row'
850
+ },
851
+ spacing: 2,
852
+ children: [
853
+ /*#__PURE__*/ _jsx(TextField, {
854
+ size: "small",
855
+ fullWidth: true,
856
+ label: "Name",
857
+ value: customer.name,
858
+ disabled: disabled,
859
+ onChange: (event)=>onChange(_extends({}, customer, {
860
+ name: event.target.value
861
+ }))
862
+ }),
863
+ /*#__PURE__*/ _jsx(TextField, {
864
+ size: "small",
865
+ fullWidth: true,
866
+ label: "Company",
867
+ value: customer.company,
868
+ disabled: disabled,
869
+ onChange: (event)=>onChange(_extends({}, customer, {
870
+ company: event.target.value
871
+ }))
872
+ })
873
+ ]
874
+ }),
875
+ /*#__PURE__*/ _jsxs(Stack, {
876
+ direction: {
877
+ xs: 'column',
878
+ sm: 'row'
879
+ },
880
+ spacing: 2,
881
+ children: [
882
+ /*#__PURE__*/ _jsx(TextField, {
883
+ size: "small",
884
+ fullWidth: true,
885
+ label: "Email",
886
+ type: "email",
887
+ value: customer.email,
888
+ disabled: disabled,
889
+ onChange: (event)=>onChange(_extends({}, customer, {
890
+ email: event.target.value
891
+ }))
892
+ }),
893
+ /*#__PURE__*/ _jsx(TextField, {
894
+ size: "small",
895
+ fullWidth: true,
896
+ label: "Phone",
897
+ value: customer.phone,
898
+ disabled: disabled,
899
+ onChange: (event)=>onChange(_extends({}, customer, {
900
+ phone: event.target.value
901
+ }))
902
+ })
903
+ ]
904
+ })
905
+ ]
906
+ });
907
+ }
908
+ /** One address on the quote. */ function QuoteAddressFields(props) {
909
+ const { label, value, disabled, helperText, onChange } = props;
910
+ const field = (key, fieldLabel, extra = {})=>/*#__PURE__*/ _jsx(TextField, _extends({
911
+ size: "small",
912
+ fullWidth: true,
913
+ label: fieldLabel,
914
+ value: value[key],
915
+ disabled: disabled,
916
+ onChange: (event)=>onChange(_extends({}, value, {
917
+ [key]: event.target.value
918
+ }))
919
+ }, extra));
920
+ return /*#__PURE__*/ _jsxs(Stack, {
921
+ spacing: 1.5,
922
+ children: [
923
+ /*#__PURE__*/ _jsx(Typography, {
924
+ variant: "subtitle2",
925
+ children: label
926
+ }),
927
+ helperText ? /*#__PURE__*/ _jsx(Typography, {
928
+ variant: "caption",
929
+ color: "text.secondary",
930
+ children: helperText
931
+ }) : null,
932
+ field('name', 'Name'),
933
+ field('line1', 'Address'),
934
+ field('line2', 'Apartment, suite, unit'),
935
+ /*#__PURE__*/ _jsxs(Stack, {
936
+ direction: {
937
+ xs: 'column',
938
+ sm: 'row'
939
+ },
940
+ spacing: 2,
941
+ children: [
942
+ field('city', 'City'),
943
+ field('state', 'State or region'),
944
+ field('postalCode', 'Postal code'),
945
+ field('country', 'Country code', {
946
+ placeholder: 'US',
947
+ slotProps: {
948
+ htmlInput: {
949
+ maxLength: 2
950
+ }
951
+ }
952
+ })
953
+ ]
954
+ })
955
+ ]
956
+ });
957
+ }
958
+ /** One line of the quote: a product with its options and add-ons, or a typed item. */ function QuoteLineEditor(props) {
959
+ var _lifted_variants_;
960
+ const { line, priced, products, disabled, onChange, onRemove } = props;
961
+ const product = line.kind === 'catalog' ? products.find((entry)=>String(entry.$id) === line.productId) : undefined;
962
+ const lifted = product ? CommerceModel.liftLegacyProduct(product) : null;
963
+ const groups = lifted ? CommerceModel.productModifierGroups(lifted) : [];
964
+ const lineCents = (priced == null ? void 0 : priced.line) ? CommerceModel.quoteLineCents(priced.line) : 0;
965
+ return /*#__PURE__*/ _jsx(Paper, {
966
+ variant: "outlined",
967
+ sx: {
968
+ p: 2
969
+ },
970
+ children: /*#__PURE__*/ _jsxs(Stack, {
971
+ spacing: 1.5,
972
+ children: [
973
+ /*#__PURE__*/ _jsxs(Stack, {
974
+ direction: "row",
975
+ spacing: 1,
976
+ sx: {
977
+ alignItems: 'flex-start'
978
+ },
979
+ children: [
980
+ /*#__PURE__*/ _jsxs(Stack, {
981
+ spacing: 1.5,
982
+ sx: {
983
+ flex: 1,
984
+ minWidth: 0
985
+ },
986
+ children: [
987
+ line.kind === 'catalog' ? /*#__PURE__*/ _jsxs(Stack, {
988
+ direction: {
989
+ xs: 'column',
990
+ sm: 'row'
991
+ },
992
+ spacing: 2,
993
+ children: [
994
+ /*#__PURE__*/ _jsx(TextField, {
995
+ label: "Product",
996
+ select: true,
997
+ size: "small",
998
+ fullWidth: true,
999
+ value: line.productId,
1000
+ disabled: disabled,
1001
+ onChange: (event)=>onChange({
1002
+ productId: event.target.value,
1003
+ variantId: '',
1004
+ modifiers: []
1005
+ }),
1006
+ children: products.map((entry)=>{
1007
+ var _entry_name;
1008
+ return /*#__PURE__*/ _jsx(MenuItem, {
1009
+ value: String(entry.$id),
1010
+ children: String((_entry_name = entry.name) != null ? _entry_name : entry.$id)
1011
+ }, String(entry.$id));
1012
+ })
1013
+ }),
1014
+ lifted && lifted.variants.length > 1 ? /*#__PURE__*/ _jsx(TextField, {
1015
+ label: "Option",
1016
+ select: true,
1017
+ size: "small",
1018
+ fullWidth: true,
1019
+ value: line.variantId || ((_lifted_variants_ = lifted.variants[0]) == null ? void 0 : _lifted_variants_.id) || '',
1020
+ disabled: disabled,
1021
+ onChange: (event)=>onChange({
1022
+ variantId: event.target.value
1023
+ }),
1024
+ children: lifted.variants.map((variant)=>{
1025
+ var _variant_options;
1026
+ return /*#__PURE__*/ _jsx(MenuItem, {
1027
+ value: variant.id,
1028
+ children: Object.values((_variant_options = variant.options) != null ? _variant_options : {}).join(' / ') || 'Default'
1029
+ }, variant.id);
1030
+ })
1031
+ }) : null
1032
+ ]
1033
+ }) : /*#__PURE__*/ _jsxs(Stack, {
1034
+ direction: {
1035
+ xs: 'column',
1036
+ sm: 'row'
1037
+ },
1038
+ spacing: 2,
1039
+ children: [
1040
+ /*#__PURE__*/ _jsx(TextField, {
1041
+ label: "Item",
1042
+ size: "small",
1043
+ fullWidth: true,
1044
+ value: line.name,
1045
+ disabled: disabled,
1046
+ onChange: (event)=>onChange({
1047
+ name: event.target.value
1048
+ })
1049
+ }),
1050
+ /*#__PURE__*/ _jsx(TextField, {
1051
+ label: "SKU (optional)",
1052
+ size: "small",
1053
+ value: line.sku,
1054
+ disabled: disabled,
1055
+ onChange: (event)=>onChange({
1056
+ sku: event.target.value
1057
+ })
1058
+ })
1059
+ ]
1060
+ }),
1061
+ groups.map((group)=>{
1062
+ var _chosen_;
1063
+ const chosen = line.kind === 'catalog' ? line.modifiers.filter((pick)=>pick.groupId === group.id).map((pick)=>pick.optionId) : [];
1064
+ return /*#__PURE__*/ _jsxs(TextField, {
1065
+ label: CommerceModel.modifierGroupRequired(group) ? group.name : `${group.name} (optional)`,
1066
+ select: true,
1067
+ size: "small",
1068
+ fullWidth: true,
1069
+ value: group.max > 1 ? chosen : (_chosen_ = chosen[0]) != null ? _chosen_ : '',
1070
+ disabled: disabled,
1071
+ slotProps: {
1072
+ select: {
1073
+ multiple: group.max > 1
1074
+ }
1075
+ },
1076
+ onChange: (event)=>{
1077
+ const raw = event.target.value;
1078
+ const picked = (Array.isArray(raw) ? raw : raw ? [
1079
+ raw
1080
+ ] : []).map(String);
1081
+ if (line.kind !== 'catalog') return;
1082
+ onChange({
1083
+ modifiers: [
1084
+ ...line.modifiers.filter((pick)=>pick.groupId !== group.id),
1085
+ ...picked.map((optionId)=>({
1086
+ groupId: group.id,
1087
+ optionId
1088
+ }))
1089
+ ]
1090
+ });
1091
+ },
1092
+ children: [
1093
+ group.max > 1 ? null : /*#__PURE__*/ _jsx(MenuItem, {
1094
+ value: "",
1095
+ children: 'None'
1096
+ }),
1097
+ group.options.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
1098
+ value: option.id,
1099
+ children: option.priceCents ? `${option.name} (+${money(option.priceCents)})` : option.name
1100
+ }, option.id))
1101
+ ]
1102
+ }, group.id);
1103
+ }),
1104
+ /*#__PURE__*/ _jsx(TextField, {
1105
+ label: "Description (optional)",
1106
+ size: "small",
1107
+ fullWidth: true,
1108
+ multiline: true,
1109
+ value: line.description,
1110
+ disabled: disabled,
1111
+ onChange: (event)=>onChange({
1112
+ description: event.target.value
1113
+ })
1114
+ }),
1115
+ /*#__PURE__*/ _jsxs(Stack, {
1116
+ direction: {
1117
+ xs: 'column',
1118
+ sm: 'row'
1119
+ },
1120
+ spacing: 2,
1121
+ sx: {
1122
+ alignItems: {
1123
+ sm: 'center'
1124
+ }
1125
+ },
1126
+ children: [
1127
+ /*#__PURE__*/ _jsx(TextField, {
1128
+ label: "Quantity",
1129
+ size: "small",
1130
+ value: line.quantity,
1131
+ disabled: disabled,
1132
+ onChange: (event)=>onChange({
1133
+ quantity: event.target.value.replace(/[^0-9]/g, '')
1134
+ }),
1135
+ slotProps: {
1136
+ htmlInput: {
1137
+ inputMode: 'numeric'
1138
+ }
1139
+ },
1140
+ sx: narrowField
1141
+ }),
1142
+ line.kind === 'custom' ? /*#__PURE__*/ _jsx(TextField, {
1143
+ label: "Unit price ($)",
1144
+ size: "small",
1145
+ value: line.included ? '' : line.price,
1146
+ disabled: disabled || line.included,
1147
+ onChange: (event)=>onChange({
1148
+ price: event.target.value
1149
+ }),
1150
+ slotProps: {
1151
+ htmlInput: {
1152
+ inputMode: 'decimal'
1153
+ }
1154
+ },
1155
+ error: !line.included && centsOf(line.price) === null,
1156
+ sx: mediumField
1157
+ }) : null,
1158
+ line.kind === 'custom' ? /*#__PURE__*/ _jsx(FormControlLabel, {
1159
+ control: /*#__PURE__*/ _jsx(Switch, {
1160
+ checked: line.ships,
1161
+ disabled: disabled,
1162
+ onChange: (event)=>onChange({
1163
+ ships: event.target.checked
1164
+ })
1165
+ }),
1166
+ label: "Ships"
1167
+ }) : null,
1168
+ /*#__PURE__*/ _jsx(FormControlLabel, {
1169
+ control: /*#__PURE__*/ _jsx(Switch, {
1170
+ checked: line.included,
1171
+ disabled: disabled,
1172
+ onChange: (event)=>onChange({
1173
+ included: event.target.checked
1174
+ })
1175
+ }),
1176
+ label: "Included at no charge"
1177
+ })
1178
+ ]
1179
+ })
1180
+ ]
1181
+ }),
1182
+ /*#__PURE__*/ _jsxs(Stack, {
1183
+ sx: {
1184
+ alignItems: 'flex-end',
1185
+ flexShrink: 0
1186
+ },
1187
+ children: [
1188
+ /*#__PURE__*/ _jsx(Button, {
1189
+ size: "small",
1190
+ color: "inherit",
1191
+ disabled: disabled,
1192
+ onClick: onRemove,
1193
+ children: 'Remove'
1194
+ }),
1195
+ /*#__PURE__*/ _jsx(Typography, {
1196
+ variant: "body2",
1197
+ sx: {
1198
+ fontWeight: 'fontWeightMedium'
1199
+ },
1200
+ children: line.included ? 'Included' : money(lineCents)
1201
+ }),
1202
+ (priced == null ? void 0 : priced.line) && !line.included && priced.line.quantity > 1 ? /*#__PURE__*/ _jsx(Typography, {
1203
+ variant: "caption",
1204
+ color: "text.secondary",
1205
+ children: `${money(priced.line.unitAmountCents)} each`
1206
+ }) : null
1207
+ ]
1208
+ })
1209
+ ]
1210
+ }),
1211
+ (priced == null ? void 0 : priced.error) ? /*#__PURE__*/ _jsx(Typography, {
1212
+ variant: "caption",
1213
+ color: "error",
1214
+ children: priced.error
1215
+ }) : null
1216
+ ]
1217
+ })
1218
+ });
1219
+ }
1220
+ /** The quote's totals, as the customer will read them. */ function QuoteTotalsSummary(props) {
1221
+ const { totals, tax, savedService, savedTotals } = props;
1222
+ const shown = savedService && savedTotals ? _extends({}, totals, savedTotals) : totals;
1223
+ const taxLabel = CommerceModel.describeQuoteTax(savedService != null ? savedService : tax);
1224
+ const row = (label, value, strong = false)=>/*#__PURE__*/ _jsxs(Stack, {
1225
+ direction: "row",
1226
+ sx: {
1227
+ justifyContent: 'space-between'
1228
+ },
1229
+ children: [
1230
+ /*#__PURE__*/ _jsx(Typography, {
1231
+ variant: strong ? 'subtitle1' : 'body2',
1232
+ color: strong ? 'text.primary' : 'text.secondary',
1233
+ children: label
1234
+ }),
1235
+ /*#__PURE__*/ _jsx(Typography, {
1236
+ variant: strong ? 'subtitle1' : 'body2',
1237
+ children: value
1238
+ })
1239
+ ]
1240
+ });
1241
+ return /*#__PURE__*/ _jsxs(Stack, {
1242
+ spacing: 0.5,
1243
+ sx: (theme)=>({
1244
+ alignSelf: {
1245
+ sm: 'flex-end'
1246
+ },
1247
+ minWidth: {
1248
+ sm: theme.spacing(40)
1249
+ }
1250
+ }),
1251
+ children: [
1252
+ row('Items', money(shown.itemsCents)),
1253
+ shown.discountCents ? row('Discount', `−${money(shown.discountCents)}`) : null,
1254
+ shown.shippingCents ? row('Shipping', money(shown.shippingCents)) : null,
1255
+ tax.kind === 'at-payment' && !savedService ? /*#__PURE__*/ _jsx(Typography, {
1256
+ variant: "body2",
1257
+ color: "text.secondary",
1258
+ children: taxLabel
1259
+ }) : row(taxLabel, money(shown.taxCents)),
1260
+ row(totals.taxAtPayment && !savedService ? 'Total before tax' : 'Total', money(shown.totalCents), true)
1261
+ ]
1262
+ });
1263
+ }
1264
+ export default QuoteEditorDialog;
1265
+
1266
+ //# sourceMappingURL=quote-editor-dialog.component.js.map