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