@aglyn/plugins-commerce 1.0.0-beta.290 → 1.0.0-beta.292

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 (183) hide show
  1. package/README.md +4 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/cart-fulfillment.d.ts +3 -0
  4. package/src/lib/components/cart-fulfillment.js.map +1 -1
  5. package/src/lib/components/cart.js +14 -4
  6. package/src/lib/components/cart.js.map +1 -1
  7. package/src/lib/components/commerce-console-page.js +2 -1
  8. package/src/lib/components/commerce-console-page.js.map +1 -1
  9. package/src/lib/components/console/host-orders-card.component.d.ts +6 -0
  10. package/src/lib/components/console/host-orders-card.component.js +3 -2
  11. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  12. package/src/lib/components/console/invoice-send.component.d.ts +44 -0
  13. package/src/lib/components/console/invoice-send.component.js +237 -0
  14. package/src/lib/components/console/invoice-send.component.js.map +1 -0
  15. package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
  16. package/src/lib/components/console/order-detail-dialog.component.js +9 -2
  17. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  18. package/src/lib/components/console/order-invoice.component.d.ts +9 -0
  19. package/src/lib/components/console/order-invoice.component.js +27 -2
  20. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  21. package/src/lib/components/console/pos/pos-item-dialog.component.js +3 -2
  22. package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
  23. package/src/lib/components/console/pos-kiosk-page.component.js +2 -1
  24. package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -1
  25. package/src/lib/components/console/pos-page.component.js +1 -1
  26. package/src/lib/components/console/pos-page.component.js.map +1 -1
  27. package/src/lib/components/console/product-add-ons-fields.component.d.ts +41 -0
  28. package/src/lib/components/console/product-add-ons-fields.component.js +356 -0
  29. package/src/lib/components/console/product-add-ons-fields.component.js.map +1 -0
  30. package/src/lib/components/console/product-editor-dialog.component.js +31 -5
  31. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  32. package/src/lib/components/console/product-register-fields.component.d.ts +4 -5
  33. package/src/lib/components/console/product-register-fields.component.js +8 -197
  34. package/src/lib/components/console/product-register-fields.component.js.map +1 -1
  35. package/src/lib/components/console/products-hub-zone.component.js +6 -0
  36. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  37. package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
  38. package/src/lib/components/console/quote-editor-dialog.component.js +102 -4
  39. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
  40. package/src/lib/components/console/quote-pay-link.component.d.ts +8 -1
  41. package/src/lib/components/console/quote-pay-link.component.js +29 -3
  42. package/src/lib/components/console/quote-pay-link.component.js.map +1 -1
  43. package/src/lib/components/console/quote-proofs.component.d.ts +11 -0
  44. package/src/lib/components/console/quote-proofs.component.js +68 -0
  45. package/src/lib/components/console/quote-proofs.component.js.map +1 -1
  46. package/src/lib/components/console/quote-send.component.d.ts +42 -0
  47. package/src/lib/components/console/quote-send.component.js +304 -0
  48. package/src/lib/components/console/quote-send.component.js.map +1 -0
  49. package/src/lib/components/console/quote-start.component.d.ts +45 -0
  50. package/src/lib/components/console/quote-start.component.js +182 -0
  51. package/src/lib/components/console/quote-start.component.js.map +1 -0
  52. package/src/lib/components/product-add-ons.d.ts +28 -0
  53. package/src/lib/components/product-add-ons.js +136 -0
  54. package/src/lib/components/product-add-ons.js.map +1 -0
  55. package/src/lib/components/product-detail.d.ts +16 -0
  56. package/src/lib/components/product-detail.js +112 -19
  57. package/src/lib/components/product-detail.js.map +1 -1
  58. package/src/lib/components/product-grid.d.ts +8 -2
  59. package/src/lib/components/product-grid.js +6 -1
  60. package/src/lib/components/product-grid.js.map +1 -1
  61. package/src/lib/components/product-options-field-body.d.ts +44 -0
  62. package/src/lib/components/product-options-field-body.js +260 -0
  63. package/src/lib/components/product-options-field-body.js.map +1 -0
  64. package/src/lib/components/product-options-field.d.ts +32 -0
  65. package/src/lib/components/product-options-field.js +128 -0
  66. package/src/lib/components/product-options-field.js.map +1 -0
  67. package/src/lib/components/quote-page-body.js +39 -20
  68. package/src/lib/components/quote-page-body.js.map +1 -1
  69. package/src/lib/model/commerce-cart.d.ts +15 -3
  70. package/src/lib/model/commerce-cart.js +10 -3
  71. package/src/lib/model/commerce-cart.js.map +1 -1
  72. package/src/lib/model/commerce-invoices.d.ts +139 -0
  73. package/src/lib/model/commerce-invoices.js +343 -0
  74. package/src/lib/model/commerce-invoices.js.map +1 -0
  75. package/src/lib/model/commerce-orders.d.ts +35 -5
  76. package/src/lib/model/commerce-orders.js +3 -0
  77. package/src/lib/model/commerce-orders.js.map +1 -1
  78. package/src/lib/model/commerce-pos-kiosk.d.ts +1 -1
  79. package/src/lib/model/commerce-pos-kiosk.js +3 -2
  80. package/src/lib/model/commerce-pos-kiosk.js.map +1 -1
  81. package/src/lib/model/commerce-product-field.d.ts +23 -0
  82. package/src/lib/model/commerce-product-field.js +26 -0
  83. package/src/lib/model/commerce-product-field.js.map +1 -0
  84. package/src/lib/model/commerce-quote-email.d.ts +81 -0
  85. package/src/lib/model/commerce-quote-email.js +170 -0
  86. package/src/lib/model/commerce-quote-email.js.map +1 -0
  87. package/src/lib/model/commerce-quote-pay.d.ts +49 -6
  88. package/src/lib/model/commerce-quote-pay.js +69 -36
  89. package/src/lib/model/commerce-quote-pay.js.map +1 -1
  90. package/src/lib/model/commerce-quote-request.d.ts +93 -0
  91. package/src/lib/model/commerce-quote-request.js +207 -0
  92. package/src/lib/model/commerce-quote-request.js.map +1 -0
  93. package/src/lib/model/commerce-quotes.d.ts +81 -0
  94. package/src/lib/model/commerce-quotes.js +1 -1
  95. package/src/lib/model/commerce-quotes.js.map +1 -1
  96. package/src/lib/model/commerce.d.ts +18 -0
  97. package/src/lib/model/commerce.js +26 -0
  98. package/src/lib/model/commerce.js.map +1 -1
  99. package/src/lib/model/index.d.ts +3 -0
  100. package/src/lib/model/index.js +3 -0
  101. package/src/lib/model/index.js.map +1 -1
  102. package/src/lib/model/product-modifiers.d.ts +74 -4
  103. package/src/lib/model/product-modifiers.js +131 -10
  104. package/src/lib/model/product-modifiers.js.map +1 -1
  105. package/src/lib/plugin.js +19 -0
  106. package/src/lib/plugin.js.map +1 -1
  107. package/src/lib/server/billing-webhook.js +164 -98
  108. package/src/lib/server/billing-webhook.js.map +1 -1
  109. package/src/lib/server/cart-checkout.js +14 -2
  110. package/src/lib/server/cart-checkout.js.map +1 -1
  111. package/src/lib/server/cart.d.ts +5 -0
  112. package/src/lib/server/cart.js +41 -4
  113. package/src/lib/server/cart.js.map +1 -1
  114. package/src/lib/server/catalog.d.ts +6 -0
  115. package/src/lib/server/catalog.js +2 -0
  116. package/src/lib/server/catalog.js.map +1 -1
  117. package/src/lib/server/checkout-extras.js +3 -1
  118. package/src/lib/server/checkout-extras.js.map +1 -1
  119. package/src/lib/server/checkout.js +28 -3
  120. package/src/lib/server/checkout.js.map +1 -1
  121. package/src/lib/server/commerce-pdf.d.ts +5 -1
  122. package/src/lib/server/commerce-pdf.js +37 -10
  123. package/src/lib/server/commerce-pdf.js.map +1 -1
  124. package/src/lib/server/draft-order.js +23 -3
  125. package/src/lib/server/draft-order.js.map +1 -1
  126. package/src/lib/server/invoice-email.d.ts +76 -0
  127. package/src/lib/server/invoice-email.js +183 -0
  128. package/src/lib/server/invoice-email.js.map +1 -0
  129. package/src/lib/server/local-fulfillment.js +5 -1
  130. package/src/lib/server/local-fulfillment.js.map +1 -1
  131. package/src/lib/server/order-invoice-send.d.ts +43 -0
  132. package/src/lib/server/order-invoice-send.js +218 -0
  133. package/src/lib/server/order-invoice-send.js.map +1 -0
  134. package/src/lib/server/order-status.d.ts +11 -0
  135. package/src/lib/server/order-status.js +44 -8
  136. package/src/lib/server/order-status.js.map +1 -1
  137. package/src/lib/server/person-eraser.d.ts +3 -2
  138. package/src/lib/server/person-eraser.js +25 -6
  139. package/src/lib/server/person-eraser.js.map +1 -1
  140. package/src/lib/server/pos-offline-sync.js +1 -1
  141. package/src/lib/server/pos-offline-sync.js.map +1 -1
  142. package/src/lib/server/pos-order.js +1 -1
  143. package/src/lib/server/pos-order.js.map +1 -1
  144. package/src/lib/server/product.d.ts +26 -0
  145. package/src/lib/server/product.js +18 -0
  146. package/src/lib/server/product.js.map +1 -1
  147. package/src/lib/server/products-write.js +5 -2
  148. package/src/lib/server/products-write.js.map +1 -1
  149. package/src/lib/server/quote-email.d.ts +85 -0
  150. package/src/lib/server/quote-email.js +211 -0
  151. package/src/lib/server/quote-email.js.map +1 -0
  152. package/src/lib/server/quote-link-token.d.ts +11 -0
  153. package/src/lib/server/quote-link-token.js +21 -0
  154. package/src/lib/server/quote-link-token.js.map +1 -1
  155. package/src/lib/server/quote-pay.d.ts +8 -3
  156. package/src/lib/server/quote-pay.js +105 -43
  157. package/src/lib/server/quote-pay.js.map +1 -1
  158. package/src/lib/server/quote.d.ts +11 -0
  159. package/src/lib/server/quote.js +309 -12
  160. package/src/lib/server/quote.js.map +1 -1
  161. package/src/lib/server/site-page-resolver.js +2 -1
  162. package/src/lib/server/site-page-resolver.js.map +1 -1
  163. package/src/lib/server.js +5 -0
  164. package/src/lib/server.js.map +1 -1
  165. package/src/lib/site.js +8 -0
  166. package/src/lib/site.js.map +1 -1
  167. package/src/lib/tenant-emails.js +243 -0
  168. package/src/lib/tenant-emails.js.map +1 -1
  169. package/src/lib/utils/add-to-cart.d.ts +8 -0
  170. package/src/lib/utils/add-to-cart.js +10 -2
  171. package/src/lib/utils/add-to-cart.js.map +1 -1
  172. package/src/lib/utils/commerce-document-pdf.d.ts +5 -0
  173. package/src/lib/utils/commerce-document-pdf.js +6 -1
  174. package/src/lib/utils/commerce-document-pdf.js.map +1 -1
  175. package/src/lib/utils/invoice-email-content.d.ts +45 -0
  176. package/src/lib/utils/invoice-email-content.js +126 -0
  177. package/src/lib/utils/invoice-email-content.js.map +1 -0
  178. package/src/lib/utils/order-invoice.d.ts +1 -1
  179. package/src/lib/utils/order-invoice.js +14 -6
  180. package/src/lib/utils/order-invoice.js.map +1 -1
  181. package/src/lib/utils/quote-email-content.d.ts +63 -0
  182. package/src/lib/utils/quote-email-content.js +157 -0
  183. package/src/lib/utils/quote-email-content.js.map +1 -0
@@ -0,0 +1,304 @@
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
+ import * as CommerceModel from "../../model/index.js";
19
+ import { commerceSitePermissionRefusal } from "../../model/plugin-permissions.js";
20
+ import { siteContactEmail, resolveHostToken } from "@aglyn/aglyn/app-utils/host-tokens";
21
+ import { BlockedControl } from "@aglyn/shared-ui-jsx/components/blocked-control.component";
22
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
23
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
24
+ import { useFirestore, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
25
+ import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField, Typography } from "@mui/material";
26
+ import { doc } from "firebase/firestore";
27
+ import { useCallback, useEffect, useRef, useState } from "react";
28
+ import { InvoiceSendDialog, invoiceSentLine } from "./invoice-send.component.js";
29
+ const SEND_REFUSAL = commerceSitePermissionRefusal('commerce.quotes.send');
30
+ /** `Oct 29, 2026, 3:04 PM`. */ const sentAt = (atMs)=>new Date(atMs).toLocaleString('en-US', {
31
+ dateStyle: 'medium',
32
+ timeStyle: 'short'
33
+ });
34
+ /**
35
+ * Emailing the quote to the customer (AGL-4097): when it was last sent and to
36
+ * whom, and Send quote, Send again or Send revision N, which opens a short
37
+ * form (To, Subject, Message). The server writes the email in the site's
38
+ * look with the proof, the PDF and the pay button, in the customer's
39
+ * conversation with the business.
40
+ *
41
+ * Send invoice (AGL-4099) sits beside it: for an unpaid quote, an invoice for
42
+ * a customer who needs one to pay (with the same pay link); for a paid one,
43
+ * the invoice as their record. Both reply into the quote's conversation.
44
+ */ export function QuoteSendSection(props) {
45
+ var _resolveHostToken, _ref;
46
+ const { hostId, quoteId, quote, canSend, edited } = props;
47
+ const firestore = useFirestore();
48
+ const { data: user } = useUser();
49
+ const { enqueueSnackbar } = useSnackbar();
50
+ const { data: host } = useFirestoreDoc(()=>doc(firestore, 'hosts', hostId), [
51
+ firestore,
52
+ hostId
53
+ ]);
54
+ const businessName = String((_resolveHostToken = resolveHostToken('businessName', host != null ? host : {})) != null ? _resolveHostToken : '').trim();
55
+ const section = quote.quote;
56
+ const unpaid = CommerceModel.isUnpaidQuote(quote);
57
+ const last = CommerceModel.lastQuoteSend(section);
58
+ const revision = Number((_ref = section == null ? void 0 : section.revision) != null ? _ref : 1);
59
+ const label = CommerceModel.quoteSendLabel(section);
60
+ const replacesLink = CommerceModel.quoteSendReplacesLink(section);
61
+ const approvalFirst = CommerceModel.quotePaymentBlockedByApproval(section);
62
+ const proofCount = CommerceModel.currentQuoteProofs(section).length;
63
+ const listed = siteContactEmail(host);
64
+ const contact = listed && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(listed) ? listed : null;
65
+ const [open, setOpen] = useState(false);
66
+ const [invoicing, setInvoicing] = useState(false);
67
+ const invoiceLine = invoiceSentLine(quote);
68
+ const closed = quote.status === 'cancelled' || quote.status === 'refunded';
69
+ const [busy, setBusy] = useState(false);
70
+ const [to, setTo] = useState('');
71
+ const [subject, setSubject] = useState('');
72
+ const [message, setMessage] = useState('');
73
+ /*
74
+ * One attempt per form (AGL-1697's rule): a retried Send presents the same
75
+ * key and is answered from the first, so the customer never gets two; a
76
+ * changed form is a new attempt.
77
+ */ const attemptKey = useRef('');
78
+ useEffect(()=>{
79
+ attemptKey.current = '';
80
+ }, [
81
+ to,
82
+ subject,
83
+ message
84
+ ]);
85
+ const openForm = useCallback(()=>{
86
+ setTo(CommerceModel.quoteSendPrefill(quote));
87
+ setSubject(CommerceModel.quoteEmailDefaultSubject(quote, businessName, quoteId));
88
+ setMessage('');
89
+ setOpen(true);
90
+ }, [
91
+ quote,
92
+ businessName,
93
+ quoteId
94
+ ]);
95
+ const send = useCallback(async ()=>{
96
+ setBusy(true);
97
+ try {
98
+ var _payload_to;
99
+ if (!attemptKey.current) {
100
+ var _ref;
101
+ var _globalThis_crypto_randomUUID, _globalThis_crypto;
102
+ 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)}`;
103
+ }
104
+ const response = await authorizedFetch(user, '/api/commerce/quote', {
105
+ method: 'POST',
106
+ headers: {
107
+ 'Content-Type': 'application/json',
108
+ 'Idempotency-Key': attemptKey.current
109
+ },
110
+ body: JSON.stringify({
111
+ action: 'send',
112
+ hostId,
113
+ quoteId,
114
+ to,
115
+ subject,
116
+ message
117
+ })
118
+ });
119
+ const payload = await response.json().catch(()=>({}));
120
+ if (!response.ok) {
121
+ var _ref1;
122
+ return void enqueueSnackbar((_ref1 = payload == null ? void 0 : payload.error) != null ? _ref1 : 'The quote could not be sent', {
123
+ variant: 'error',
124
+ allowDuplicate: true
125
+ });
126
+ }
127
+ enqueueSnackbar(`${CommerceModel.formatQuoteNumber(quote, quoteId)} sent to ${String((_payload_to = payload.to) != null ? _payload_to : to)}` + (payload.linkReplaced ? '. The link in your earlier email no longer opens the quote.' : ''), {
128
+ variant: 'success',
129
+ persist: false
130
+ });
131
+ setOpen(false);
132
+ } finally{
133
+ setBusy(false);
134
+ }
135
+ }, [
136
+ user,
137
+ hostId,
138
+ quoteId,
139
+ to,
140
+ subject,
141
+ message,
142
+ quote,
143
+ enqueueSnackbar
144
+ ]);
145
+ const status = last ? `Sent ${sentAt(Number(last.atMs))} to ${last.to || 'the customer'}` + (Number(last.revision) < revision ? ` (revision ${last.revision})` : '') + '.' : 'Not sent yet. Email it with your message, the proof and the PDF, or copy the pay link below and send it yourself.';
146
+ return /*#__PURE__*/ _jsxs(Stack, {
147
+ spacing: 1.5,
148
+ children: [
149
+ /*#__PURE__*/ _jsxs(Stack, {
150
+ spacing: 0.5,
151
+ children: [
152
+ /*#__PURE__*/ _jsx(Typography, {
153
+ variant: "subtitle2",
154
+ children: 'Email to customer'
155
+ }),
156
+ /*#__PURE__*/ _jsx(Typography, {
157
+ variant: "body2",
158
+ color: "text.secondary",
159
+ children: unpaid ? status : last ? status : 'This quote was paid without being emailed.'
160
+ }),
161
+ unpaid && replacesLink ? /*#__PURE__*/ _jsx(Typography, {
162
+ variant: "body2",
163
+ color: "text.secondary",
164
+ children: `Sending revision ${revision} gives your customer a new pay link. The link in your earlier email will say the quote was updated.`
165
+ }) : null,
166
+ unpaid && edited ? /*#__PURE__*/ _jsx(Typography, {
167
+ variant: "body2",
168
+ color: "text.secondary",
169
+ children: 'Save your changes first: the email shows the quote as it was last saved.'
170
+ }) : null,
171
+ invoiceLine ? /*#__PURE__*/ _jsx(Typography, {
172
+ variant: "body2",
173
+ color: "text.secondary",
174
+ children: invoiceLine
175
+ }) : null
176
+ ]
177
+ }),
178
+ !closed ? /*#__PURE__*/ _jsxs(Stack, {
179
+ direction: "row",
180
+ spacing: 1,
181
+ children: [
182
+ unpaid ? /*#__PURE__*/ _jsx(BlockedControl, {
183
+ blocked: !canSend,
184
+ reason: SEND_REFUSAL,
185
+ children: /*#__PURE__*/ _jsx(Button, {
186
+ variant: "contained",
187
+ size: "small",
188
+ disabled: busy || edited,
189
+ onClick: openForm,
190
+ children: label
191
+ })
192
+ }) : null,
193
+ /*#__PURE__*/ _jsx(BlockedControl, {
194
+ blocked: !canSend,
195
+ reason: SEND_REFUSAL,
196
+ children: /*#__PURE__*/ _jsx(Button, {
197
+ variant: unpaid ? 'outlined' : 'contained',
198
+ size: "small",
199
+ disabled: busy || unpaid && edited,
200
+ onClick: ()=>setInvoicing(true),
201
+ children: 'Send invoice'
202
+ })
203
+ })
204
+ ]
205
+ }) : null,
206
+ /*#__PURE__*/ _jsx(InvoiceSendDialog, {
207
+ open: invoicing,
208
+ onClose: ()=>setInvoicing(false),
209
+ hostId: hostId,
210
+ orderId: quoteId,
211
+ order: quote
212
+ }),
213
+ /*#__PURE__*/ _jsxs(Dialog, {
214
+ open: open,
215
+ onClose: busy ? undefined : ()=>setOpen(false),
216
+ maxWidth: "sm",
217
+ fullWidth: true,
218
+ "aria-labelledby": "quote-send-title",
219
+ children: [
220
+ /*#__PURE__*/ _jsx(DialogTitle, {
221
+ id: "quote-send-title",
222
+ children: `${label}: ${CommerceModel.formatQuoteNumber(quote, quoteId)}`
223
+ }),
224
+ /*#__PURE__*/ _jsx(DialogContent, {
225
+ children: /*#__PURE__*/ _jsxs(Stack, {
226
+ spacing: 2,
227
+ sx: {
228
+ pt: 1
229
+ },
230
+ children: [
231
+ /*#__PURE__*/ _jsx(TextField, {
232
+ label: "To",
233
+ type: "email",
234
+ value: to,
235
+ onChange: (event)=>setTo(event.target.value),
236
+ required: true,
237
+ fullWidth: true,
238
+ disabled: busy
239
+ }),
240
+ /*#__PURE__*/ _jsx(TextField, {
241
+ label: "Subject",
242
+ value: subject,
243
+ onChange: (event)=>setSubject(event.target.value),
244
+ fullWidth: true,
245
+ disabled: busy,
246
+ slotProps: {
247
+ htmlInput: {
248
+ maxLength: CommerceModel.QUOTE_MAX_SEND_SUBJECT
249
+ }
250
+ },
251
+ helperText: (section == null ? void 0 : section.emailThread) ? 'Keep the subject the same to keep these emails in one conversation in your customer’s inbox.' : undefined
252
+ }),
253
+ /*#__PURE__*/ _jsx(TextField, {
254
+ label: "Message",
255
+ value: message,
256
+ onChange: (event)=>setMessage(event.target.value),
257
+ multiline: true,
258
+ minRows: 4,
259
+ fullWidth: true,
260
+ disabled: busy,
261
+ slotProps: {
262
+ htmlInput: {
263
+ maxLength: CommerceModel.QUOTE_MAX_SEND_MESSAGE
264
+ }
265
+ },
266
+ helperText: "Shown at the top of the email, in your words."
267
+ }),
268
+ /*#__PURE__*/ _jsx(Typography, {
269
+ variant: "body2",
270
+ color: "text.secondary",
271
+ children: `The email shows the quote number, revision ${revision} and total` + (proofCount ? ', the proof' : '') + `, with the quote attached as a PDF and a button to ${approvalFirst ? 'approve the proof' : 'pay'} on your site. ` + `Replies go to ${contact || 'your email'}.`
272
+ }),
273
+ replacesLink ? /*#__PURE__*/ _jsx(Typography, {
274
+ variant: "body2",
275
+ color: "text.secondary",
276
+ children: 'This sends a new pay link. The link in your earlier email will say the quote was updated.'
277
+ }) : null
278
+ ]
279
+ })
280
+ }),
281
+ /*#__PURE__*/ _jsxs(DialogActions, {
282
+ children: [
283
+ /*#__PURE__*/ _jsx(Button, {
284
+ onClick: ()=>setOpen(false),
285
+ disabled: busy,
286
+ children: 'Cancel'
287
+ }),
288
+ /*#__PURE__*/ _jsx(Button, {
289
+ variant: "contained",
290
+ disabled: busy || !to.trim(),
291
+ onClick: ()=>void send(),
292
+ children: busy ? 'Sending…' : 'Send'
293
+ })
294
+ ]
295
+ })
296
+ ]
297
+ })
298
+ ]
299
+ });
300
+ }
301
+ QuoteSendSection.displayName = 'QuoteSendSection';
302
+ export default QuoteSendSection;
303
+
304
+ //# sourceMappingURL=quote-send.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/quote-send.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport * as CommerceModel from '../../model'\nimport { commerceSitePermissionRefusal } from '../../model/plugin-permissions'\nimport { siteContactEmail, resolveHostToken, type HostTokenSource } from '@aglyn/aglyn/app-utils/host-tokens'\nimport { BlockedControl } from '@aglyn/shared-ui-jsx/components/blocked-control.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useFirestore, useFirestoreDoc, useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { InvoiceSendDialog, invoiceSentLine } from './invoice-send.component'\n\nconst SEND_REFUSAL = commerceSitePermissionRefusal('commerce.quotes.send')\n\nexport interface QuoteSendSectionProps {\n hostId: string\n quoteId: string\n quote: CommerceModel.HostOrder\n /** Holds \"Send quotes and invoices to customers\" (`commerce.quotes.send`). */\n canSend: boolean\n /** The editor holds changes not saved yet, which the email would not show. */\n edited: boolean\n}\n\n/** `Oct 29, 2026, 3:04 PM`. */\nconst sentAt = (atMs: number) =>\n new Date(atMs).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })\n\n/**\n * Emailing the quote to the customer (AGL-4097): when it was last sent and to\n * whom, and Send quote, Send again or Send revision N, which opens a short\n * form (To, Subject, Message). The server writes the email in the site's\n * look with the proof, the PDF and the pay button, in the customer's\n * conversation with the business.\n *\n * Send invoice (AGL-4099) sits beside it: for an unpaid quote, an invoice for\n * a customer who needs one to pay (with the same pay link); for a paid one,\n * the invoice as their record. Both reply into the quote's conversation.\n */\nexport function QuoteSendSection(props: QuoteSendSectionProps) {\n const { hostId, quoteId, quote, canSend, edited } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { data: host } = useFirestoreDoc<Record<string, unknown>>(\n () => doc(firestore, 'hosts', hostId),\n [firestore, hostId],\n )\n const businessName = String(resolveHostToken('businessName', (host ?? {}) as HostTokenSource) ?? '').trim()\n const section = quote.quote\n const unpaid = CommerceModel.isUnpaidQuote(quote)\n const last = CommerceModel.lastQuoteSend(section)\n const revision = Number(section?.revision ?? 1)\n const label = CommerceModel.quoteSendLabel(section)\n const replacesLink = CommerceModel.quoteSendReplacesLink(section)\n const approvalFirst = CommerceModel.quotePaymentBlockedByApproval(section)\n const proofCount = CommerceModel.currentQuoteProofs(section).length\n const listed = siteContactEmail(host as never)\n const contact = listed && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(listed) ? listed : null\n\n const [open, setOpen] = useState(false)\n const [invoicing, setInvoicing] = useState(false)\n const invoiceLine = invoiceSentLine(quote)\n const closed = quote.status === 'cancelled' || quote.status === 'refunded'\n const [busy, setBusy] = useState(false)\n const [to, setTo] = useState('')\n const [subject, setSubject] = useState('')\n const [message, setMessage] = useState('')\n\n /*\n * One attempt per form (AGL-1697's rule): a retried Send presents the same\n * key and is answered from the first, so the customer never gets two; a\n * changed form is a new attempt.\n */\n const attemptKey = useRef('')\n useEffect(() => {\n attemptKey.current = ''\n }, [to, subject, message])\n\n const openForm = useCallback(() => {\n setTo(CommerceModel.quoteSendPrefill(quote))\n setSubject(CommerceModel.quoteEmailDefaultSubject(quote, businessName, quoteId))\n setMessage('')\n setOpen(true)\n }, [quote, businessName, quoteId])\n\n const send = useCallback(async () => {\n setBusy(true)\n try {\n if (!attemptKey.current) {\n attemptKey.current = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`\n }\n const response = await authorizedFetch(user, '/api/commerce/quote', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Idempotency-Key': attemptKey.current },\n body: JSON.stringify({ action: 'send', hostId, quoteId, to, subject, message }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(payload?.error ?? 'The quote could not be sent', { variant: 'error', allowDuplicate: true })\n }\n enqueueSnackbar(\n `${CommerceModel.formatQuoteNumber(quote, quoteId)} sent to ${String(payload.to ?? to)}` +\n (payload.linkReplaced ? '. The link in your earlier email no longer opens the quote.' : ''),\n { variant: 'success', persist: false },\n )\n setOpen(false)\n } finally {\n setBusy(false)\n }\n }, [user, hostId, quoteId, to, subject, message, quote, enqueueSnackbar])\n\n const status = last\n ? `Sent ${sentAt(Number(last.atMs))} to ${last.to || 'the customer'}` +\n (Number(last.revision) < revision ? ` (revision ${last.revision})` : '') +\n '.'\n : 'Not sent yet. Email it with your message, the proof and the PDF, or copy the pay link below and send it yourself.'\n\n return (\n <Stack spacing={1.5}>\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Email to customer'}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {unpaid ? status : last ? status : 'This quote was paid without being emailed.'}\n </Typography>\n {unpaid && replacesLink ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`Sending revision ${revision} gives your customer a new pay link. The link in your earlier email will say the quote was updated.`}\n </Typography>\n ) : null}\n {unpaid && edited ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Save your changes first: the email shows the quote as it was last saved.'}\n </Typography>\n ) : null}\n {invoiceLine ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {invoiceLine}\n </Typography>\n ) : null}\n </Stack>\n {!closed ? (\n <Stack direction=\"row\" spacing={1}>\n {unpaid ? (\n <BlockedControl blocked={!canSend} reason={SEND_REFUSAL}>\n <Button variant=\"contained\" size=\"small\" disabled={busy || edited} onClick={openForm}>\n {label}\n </Button>\n </BlockedControl>\n ) : null}\n <BlockedControl blocked={!canSend} reason={SEND_REFUSAL}>\n <Button\n variant={unpaid ? 'outlined' : 'contained'}\n size=\"small\"\n disabled={busy || (unpaid && edited)}\n onClick={() => setInvoicing(true)}\n >\n {'Send invoice'}\n </Button>\n </BlockedControl>\n </Stack>\n ) : null}\n <InvoiceSendDialog\n open={invoicing}\n onClose={() => setInvoicing(false)}\n hostId={hostId}\n orderId={quoteId}\n order={quote}\n />\n <Dialog open={open} onClose={busy ? undefined : () => setOpen(false)} maxWidth=\"sm\" fullWidth aria-labelledby=\"quote-send-title\">\n <DialogTitle id=\"quote-send-title\">{`${label}: ${CommerceModel.formatQuoteNumber(quote, quoteId)}`}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n <TextField\n label=\"To\"\n type=\"email\"\n value={to}\n onChange={(event) => setTo(event.target.value)}\n required\n fullWidth\n disabled={busy}\n />\n <TextField\n label=\"Subject\"\n value={subject}\n onChange={(event) => setSubject(event.target.value)}\n fullWidth\n disabled={busy}\n slotProps={{ htmlInput: { maxLength: CommerceModel.QUOTE_MAX_SEND_SUBJECT } }}\n helperText={\n section?.emailThread\n ? 'Keep the subject the same to keep these emails in one conversation in your customer’s inbox.'\n : undefined\n }\n />\n <TextField\n label=\"Message\"\n value={message}\n onChange={(event) => setMessage(event.target.value)}\n multiline\n minRows={4}\n fullWidth\n disabled={busy}\n slotProps={{ htmlInput: { maxLength: CommerceModel.QUOTE_MAX_SEND_MESSAGE } }}\n helperText=\"Shown at the top of the email, in your words.\"\n />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`The email shows the quote number, revision ${revision} and total` +\n (proofCount ? ', the proof' : '') +\n `, with the quote attached as a PDF and a button to ${approvalFirst ? 'approve the proof' : 'pay'} on your site. ` +\n `Replies go to ${contact || 'your email'}.`}\n </Typography>\n {replacesLink ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This sends a new pay link. The link in your earlier email will say the quote was updated.'}\n </Typography>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setOpen(false)} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" disabled={busy || !to.trim()} onClick={() => void send()}>\n {busy ? 'Sending…' : 'Send'}\n </Button>\n </DialogActions>\n </Dialog>\n </Stack>\n )\n}\nQuoteSendSection.displayName = 'QuoteSendSection'\n\nexport default QuoteSendSection\n"],"names":["CommerceModel","commerceSitePermissionRefusal","siteContactEmail","resolveHostToken","BlockedControl","useSnackbar","authorizedFetch","useFirestore","useFirestoreDoc","useUser","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","doc","useCallback","useEffect","useRef","useState","InvoiceSendDialog","invoiceSentLine","SEND_REFUSAL","sentAt","atMs","Date","toLocaleString","dateStyle","timeStyle","QuoteSendSection","props","hostId","quoteId","quote","canSend","edited","firestore","data","user","enqueueSnackbar","host","businessName","String","trim","section","unpaid","isUnpaidQuote","last","lastQuoteSend","revision","Number","label","quoteSendLabel","replacesLink","quoteSendReplacesLink","approvalFirst","quotePaymentBlockedByApproval","proofCount","currentQuoteProofs","length","listed","contact","test","open","setOpen","invoicing","setInvoicing","invoiceLine","closed","status","busy","setBusy","to","setTo","subject","setSubject","message","setMessage","attemptKey","current","openForm","quoteSendPrefill","quoteEmailDefaultSubject","send","payload","globalThis","crypto","randomUUID","now","Math","random","toString","slice","response","method","headers","body","JSON","stringify","action","json","catch","ok","error","variant","allowDuplicate","formatQuoteNumber","linkReplaced","persist","spacing","color","direction","blocked","reason","size","disabled","onClick","onClose","orderId","order","undefined","maxWidth","fullWidth","aria-labelledby","id","sx","pt","type","value","onChange","event","target","required","slotProps","htmlInput","maxLength","QUOTE_MAX_SEND_SUBJECT","helperText","emailThread","multiline","minRows","QUOTE_MAX_SEND_MESSAGE","displayName"],"mappings":"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"}
@@ -0,0 +1,45 @@
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
+ */
17
+ /** The Inbox zone's props, restated: this plugin may not import the Inbox. */
18
+ export interface QuoteStartFromRequestProps {
19
+ hostId: string;
20
+ submissionId: string;
21
+ requestKind: string | null;
22
+ formName: string;
23
+ }
24
+ /** The CRM zone's props, restated: this plugin may not import the CRM. */
25
+ export interface QuoteStartFromContactProps {
26
+ hostId: string | null;
27
+ orgId: string | undefined;
28
+ kind: 'contact' | 'lead';
29
+ recordId: string;
30
+ name: string;
31
+ email: string | null;
32
+ phone: string;
33
+ company: string;
34
+ }
35
+ /** "Start a quote" on an open quote request in the Inbox. */
36
+ export declare function QuoteStartFromRequest(props: QuoteStartFromRequestProps): import("react").JSX.Element;
37
+ export declare namespace QuoteStartFromRequest {
38
+ var displayName: string;
39
+ }
40
+ /** "Start a quote" in a contact's header in the CRM. */
41
+ export declare function QuoteStartFromContact(props: QuoteStartFromContactProps): import("react").JSX.Element;
42
+ export declare namespace QuoteStartFromContact {
43
+ var displayName: string;
44
+ }
45
+ export default QuoteStartFromRequest;
@@ -0,0 +1,182 @@
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, Fragment as _Fragment } from "react/jsx-runtime";
19
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
20
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
21
+ import { ceilingedWindow, collectionCeiling, useFirestore, useFirestoreCollection, useUser } from "@aglyn/tenant-feature-instance";
22
+ import { Button } from "@mui/material";
23
+ import { collection } from "firebase/firestore";
24
+ import { useCallback, useMemo, useState } from "react";
25
+ import QuoteEditorDialog from "./quote-editor-dialog.component.js";
26
+ /*==========================================
27
+ * START A QUOTE FROM SOMEWHERE ELSE (AGL-4098).
28
+ *
29
+ * Two widgets in other plugins' zones, each a button that opens the quote
30
+ * editor already filled in:
31
+ *
32
+ * - on an open submission in the Inbox (`inboxSubmissionActions`), for a
33
+ * quote request: the editor asks this plugin's route for the request's
34
+ * prefill (`start-from-request`), which reads the request through the
35
+ * forms plugin's facts for the member asking;
36
+ * - in a contact's header in the CRM (`crmRecordActions`): the person as the
37
+ * page shows them is the quote's customer.
38
+ *
39
+ * Either way the quote is the editor's ordinary new quote, saved by the
40
+ * ordinary create under "Create and edit quotes"; the route refuses a member
41
+ * without it, in words naming the permission.
42
+ *=========================================*/ /** The products the editor's picker offers, as the Quotes card reads them. */ const PRODUCT_WINDOW = 200;
43
+ /** The editor over the store's products, read only while it is open. */ function QuoteStartDialog(props) {
44
+ const { hostId, start, onClose } = props;
45
+ const firestore = useFirestore();
46
+ const { data: productDocs } = useFirestoreCollection(()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'products'), PRODUCT_WINDOW), [
47
+ firestore,
48
+ hostId
49
+ ], {
50
+ idField: '$id'
51
+ });
52
+ const products = useMemo(()=>ceilingedWindow(productDocs != null ? productDocs : undefined, PRODUCT_WINDOW).rows.filter((product)=>!product['deletedAt']), [
53
+ productDocs
54
+ ]);
55
+ // The route decided the member may make quotes when it answered the start.
56
+ return /*#__PURE__*/ _jsx(QuoteEditorDialog, {
57
+ hostId: hostId,
58
+ quote: null,
59
+ products: products,
60
+ canEdit: true,
61
+ canSend: false,
62
+ start: start,
63
+ onClose: onClose
64
+ });
65
+ }
66
+ /** "Start a quote" on an open quote request in the Inbox. */ export function QuoteStartFromRequest(props) {
67
+ const { hostId, submissionId, requestKind } = props;
68
+ const { data: user } = useUser();
69
+ const { enqueueSnackbar } = useSnackbar();
70
+ const [busy, setBusy] = useState(false);
71
+ const [start, setStart] = useState(null);
72
+ const open = useCallback(async ()=>{
73
+ setBusy(true);
74
+ try {
75
+ var _payload_files;
76
+ const response = await authorizedFetch(user, '/api/commerce/quote', {
77
+ method: 'POST',
78
+ headers: {
79
+ 'Content-Type': 'application/json'
80
+ },
81
+ body: JSON.stringify({
82
+ action: 'start-from-request',
83
+ hostId,
84
+ submissionId
85
+ })
86
+ });
87
+ const payload = await response.json().catch(()=>({}));
88
+ if (!response.ok) {
89
+ var _ref;
90
+ return void enqueueSnackbar((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'The quote could not be started', {
91
+ variant: 'error',
92
+ allowDuplicate: true
93
+ });
94
+ }
95
+ setStart({
96
+ from: {
97
+ kind: 'submission',
98
+ id: submissionId
99
+ },
100
+ prefill: payload.prefill,
101
+ request: payload.request,
102
+ files: (_payload_files = payload.files) != null ? _payload_files : [],
103
+ threaded: payload.threaded === true
104
+ });
105
+ } catch (unused) {
106
+ enqueueSnackbar('The quote could not be started', {
107
+ variant: 'error',
108
+ allowDuplicate: true
109
+ });
110
+ } finally{
111
+ setBusy(false);
112
+ }
113
+ }, [
114
+ user,
115
+ hostId,
116
+ submissionId,
117
+ enqueueSnackbar
118
+ ]);
119
+ // Only a quote request has a quote to start.
120
+ if (requestKind !== 'quote' || !hostId) return null;
121
+ return /*#__PURE__*/ _jsxs(_Fragment, {
122
+ children: [
123
+ /*#__PURE__*/ _jsx(Button, {
124
+ variant: "outlined",
125
+ disabled: busy || !user,
126
+ onClick: ()=>void open(),
127
+ children: busy ? 'Starting…' : 'Start a quote'
128
+ }),
129
+ start ? /*#__PURE__*/ _jsx(QuoteStartDialog, {
130
+ hostId: hostId,
131
+ start: start,
132
+ onClose: ()=>setStart(null)
133
+ }) : null
134
+ ]
135
+ });
136
+ }
137
+ QuoteStartFromRequest.displayName = 'QuoteStartFromRequest';
138
+ /** "Start a quote" in a contact's header in the CRM. */ export function QuoteStartFromContact(props) {
139
+ const { hostId, kind, recordId, name, email, phone, company } = props;
140
+ const [start, setStart] = useState(null);
141
+ // A quote is a site's: at the organization level with no site, there is none to start.
142
+ if (!hostId) return null;
143
+ return /*#__PURE__*/ _jsxs(_Fragment, {
144
+ children: [
145
+ /*#__PURE__*/ _jsx(Button, {
146
+ size: "small",
147
+ variant: "outlined",
148
+ onClick: ()=>setStart({
149
+ from: null,
150
+ prefill: {
151
+ customer: _extends({
152
+ record: {
153
+ kind,
154
+ id: recordId
155
+ }
156
+ }, name ? {
157
+ name
158
+ } : {}, email ? {
159
+ email
160
+ } : {}, phone ? {
161
+ phone
162
+ } : {}, company ? {
163
+ company
164
+ } : {}),
165
+ lines: [],
166
+ notes: ''
167
+ }
168
+ }),
169
+ children: 'Start a quote'
170
+ }),
171
+ start ? /*#__PURE__*/ _jsx(QuoteStartDialog, {
172
+ hostId: hostId,
173
+ start: start,
174
+ onClose: ()=>setStart(null)
175
+ }) : null
176
+ ]
177
+ });
178
+ }
179
+ QuoteStartFromContact.displayName = 'QuoteStartFromContact';
180
+ export default QuoteStartFromRequest;
181
+
182
+ //# sourceMappingURL=quote-start.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/quote-start.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { Button } from '@mui/material'\nimport { collection } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport QuoteEditorDialog, { type QuoteEditorStart } from './quote-editor-dialog.component'\n\n/*==========================================\n * START A QUOTE FROM SOMEWHERE ELSE (AGL-4098).\n *\n * Two widgets in other plugins' zones, each a button that opens the quote\n * editor already filled in:\n *\n * - on an open submission in the Inbox (`inboxSubmissionActions`), for a\n * quote request: the editor asks this plugin's route for the request's\n * prefill (`start-from-request`), which reads the request through the\n * forms plugin's facts for the member asking;\n * - in a contact's header in the CRM (`crmRecordActions`): the person as the\n * page shows them is the quote's customer.\n *\n * Either way the quote is the editor's ordinary new quote, saved by the\n * ordinary create under \"Create and edit quotes\"; the route refuses a member\n * without it, in words naming the permission.\n *=========================================*/\n\n/** The products the editor's picker offers, as the Quotes card reads them. */\nconst PRODUCT_WINDOW = 200\n\n/** The Inbox zone's props, restated: this plugin may not import the Inbox. */\nexport interface QuoteStartFromRequestProps {\n hostId: string\n submissionId: string\n requestKind: string | null\n formName: string\n}\n\n/** The CRM zone's props, restated: this plugin may not import the CRM. */\nexport interface QuoteStartFromContactProps {\n hostId: string | null\n orgId: string | undefined\n kind: 'contact' | 'lead'\n recordId: string\n name: string\n email: string | null\n phone: string\n company: string\n}\n\n/** The editor over the store's products, read only while it is open. */\nfunction QuoteStartDialog(props: { hostId: string; start: QuoteEditorStart; onClose: () => void }) {\n const { hostId, start, onClose } = props\n const firestore = useFirestore()\n const { data: productDocs } = useFirestoreCollection<Record<string, any>>(\n () => collectionCeiling(collection(firestore, 'hosts', hostId, 'products'), PRODUCT_WINDOW),\n [firestore, hostId],\n { idField: '$id' },\n )\n const products = useMemo(\n () =>\n ceilingedWindow<Record<string, any>>(productDocs ?? undefined, PRODUCT_WINDOW).rows.filter(\n (product) => !product['deletedAt'],\n ),\n [productDocs],\n )\n // The route decided the member may make quotes when it answered the start.\n return (\n <QuoteEditorDialog hostId={hostId} quote={null} products={products} canEdit canSend={false} start={start} onClose={onClose} />\n )\n}\n\n/** \"Start a quote\" on an open quote request in the Inbox. */\nexport function QuoteStartFromRequest(props: QuoteStartFromRequestProps) {\n const { hostId, submissionId, requestKind } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const [busy, setBusy] = useState(false)\n const [start, setStart] = useState<QuoteEditorStart | null>(null)\n\n const open = useCallback(async () => {\n setBusy(true)\n try {\n const response = await authorizedFetch(user, '/api/commerce/quote', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ action: 'start-from-request', hostId, submissionId }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(payload?.error ?? 'The quote could not be started', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n setStart({\n from: { kind: 'submission', id: submissionId },\n prefill: payload.prefill,\n request: payload.request,\n files: payload.files ?? [],\n threaded: payload.threaded === true,\n })\n } catch {\n enqueueSnackbar('The quote could not be started', { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(false)\n }\n }, [user, hostId, submissionId, enqueueSnackbar])\n\n // Only a quote request has a quote to start.\n if (requestKind !== 'quote' || !hostId) return null\n return (\n <>\n <Button variant=\"outlined\" disabled={busy || !user} onClick={() => void open()}>\n {busy ? 'Starting…' : 'Start a quote'}\n </Button>\n {start ? <QuoteStartDialog hostId={hostId} start={start} onClose={() => setStart(null)} /> : null}\n </>\n )\n}\nQuoteStartFromRequest.displayName = 'QuoteStartFromRequest'\n\n/** \"Start a quote\" in a contact's header in the CRM. */\nexport function QuoteStartFromContact(props: QuoteStartFromContactProps) {\n const { hostId, kind, recordId, name, email, phone, company } = props\n const [start, setStart] = useState<QuoteEditorStart | null>(null)\n // A quote is a site's: at the organization level with no site, there is none to start.\n if (!hostId) return null\n return (\n <>\n <Button\n size=\"small\"\n variant=\"outlined\"\n onClick={() =>\n setStart({\n from: null,\n prefill: {\n customer: {\n record: { kind, id: recordId },\n ...(name ? { name } : {}),\n ...(email ? { email } : {}),\n ...(phone ? { phone } : {}),\n ...(company ? { company } : {}),\n },\n lines: [],\n notes: '',\n },\n })\n }\n >\n {'Start a quote'}\n </Button>\n {start ? <QuoteStartDialog hostId={hostId} start={start} onClose={() => setStart(null)} /> : null}\n </>\n )\n}\nQuoteStartFromContact.displayName = 'QuoteStartFromContact'\n\nexport default QuoteStartFromRequest\n"],"names":["useSnackbar","authorizedFetch","ceilingedWindow","collectionCeiling","useFirestore","useFirestoreCollection","useUser","Button","collection","useCallback","useMemo","useState","QuoteEditorDialog","PRODUCT_WINDOW","QuoteStartDialog","props","hostId","start","onClose","firestore","data","productDocs","idField","products","undefined","rows","filter","product","quote","canEdit","canSend","QuoteStartFromRequest","submissionId","requestKind","user","enqueueSnackbar","busy","setBusy","setStart","open","payload","response","method","headers","body","JSON","stringify","action","json","catch","ok","error","variant","allowDuplicate","from","kind","id","prefill","request","files","threaded","disabled","onClick","displayName","QuoteStartFromContact","recordId","name","email","phone","company","size","customer","record","lines","notes"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { type ModifierSelection, type ProductModifierGroup } from '../model/product-modifiers';
2
+ export interface ProductAddOnsProps {
3
+ /** The groups the website shows, as the product's page received them. */
4
+ groups: readonly ProductModifierGroup[];
5
+ /** The variant picked: a choice priced by variant shows its price on it. */
6
+ variantId?: string;
7
+ value: readonly ModifierSelection[];
8
+ onChange: (next: ModifierSelection[]) => void;
9
+ /** The store's money, e.g. `$1,250.00`. */
10
+ formatCents: (cents: number) => string;
11
+ disabled?: boolean;
12
+ }
13
+ /** The picks with one group's answer replaced, in the groups' own order. */
14
+ export declare function withGroupAnswer(groups: readonly ProductModifierGroup[], value: readonly ModifierSelection[], groupId: string, optionIds: readonly string[]): ModifierSelection[];
15
+ /** What a group asks for, said under it. */
16
+ export declare function addOnGroupHint(group: Pick<ProductModifierGroup, 'min' | 'max'>): string;
17
+ /**
18
+ * The add-ons a product's page offers (AGL-4100): a group that takes one
19
+ * choice is a select, one that takes several is a set of checkboxes, each
20
+ * choice with what it adds on the picked variant. A required group is marked
21
+ * as the form builder marks a required field. Its own chunk, loaded only by a
22
+ * product that has add-ons on the website.
23
+ */
24
+ export declare function ProductAddOns(props: ProductAddOnsProps): import("react").JSX.Element;
25
+ export declare namespace ProductAddOns {
26
+ var displayName: string;
27
+ }
28
+ export default ProductAddOns;