@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.
- package/README.md +4 -2
- package/package.json +13 -13
- package/src/lib/components/cart-fulfillment.d.ts +3 -0
- package/src/lib/components/cart-fulfillment.js.map +1 -1
- package/src/lib/components/cart.js +14 -4
- package/src/lib/components/cart.js.map +1 -1
- package/src/lib/components/commerce-console-page.js +2 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.d.ts +6 -0
- package/src/lib/components/console/host-orders-card.component.js +3 -2
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/invoice-send.component.d.ts +44 -0
- package/src/lib/components/console/invoice-send.component.js +237 -0
- package/src/lib/components/console/invoice-send.component.js.map +1 -0
- package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
- package/src/lib/components/console/order-detail-dialog.component.js +9 -2
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/order-invoice.component.d.ts +9 -0
- package/src/lib/components/console/order-invoice.component.js +27 -2
- package/src/lib/components/console/order-invoice.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-item-dialog.component.js +3 -2
- package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
- package/src/lib/components/console/pos-kiosk-page.component.js +2 -1
- package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +1 -1
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/product-add-ons-fields.component.d.ts +41 -0
- package/src/lib/components/console/product-add-ons-fields.component.js +356 -0
- package/src/lib/components/console/product-add-ons-fields.component.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +31 -5
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-register-fields.component.d.ts +4 -5
- package/src/lib/components/console/product-register-fields.component.js +8 -197
- package/src/lib/components/console/product-register-fields.component.js.map +1 -1
- package/src/lib/components/console/products-hub-zone.component.js +6 -0
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
- package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
- package/src/lib/components/console/quote-editor-dialog.component.js +102 -4
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/quote-pay-link.component.d.ts +8 -1
- package/src/lib/components/console/quote-pay-link.component.js +29 -3
- package/src/lib/components/console/quote-pay-link.component.js.map +1 -1
- package/src/lib/components/console/quote-proofs.component.d.ts +11 -0
- package/src/lib/components/console/quote-proofs.component.js +68 -0
- package/src/lib/components/console/quote-proofs.component.js.map +1 -1
- package/src/lib/components/console/quote-send.component.d.ts +42 -0
- package/src/lib/components/console/quote-send.component.js +304 -0
- package/src/lib/components/console/quote-send.component.js.map +1 -0
- package/src/lib/components/console/quote-start.component.d.ts +45 -0
- package/src/lib/components/console/quote-start.component.js +182 -0
- package/src/lib/components/console/quote-start.component.js.map +1 -0
- package/src/lib/components/product-add-ons.d.ts +28 -0
- package/src/lib/components/product-add-ons.js +136 -0
- package/src/lib/components/product-add-ons.js.map +1 -0
- package/src/lib/components/product-detail.d.ts +16 -0
- package/src/lib/components/product-detail.js +112 -19
- package/src/lib/components/product-detail.js.map +1 -1
- package/src/lib/components/product-grid.d.ts +8 -2
- package/src/lib/components/product-grid.js +6 -1
- package/src/lib/components/product-grid.js.map +1 -1
- package/src/lib/components/product-options-field-body.d.ts +44 -0
- package/src/lib/components/product-options-field-body.js +260 -0
- package/src/lib/components/product-options-field-body.js.map +1 -0
- package/src/lib/components/product-options-field.d.ts +32 -0
- package/src/lib/components/product-options-field.js +128 -0
- package/src/lib/components/product-options-field.js.map +1 -0
- package/src/lib/components/quote-page-body.js +39 -20
- package/src/lib/components/quote-page-body.js.map +1 -1
- package/src/lib/model/commerce-cart.d.ts +15 -3
- package/src/lib/model/commerce-cart.js +10 -3
- package/src/lib/model/commerce-cart.js.map +1 -1
- package/src/lib/model/commerce-invoices.d.ts +139 -0
- package/src/lib/model/commerce-invoices.js +343 -0
- package/src/lib/model/commerce-invoices.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +35 -5
- package/src/lib/model/commerce-orders.js +3 -0
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-pos-kiosk.d.ts +1 -1
- package/src/lib/model/commerce-pos-kiosk.js +3 -2
- package/src/lib/model/commerce-pos-kiosk.js.map +1 -1
- package/src/lib/model/commerce-product-field.d.ts +23 -0
- package/src/lib/model/commerce-product-field.js +26 -0
- package/src/lib/model/commerce-product-field.js.map +1 -0
- package/src/lib/model/commerce-quote-email.d.ts +81 -0
- package/src/lib/model/commerce-quote-email.js +170 -0
- package/src/lib/model/commerce-quote-email.js.map +1 -0
- package/src/lib/model/commerce-quote-pay.d.ts +49 -6
- package/src/lib/model/commerce-quote-pay.js +69 -36
- package/src/lib/model/commerce-quote-pay.js.map +1 -1
- package/src/lib/model/commerce-quote-request.d.ts +93 -0
- package/src/lib/model/commerce-quote-request.js +207 -0
- package/src/lib/model/commerce-quote-request.js.map +1 -0
- package/src/lib/model/commerce-quotes.d.ts +81 -0
- package/src/lib/model/commerce-quotes.js +1 -1
- package/src/lib/model/commerce-quotes.js.map +1 -1
- package/src/lib/model/commerce.d.ts +18 -0
- package/src/lib/model/commerce.js +26 -0
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +3 -0
- package/src/lib/model/index.js +3 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/product-modifiers.d.ts +74 -4
- package/src/lib/model/product-modifiers.js +131 -10
- package/src/lib/model/product-modifiers.js.map +1 -1
- package/src/lib/plugin.js +19 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/billing-webhook.js +164 -98
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cart-checkout.js +14 -2
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/cart.d.ts +5 -0
- package/src/lib/server/cart.js +41 -4
- package/src/lib/server/cart.js.map +1 -1
- package/src/lib/server/catalog.d.ts +6 -0
- package/src/lib/server/catalog.js +2 -0
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout-extras.js +3 -1
- package/src/lib/server/checkout-extras.js.map +1 -1
- package/src/lib/server/checkout.js +28 -3
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/commerce-pdf.d.ts +5 -1
- package/src/lib/server/commerce-pdf.js +37 -10
- package/src/lib/server/commerce-pdf.js.map +1 -1
- package/src/lib/server/draft-order.js +23 -3
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/invoice-email.d.ts +76 -0
- package/src/lib/server/invoice-email.js +183 -0
- package/src/lib/server/invoice-email.js.map +1 -0
- package/src/lib/server/local-fulfillment.js +5 -1
- package/src/lib/server/local-fulfillment.js.map +1 -1
- package/src/lib/server/order-invoice-send.d.ts +43 -0
- package/src/lib/server/order-invoice-send.js +218 -0
- package/src/lib/server/order-invoice-send.js.map +1 -0
- package/src/lib/server/order-status.d.ts +11 -0
- package/src/lib/server/order-status.js +44 -8
- package/src/lib/server/order-status.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +3 -2
- package/src/lib/server/person-eraser.js +25 -6
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/pos-offline-sync.js +1 -1
- package/src/lib/server/pos-offline-sync.js.map +1 -1
- package/src/lib/server/pos-order.js +1 -1
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/product.d.ts +26 -0
- package/src/lib/server/product.js +18 -0
- package/src/lib/server/product.js.map +1 -1
- package/src/lib/server/products-write.js +5 -2
- package/src/lib/server/products-write.js.map +1 -1
- package/src/lib/server/quote-email.d.ts +85 -0
- package/src/lib/server/quote-email.js +211 -0
- package/src/lib/server/quote-email.js.map +1 -0
- package/src/lib/server/quote-link-token.d.ts +11 -0
- package/src/lib/server/quote-link-token.js +21 -0
- package/src/lib/server/quote-link-token.js.map +1 -1
- package/src/lib/server/quote-pay.d.ts +8 -3
- package/src/lib/server/quote-pay.js +105 -43
- package/src/lib/server/quote-pay.js.map +1 -1
- package/src/lib/server/quote.d.ts +11 -0
- package/src/lib/server/quote.js +309 -12
- package/src/lib/server/quote.js.map +1 -1
- package/src/lib/server/site-page-resolver.js +2 -1
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/site.js +8 -0
- package/src/lib/site.js.map +1 -1
- package/src/lib/tenant-emails.js +243 -0
- package/src/lib/tenant-emails.js.map +1 -1
- package/src/lib/utils/add-to-cart.d.ts +8 -0
- package/src/lib/utils/add-to-cart.js +10 -2
- package/src/lib/utils/add-to-cart.js.map +1 -1
- package/src/lib/utils/commerce-document-pdf.d.ts +5 -0
- package/src/lib/utils/commerce-document-pdf.js +6 -1
- package/src/lib/utils/commerce-document-pdf.js.map +1 -1
- package/src/lib/utils/invoice-email-content.d.ts +45 -0
- package/src/lib/utils/invoice-email-content.js +126 -0
- package/src/lib/utils/invoice-email-content.js.map +1 -0
- package/src/lib/utils/order-invoice.d.ts +1 -1
- package/src/lib/utils/order-invoice.js +14 -6
- package/src/lib/utils/order-invoice.js.map +1 -1
- package/src/lib/utils/quote-email-content.d.ts +63 -0
- package/src/lib/utils/quote-email-content.js +157 -0
- 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;
|