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

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 (140) hide show
  1. package/README.md +4 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/commerce-console-page.js +2 -1
  4. package/src/lib/components/commerce-console-page.js.map +1 -1
  5. package/src/lib/components/console/commerce-pdf-download.d.ts +30 -0
  6. package/src/lib/components/console/commerce-pdf-download.js +53 -0
  7. package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
  8. package/src/lib/components/console/host-orders-card.component.d.ts +6 -0
  9. package/src/lib/components/console/host-orders-card.component.js +3 -2
  10. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  11. package/src/lib/components/console/host-quotes-card.component.js +20 -4
  12. package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
  13. package/src/lib/components/console/invoice-send.component.d.ts +44 -0
  14. package/src/lib/components/console/invoice-send.component.js +237 -0
  15. package/src/lib/components/console/invoice-send.component.js.map +1 -0
  16. package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
  17. package/src/lib/components/console/order-detail-dialog.component.js +9 -2
  18. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  19. package/src/lib/components/console/order-invoice.component.d.ts +15 -4
  20. package/src/lib/components/console/order-invoice.component.js +93 -12
  21. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  22. package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
  23. package/src/lib/components/console/quote-editor-dialog.component.js +154 -5
  24. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
  25. package/src/lib/components/console/quote-proofs.component.d.ts +48 -0
  26. package/src/lib/components/console/quote-proofs.component.js +694 -0
  27. package/src/lib/components/console/quote-proofs.component.js.map +1 -0
  28. package/src/lib/components/console/quote-send.component.d.ts +42 -0
  29. package/src/lib/components/console/quote-send.component.js +307 -0
  30. package/src/lib/components/console/quote-send.component.js.map +1 -0
  31. package/src/lib/components/console/quote-start.component.d.ts +45 -0
  32. package/src/lib/components/console/quote-start.component.js +182 -0
  33. package/src/lib/components/console/quote-start.component.js.map +1 -0
  34. package/src/lib/components/product-options-field-body.d.ts +44 -0
  35. package/src/lib/components/product-options-field-body.js +260 -0
  36. package/src/lib/components/product-options-field-body.js.map +1 -0
  37. package/src/lib/components/product-options-field.d.ts +32 -0
  38. package/src/lib/components/product-options-field.js +128 -0
  39. package/src/lib/components/product-options-field.js.map +1 -0
  40. package/src/lib/components/quote-page-body.js +67 -20
  41. package/src/lib/components/quote-page-body.js.map +1 -1
  42. package/src/lib/components/quote-proof-panel.d.ts +42 -0
  43. package/src/lib/components/quote-proof-panel.js +436 -0
  44. package/src/lib/components/quote-proof-panel.js.map +1 -0
  45. package/src/lib/model/commerce-invoices.d.ts +132 -0
  46. package/src/lib/model/commerce-invoices.js +332 -0
  47. package/src/lib/model/commerce-invoices.js.map +1 -0
  48. package/src/lib/model/commerce-orders.d.ts +29 -2
  49. package/src/lib/model/commerce-orders.js.map +1 -1
  50. package/src/lib/model/commerce-product-field.d.ts +23 -0
  51. package/src/lib/model/commerce-product-field.js +26 -0
  52. package/src/lib/model/commerce-product-field.js.map +1 -0
  53. package/src/lib/model/commerce-quote-email.d.ts +67 -0
  54. package/src/lib/model/commerce-quote-email.js +154 -0
  55. package/src/lib/model/commerce-quote-email.js.map +1 -0
  56. package/src/lib/model/commerce-quote-pay.d.ts +76 -6
  57. package/src/lib/model/commerce-quote-pay.js +105 -39
  58. package/src/lib/model/commerce-quote-pay.js.map +1 -1
  59. package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
  60. package/src/lib/model/commerce-quote-proofs.js +348 -0
  61. package/src/lib/model/commerce-quote-proofs.js.map +1 -0
  62. package/src/lib/model/commerce-quote-request.d.ts +93 -0
  63. package/src/lib/model/commerce-quote-request.js +207 -0
  64. package/src/lib/model/commerce-quote-request.js.map +1 -0
  65. package/src/lib/model/commerce-quotes.d.ts +140 -3
  66. package/src/lib/model/commerce-quotes.js.map +1 -1
  67. package/src/lib/model/index.d.ts +4 -0
  68. package/src/lib/model/index.js +4 -0
  69. package/src/lib/model/index.js.map +1 -1
  70. package/src/lib/plugin.js +19 -0
  71. package/src/lib/plugin.js.map +1 -1
  72. package/src/lib/server/billing-webhook.js +115 -70
  73. package/src/lib/server/billing-webhook.js.map +1 -1
  74. package/src/lib/server/commerce-pdf.d.ts +104 -0
  75. package/src/lib/server/commerce-pdf.js +310 -0
  76. package/src/lib/server/commerce-pdf.js.map +1 -0
  77. package/src/lib/server/draft-order.js +23 -3
  78. package/src/lib/server/draft-order.js.map +1 -1
  79. package/src/lib/server/invoice-email.d.ts +76 -0
  80. package/src/lib/server/invoice-email.js +183 -0
  81. package/src/lib/server/invoice-email.js.map +1 -0
  82. package/src/lib/server/order-invoice-send.d.ts +43 -0
  83. package/src/lib/server/order-invoice-send.js +218 -0
  84. package/src/lib/server/order-invoice-send.js.map +1 -0
  85. package/src/lib/server/order-print.d.ts +6 -0
  86. package/src/lib/server/order-print.js +22 -5
  87. package/src/lib/server/order-print.js.map +1 -1
  88. package/src/lib/server/order-status.d.ts +11 -0
  89. package/src/lib/server/order-status.js +44 -8
  90. package/src/lib/server/order-status.js.map +1 -1
  91. package/src/lib/server/pdf-response.d.ts +31 -0
  92. package/src/lib/server/pdf-response.js +30 -0
  93. package/src/lib/server/pdf-response.js.map +1 -0
  94. package/src/lib/server/person-eraser.d.ts +3 -2
  95. package/src/lib/server/person-eraser.js +25 -6
  96. package/src/lib/server/person-eraser.js.map +1 -1
  97. package/src/lib/server/pos-reader-splash.d.ts +2 -18
  98. package/src/lib/server/pos-reader-splash.js +8 -93
  99. package/src/lib/server/pos-reader-splash.js.map +1 -1
  100. package/src/lib/server/quote-email.d.ts +85 -0
  101. package/src/lib/server/quote-email.js +211 -0
  102. package/src/lib/server/quote-email.js.map +1 -0
  103. package/src/lib/server/quote-link-token.d.ts +11 -0
  104. package/src/lib/server/quote-link-token.js +21 -0
  105. package/src/lib/server/quote-link-token.js.map +1 -1
  106. package/src/lib/server/quote-pay.d.ts +38 -6
  107. package/src/lib/server/quote-pay.js +115 -53
  108. package/src/lib/server/quote-pay.js.map +1 -1
  109. package/src/lib/server/quote-proofs.d.ts +51 -0
  110. package/src/lib/server/quote-proofs.js +500 -0
  111. package/src/lib/server/quote-proofs.js.map +1 -0
  112. package/src/lib/server/quote-uploads.d.ts +33 -0
  113. package/src/lib/server/quote-uploads.js +29 -0
  114. package/src/lib/server/quote-uploads.js.map +1 -0
  115. package/src/lib/server/quote.d.ts +25 -0
  116. package/src/lib/server/quote.js +638 -8
  117. package/src/lib/server/quote.js.map +1 -1
  118. package/src/lib/server/site-image-source.d.ts +38 -0
  119. package/src/lib/server/site-image-source.js +130 -0
  120. package/src/lib/server/site-image-source.js.map +1 -0
  121. package/src/lib/server/site-page-resolver.js +2 -1
  122. package/src/lib/server/site-page-resolver.js.map +1 -1
  123. package/src/lib/server.js +24 -1
  124. package/src/lib/server.js.map +1 -1
  125. package/src/lib/site.js +8 -0
  126. package/src/lib/site.js.map +1 -1
  127. package/src/lib/tenant-emails.js +243 -0
  128. package/src/lib/tenant-emails.js.map +1 -1
  129. package/src/lib/utils/commerce-document-pdf.d.ts +82 -0
  130. package/src/lib/utils/commerce-document-pdf.js +613 -0
  131. package/src/lib/utils/commerce-document-pdf.js.map +1 -0
  132. package/src/lib/utils/invoice-email-content.d.ts +45 -0
  133. package/src/lib/utils/invoice-email-content.js +126 -0
  134. package/src/lib/utils/invoice-email-content.js.map +1 -0
  135. package/src/lib/utils/order-invoice.d.ts +52 -3
  136. package/src/lib/utils/order-invoice.js +124 -64
  137. package/src/lib/utils/order-invoice.js.map +1 -1
  138. package/src/lib/utils/quote-email-content.d.ts +62 -0
  139. package/src/lib/utils/quote-email-content.js +154 -0
  140. package/src/lib/utils/quote-email-content.js.map +1 -0
@@ -0,0 +1,237 @@
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 { siteContactEmail, resolveHostToken } from "@aglyn/aglyn/app-utils/host-tokens";
20
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
21
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
22
+ import { useFirestore, useUser } from "@aglyn/tenant-feature-instance";
23
+ import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField, Typography } from "@mui/material";
24
+ import { doc, getDoc } from "firebase/firestore";
25
+ import { useCallback, useEffect, useRef, useState } from "react";
26
+ /** `Oct 29, 2026, 3:04 PM`. */ const sentAt = (atMs)=>new Date(atMs).toLocaleString('en-US', {
27
+ dateStyle: 'medium',
28
+ timeStyle: 'short'
29
+ });
30
+ /**
31
+ * The order's (or the quote's) last invoice email, as the console says it:
32
+ * `Invoice sent Oct 29, 2026, 3:04 PM to jordan@example.com.` Null when none
33
+ * was sent.
34
+ */ export function invoiceSentLine(order) {
35
+ const last = CommerceModel.lastInvoiceSend(order);
36
+ if (!last) return null;
37
+ return `${last.paid ? 'Paid invoice' : 'Invoice'} sent ${sentAt(Number(last.atMs))} to ${last.to || 'the customer'}.`;
38
+ }
39
+ /**
40
+ * Send invoice (AGL-4099), for an order or a quote: a short form (To,
41
+ * Subject, Message) that emails the invoice in the site's look with the
42
+ * invoice PDF attached. Unpaid, the email carries a Pay button to the order's
43
+ * lasting pay link on the business's site; paid, it is the customer's record.
44
+ * A quote's invoice lands in the quote's conversation, and an order's in its
45
+ * own. The site's name and contact email are read once when the form opens,
46
+ * so the dialog that offers it listens to nothing more.
47
+ */ export function InvoiceSendDialog(props) {
48
+ const { open, onClose, hostId, orderId, order } = props;
49
+ const firestore = useFirestore();
50
+ const { data: user } = useUser();
51
+ const { enqueueSnackbar } = useSnackbar();
52
+ const [busy, setBusy] = useState(false);
53
+ const [to, setTo] = useState('');
54
+ const [subject, setSubject] = useState('');
55
+ const [message, setMessage] = useState('');
56
+ const [contact, setContact] = useState(null);
57
+ /*
58
+ * One attempt per form (AGL-1697's rule): a retried Send presents the same
59
+ * key and is answered from the first, so the customer never gets two; a
60
+ * changed form is a new attempt.
61
+ */ const attemptKey = useRef('');
62
+ useEffect(()=>{
63
+ attemptKey.current = '';
64
+ }, [
65
+ to,
66
+ subject,
67
+ message
68
+ ]);
69
+ useEffect(()=>{
70
+ if (!open) return;
71
+ let live = true;
72
+ setTo(CommerceModel.invoiceSendRecipient(order, ''));
73
+ setSubject(CommerceModel.invoiceEmailDefaultSubject(order, '', orderId));
74
+ setMessage('');
75
+ void getDoc(doc(firestore, 'hosts', hostId)).then((snapshot)=>{
76
+ var _snapshot_data, _resolveHostToken;
77
+ if (!live) return;
78
+ const host = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
79
+ const businessName = String((_resolveHostToken = resolveHostToken('businessName', host)) != null ? _resolveHostToken : '').trim();
80
+ setSubject(CommerceModel.invoiceEmailDefaultSubject(order, businessName, orderId));
81
+ const listed = siteContactEmail(host);
82
+ setContact(listed && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(listed) ? listed : null);
83
+ }).catch(()=>undefined);
84
+ return ()=>{
85
+ live = false;
86
+ };
87
+ // Filled once per opening; edits after that are the member's.
88
+ // eslint-disable-next-line react-hooks/exhaustive-deps
89
+ }, [
90
+ open
91
+ ]);
92
+ const paid = CommerceModel.invoiceIsPaid(order);
93
+ const refused = CommerceModel.invoiceSendRefusal(order, orderId, to.trim());
94
+ // The address is the form's to fix; every other refusal is the order's, said up front.
95
+ const blocker = refused && refused !== 'no-address' && refused !== 'bad-address' ? refused : null;
96
+ const threaded = Boolean(CommerceModel.invoiceEmailThread(order));
97
+ const number = CommerceModel.formatInvoiceNumber(order, orderId);
98
+ const send = useCallback(async ()=>{
99
+ setBusy(true);
100
+ try {
101
+ var _payload_to;
102
+ if (!attemptKey.current) {
103
+ var _ref;
104
+ var _globalThis_crypto_randomUUID, _globalThis_crypto;
105
+ 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)}`;
106
+ }
107
+ const response = await authorizedFetch(user, '/api/commerce/order-invoice-send', {
108
+ method: 'POST',
109
+ headers: {
110
+ 'Content-Type': 'application/json',
111
+ 'Idempotency-Key': attemptKey.current
112
+ },
113
+ body: JSON.stringify({
114
+ hostId,
115
+ orderId,
116
+ to,
117
+ subject,
118
+ message
119
+ })
120
+ });
121
+ const payload = await response.json().catch(()=>({}));
122
+ if (!response.ok) {
123
+ var _ref1;
124
+ return void enqueueSnackbar((_ref1 = payload == null ? void 0 : payload.error) != null ? _ref1 : 'The invoice could not be sent', {
125
+ variant: 'error',
126
+ allowDuplicate: true
127
+ });
128
+ }
129
+ enqueueSnackbar(`${number} sent to ${String((_payload_to = payload.to) != null ? _payload_to : to)}`, {
130
+ variant: 'success',
131
+ persist: false
132
+ });
133
+ onClose();
134
+ } finally{
135
+ setBusy(false);
136
+ }
137
+ }, [
138
+ user,
139
+ hostId,
140
+ orderId,
141
+ to,
142
+ subject,
143
+ message,
144
+ number,
145
+ enqueueSnackbar,
146
+ onClose
147
+ ]);
148
+ return /*#__PURE__*/ _jsxs(Dialog, {
149
+ open: open,
150
+ onClose: busy ? undefined : onClose,
151
+ maxWidth: "sm",
152
+ fullWidth: true,
153
+ "aria-labelledby": "invoice-send-title",
154
+ children: [
155
+ /*#__PURE__*/ _jsx(DialogTitle, {
156
+ id: "invoice-send-title",
157
+ children: `Send invoice: ${number}`
158
+ }),
159
+ /*#__PURE__*/ _jsx(DialogContent, {
160
+ children: /*#__PURE__*/ _jsxs(Stack, {
161
+ spacing: 2,
162
+ sx: {
163
+ pt: 1
164
+ },
165
+ children: [
166
+ blocker ? /*#__PURE__*/ _jsx(Typography, {
167
+ variant: "body2",
168
+ color: "error",
169
+ children: CommerceModel.invoiceSendRefusalMessage(blocker)
170
+ }) : null,
171
+ /*#__PURE__*/ _jsx(TextField, {
172
+ label: "To",
173
+ type: "email",
174
+ value: to,
175
+ onChange: (event)=>setTo(event.target.value),
176
+ required: true,
177
+ fullWidth: true,
178
+ disabled: busy
179
+ }),
180
+ /*#__PURE__*/ _jsx(TextField, {
181
+ label: "Subject",
182
+ value: subject,
183
+ onChange: (event)=>setSubject(event.target.value),
184
+ fullWidth: true,
185
+ disabled: busy,
186
+ slotProps: {
187
+ htmlInput: {
188
+ maxLength: CommerceModel.QUOTE_MAX_SEND_SUBJECT
189
+ }
190
+ },
191
+ helperText: threaded ? 'Keep the subject the same to keep these emails in one conversation in your customer’s inbox.' : undefined
192
+ }),
193
+ /*#__PURE__*/ _jsx(TextField, {
194
+ label: "Message",
195
+ value: message,
196
+ onChange: (event)=>setMessage(event.target.value),
197
+ multiline: true,
198
+ minRows: 4,
199
+ fullWidth: true,
200
+ disabled: busy,
201
+ slotProps: {
202
+ htmlInput: {
203
+ maxLength: CommerceModel.QUOTE_MAX_SEND_MESSAGE
204
+ }
205
+ },
206
+ helperText: "Shown at the top of the email, in your words."
207
+ }),
208
+ /*#__PURE__*/ _jsx(Typography, {
209
+ variant: "body2",
210
+ color: "text.secondary",
211
+ children: (paid ? 'The email shows the invoice as paid, with the date it was paid, for your customer’s records. ' : 'The email shows the invoice number, date, total and what is due, with a button to pay it by card on your site. The button keeps working until it is paid. ') + `The invoice is attached as a PDF. Replies go to ${contact || 'your email'}.`
212
+ })
213
+ ]
214
+ })
215
+ }),
216
+ /*#__PURE__*/ _jsxs(DialogActions, {
217
+ children: [
218
+ /*#__PURE__*/ _jsx(Button, {
219
+ onClick: onClose,
220
+ disabled: busy,
221
+ children: 'Cancel'
222
+ }),
223
+ /*#__PURE__*/ _jsx(Button, {
224
+ variant: "contained",
225
+ disabled: busy || !to.trim() || Boolean(blocker),
226
+ onClick: ()=>void send(),
227
+ children: busy ? 'Sending…' : 'Send'
228
+ })
229
+ ]
230
+ })
231
+ ]
232
+ });
233
+ }
234
+ InvoiceSendDialog.displayName = 'InvoiceSendDialog';
235
+ export default InvoiceSendDialog;
236
+
237
+ //# sourceMappingURL=invoice-send.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/invoice-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 { siteContactEmail, resolveHostToken, type HostTokenSource } from '@aglyn/aglyn/app-utils/host-tokens'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useFirestore, 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, getDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useRef, useState } from 'react'\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 * The order's (or the quote's) last invoice email, as the console says it:\n * `Invoice sent Oct 29, 2026, 3:04 PM to jordan@example.com.` Null when none\n * was sent.\n */\nexport function invoiceSentLine(order: Pick<CommerceModel.HostOrder, 'invoiceSends'>): string | null {\n const last = CommerceModel.lastInvoiceSend(order)\n if (!last) return null\n return `${last.paid ? 'Paid invoice' : 'Invoice'} sent ${sentAt(Number(last.atMs))} to ${last.to || 'the customer'}.`\n}\n\nexport interface InvoiceSendDialogProps {\n open: boolean\n onClose: () => void\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n}\n\n/**\n * Send invoice (AGL-4099), for an order or a quote: a short form (To,\n * Subject, Message) that emails the invoice in the site's look with the\n * invoice PDF attached. Unpaid, the email carries a Pay button to the order's\n * lasting pay link on the business's site; paid, it is the customer's record.\n * A quote's invoice lands in the quote's conversation, and an order's in its\n * own. The site's name and contact email are read once when the form opens,\n * so the dialog that offers it listens to nothing more.\n */\nexport function InvoiceSendDialog(props: InvoiceSendDialogProps) {\n const { open, onClose, hostId, orderId, order } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const [busy, setBusy] = useState(false)\n const [to, setTo] = useState('')\n const [subject, setSubject] = useState('')\n const [message, setMessage] = useState('')\n const [contact, setContact] = useState<string | null>(null)\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 useEffect(() => {\n if (!open) return\n let live = true\n setTo(CommerceModel.invoiceSendRecipient(order, ''))\n setSubject(CommerceModel.invoiceEmailDefaultSubject(order, '', orderId))\n setMessage('')\n void getDoc(doc(firestore, 'hosts', hostId))\n .then((snapshot) => {\n if (!live) return\n const host = (snapshot.data() ?? {}) as Record<string, unknown>\n const businessName = String(resolveHostToken('businessName', host as HostTokenSource) ?? '').trim()\n setSubject(CommerceModel.invoiceEmailDefaultSubject(order, businessName, orderId))\n const listed = siteContactEmail(host as never)\n setContact(listed && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(listed) ? listed : null)\n })\n .catch(() => undefined)\n return () => {\n live = false\n }\n // Filled once per opening; edits after that are the member's.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open])\n\n const paid = CommerceModel.invoiceIsPaid(order)\n const refused = CommerceModel.invoiceSendRefusal(order, orderId, to.trim())\n // The address is the form's to fix; every other refusal is the order's, said up front.\n const blocker = refused && refused !== 'no-address' && refused !== 'bad-address' ? refused : null\n const threaded = Boolean(CommerceModel.invoiceEmailThread(order))\n const number = CommerceModel.formatInvoiceNumber(order, orderId)\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/order-invoice-send', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Idempotency-Key': attemptKey.current },\n body: JSON.stringify({ hostId, orderId, to, subject, message }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(payload?.error ?? 'The invoice could not be sent', { variant: 'error', allowDuplicate: true })\n }\n enqueueSnackbar(`${number} sent to ${String(payload.to ?? to)}`, { variant: 'success', persist: false })\n onClose()\n } finally {\n setBusy(false)\n }\n }, [user, hostId, orderId, to, subject, message, number, enqueueSnackbar, onClose])\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} maxWidth=\"sm\" fullWidth aria-labelledby=\"invoice-send-title\">\n <DialogTitle id=\"invoice-send-title\">{`Send invoice: ${number}`}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n {blocker ? (\n <Typography variant=\"body2\" color=\"error\">\n {CommerceModel.invoiceSendRefusalMessage(blocker)}\n </Typography>\n ) : null}\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 threaded\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 {(paid\n ? 'The email shows the invoice as paid, with the date it was paid, for your customer’s records. '\n : 'The email shows the invoice number, date, total and what is due, with a button to pay it by card on your site. The button keeps working until it is paid. ') +\n `The invoice is attached as a PDF. Replies go to ${contact || 'your email'}.`}\n </Typography>\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" disabled={busy || !to.trim() || Boolean(blocker)} onClick={() => void send()}>\n {busy ? 'Sending…' : 'Send'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nInvoiceSendDialog.displayName = 'InvoiceSendDialog'\n\nexport default InvoiceSendDialog\n"],"names":["CommerceModel","siteContactEmail","resolveHostToken","useSnackbar","authorizedFetch","useFirestore","useUser","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","doc","getDoc","useCallback","useEffect","useRef","useState","sentAt","atMs","Date","toLocaleString","dateStyle","timeStyle","invoiceSentLine","order","last","lastInvoiceSend","paid","Number","to","InvoiceSendDialog","props","open","onClose","hostId","orderId","firestore","data","user","enqueueSnackbar","busy","setBusy","setTo","subject","setSubject","message","setMessage","contact","setContact","attemptKey","current","live","invoiceSendRecipient","invoiceEmailDefaultSubject","then","snapshot","host","businessName","String","trim","listed","test","catch","undefined","invoiceIsPaid","refused","invoiceSendRefusal","blocker","threaded","Boolean","invoiceEmailThread","number","formatInvoiceNumber","send","payload","globalThis","crypto","randomUUID","now","Math","random","toString","slice","response","method","headers","body","JSON","stringify","json","ok","error","variant","allowDuplicate","persist","maxWidth","fullWidth","aria-labelledby","id","spacing","sx","pt","color","invoiceSendRefusalMessage","label","type","value","onChange","event","target","required","disabled","slotProps","htmlInput","maxLength","QUOTE_MAX_SEND_SUBJECT","helperText","multiline","minRows","QUOTE_MAX_SEND_MESSAGE","onClick","displayName"],"mappings":"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"}
@@ -5,6 +5,11 @@ export interface OrderDetailDialogProps {
5
5
  $id: string;
6
6
  }) | null;
7
7
  onClose: () => void;
8
+ /**
9
+ * Holds "Send quotes and invoices to customers" (`commerce.quotes.send`),
10
+ * for Send invoice (AGL-4099). Absent reads as held: the route decides.
11
+ */
12
+ canSendInvoices?: boolean;
8
13
  }
9
14
  /**
10
15
  * The dispute chip's colour (AGL-1796).
@@ -39,6 +39,7 @@ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
39
39
  import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
40
40
  import { consoleOrderZoneOrder, FulfillItemsPanel, FulfillmentList, toZoneFulfillment } from "./order-fulfillment-panel.component.js";
41
41
  import { ORDER_DETAIL_ZONE, ORDER_FULFILLMENT_ZONE } from "./order-zones.js";
42
+ import { invoiceSentLine } from "./invoice-send.component.js";
42
43
  import { OrderInvoiceButton } from "./order-invoice.component.js";
43
44
  import { OrderReceiptResend } from "./order-receipt-resend.component.js";
44
45
  import { OrderReturns } from "./order-returns.component.js";
@@ -89,7 +90,7 @@ const usd = (cents)=>`$${((cents != null ? cents : 0) / 100).toFixed(2)}`;
89
90
  * write), notes, printable packing slip.
90
91
  */ export function OrderDetailDialog(props) {
91
92
  var _STATUS_COLOR_order_status, _order_lineItems, _order_extras, _order_timeline;
92
- const { hostId, order: rawOrder, onClose } = props;
93
+ const { hostId, order: rawOrder, onClose, canSendInvoices = true } = props;
93
94
  const firestore = useFirestore();
94
95
  const { data: user } = useUser();
95
96
  const { enqueueSnackbar } = useSnackbar();
@@ -1024,6 +1025,11 @@ const usd = (cents)=>`$${((cents != null ? cents : 0) / 100).toFixed(2)}`;
1024
1025
  orderId: orderId,
1025
1026
  order: order
1026
1027
  }) : null,
1028
+ invoiceSentLine(order) ? /*#__PURE__*/ _jsx(Typography, {
1029
+ variant: "body2",
1030
+ color: "text.secondary",
1031
+ children: invoiceSentLine(order)
1032
+ }) : null,
1027
1033
  order.paymentLinkUrl && order.status === 'pending' ? /*#__PURE__*/ _jsx(Button, {
1028
1034
  size: "small",
1029
1035
  sx: {
@@ -1058,7 +1064,8 @@ const usd = (cents)=>`$${((cents != null ? cents : 0) / 100).toFixed(2)}`;
1058
1064
  orderId ? /*#__PURE__*/ _jsx(OrderInvoiceButton, {
1059
1065
  hostId: hostId,
1060
1066
  orderId: orderId,
1061
- order: order
1067
+ order: order,
1068
+ canSend: canSendInvoices
1062
1069
  }) : null,
1063
1070
  orderId ? /*#__PURE__*/ _jsx(OrderReceiptResend, {
1064
1071
  hostId: hostId,