@aglyn/plugins-commerce 1.0.0-beta.290 → 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 (111) 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/host-orders-card.component.d.ts +6 -0
  6. package/src/lib/components/console/host-orders-card.component.js +3 -2
  7. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  8. package/src/lib/components/console/invoice-send.component.d.ts +44 -0
  9. package/src/lib/components/console/invoice-send.component.js +237 -0
  10. package/src/lib/components/console/invoice-send.component.js.map +1 -0
  11. package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
  12. package/src/lib/components/console/order-detail-dialog.component.js +9 -2
  13. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  14. package/src/lib/components/console/order-invoice.component.d.ts +9 -0
  15. package/src/lib/components/console/order-invoice.component.js +27 -2
  16. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  17. package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
  18. package/src/lib/components/console/quote-editor-dialog.component.js +102 -4
  19. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
  20. package/src/lib/components/console/quote-proofs.component.d.ts +11 -0
  21. package/src/lib/components/console/quote-proofs.component.js +68 -0
  22. package/src/lib/components/console/quote-proofs.component.js.map +1 -1
  23. package/src/lib/components/console/quote-send.component.d.ts +42 -0
  24. package/src/lib/components/console/quote-send.component.js +307 -0
  25. package/src/lib/components/console/quote-send.component.js.map +1 -0
  26. package/src/lib/components/console/quote-start.component.d.ts +45 -0
  27. package/src/lib/components/console/quote-start.component.js +182 -0
  28. package/src/lib/components/console/quote-start.component.js.map +1 -0
  29. package/src/lib/components/product-options-field-body.d.ts +44 -0
  30. package/src/lib/components/product-options-field-body.js +260 -0
  31. package/src/lib/components/product-options-field-body.js.map +1 -0
  32. package/src/lib/components/product-options-field.d.ts +32 -0
  33. package/src/lib/components/product-options-field.js +128 -0
  34. package/src/lib/components/product-options-field.js.map +1 -0
  35. package/src/lib/components/quote-page-body.js +39 -20
  36. package/src/lib/components/quote-page-body.js.map +1 -1
  37. package/src/lib/model/commerce-invoices.d.ts +132 -0
  38. package/src/lib/model/commerce-invoices.js +332 -0
  39. package/src/lib/model/commerce-invoices.js.map +1 -0
  40. package/src/lib/model/commerce-orders.d.ts +29 -2
  41. package/src/lib/model/commerce-orders.js.map +1 -1
  42. package/src/lib/model/commerce-product-field.d.ts +23 -0
  43. package/src/lib/model/commerce-product-field.js +26 -0
  44. package/src/lib/model/commerce-product-field.js.map +1 -0
  45. package/src/lib/model/commerce-quote-email.d.ts +67 -0
  46. package/src/lib/model/commerce-quote-email.js +154 -0
  47. package/src/lib/model/commerce-quote-email.js.map +1 -0
  48. package/src/lib/model/commerce-quote-pay.d.ts +49 -6
  49. package/src/lib/model/commerce-quote-pay.js +69 -36
  50. package/src/lib/model/commerce-quote-pay.js.map +1 -1
  51. package/src/lib/model/commerce-quote-request.d.ts +93 -0
  52. package/src/lib/model/commerce-quote-request.js +207 -0
  53. package/src/lib/model/commerce-quote-request.js.map +1 -0
  54. package/src/lib/model/commerce-quotes.d.ts +81 -0
  55. package/src/lib/model/commerce-quotes.js.map +1 -1
  56. package/src/lib/model/index.d.ts +3 -0
  57. package/src/lib/model/index.js +3 -0
  58. package/src/lib/model/index.js.map +1 -1
  59. package/src/lib/plugin.js +19 -0
  60. package/src/lib/plugin.js.map +1 -1
  61. package/src/lib/server/billing-webhook.js +115 -70
  62. package/src/lib/server/billing-webhook.js.map +1 -1
  63. package/src/lib/server/commerce-pdf.d.ts +5 -1
  64. package/src/lib/server/commerce-pdf.js +37 -10
  65. package/src/lib/server/commerce-pdf.js.map +1 -1
  66. package/src/lib/server/draft-order.js +23 -3
  67. package/src/lib/server/draft-order.js.map +1 -1
  68. package/src/lib/server/invoice-email.d.ts +76 -0
  69. package/src/lib/server/invoice-email.js +183 -0
  70. package/src/lib/server/invoice-email.js.map +1 -0
  71. package/src/lib/server/order-invoice-send.d.ts +43 -0
  72. package/src/lib/server/order-invoice-send.js +218 -0
  73. package/src/lib/server/order-invoice-send.js.map +1 -0
  74. package/src/lib/server/order-status.d.ts +11 -0
  75. package/src/lib/server/order-status.js +44 -8
  76. package/src/lib/server/order-status.js.map +1 -1
  77. package/src/lib/server/person-eraser.d.ts +3 -2
  78. package/src/lib/server/person-eraser.js +25 -6
  79. package/src/lib/server/person-eraser.js.map +1 -1
  80. package/src/lib/server/quote-email.d.ts +85 -0
  81. package/src/lib/server/quote-email.js +211 -0
  82. package/src/lib/server/quote-email.js.map +1 -0
  83. package/src/lib/server/quote-link-token.d.ts +11 -0
  84. package/src/lib/server/quote-link-token.js +21 -0
  85. package/src/lib/server/quote-link-token.js.map +1 -1
  86. package/src/lib/server/quote-pay.d.ts +8 -3
  87. package/src/lib/server/quote-pay.js +105 -43
  88. package/src/lib/server/quote-pay.js.map +1 -1
  89. package/src/lib/server/quote.d.ts +11 -0
  90. package/src/lib/server/quote.js +309 -12
  91. package/src/lib/server/quote.js.map +1 -1
  92. package/src/lib/server/site-page-resolver.js +2 -1
  93. package/src/lib/server/site-page-resolver.js.map +1 -1
  94. package/src/lib/server.js +5 -0
  95. package/src/lib/server.js.map +1 -1
  96. package/src/lib/site.js +8 -0
  97. package/src/lib/site.js.map +1 -1
  98. package/src/lib/tenant-emails.js +243 -0
  99. package/src/lib/tenant-emails.js.map +1 -1
  100. package/src/lib/utils/commerce-document-pdf.d.ts +5 -0
  101. package/src/lib/utils/commerce-document-pdf.js +6 -1
  102. package/src/lib/utils/commerce-document-pdf.js.map +1 -1
  103. package/src/lib/utils/invoice-email-content.d.ts +45 -0
  104. package/src/lib/utils/invoice-email-content.js +126 -0
  105. package/src/lib/utils/invoice-email-content.js.map +1 -0
  106. package/src/lib/utils/order-invoice.d.ts +1 -1
  107. package/src/lib/utils/order-invoice.js +14 -6
  108. package/src/lib/utils/order-invoice.js.map +1 -1
  109. package/src/lib/utils/quote-email-content.d.ts +62 -0
  110. package/src/lib/utils/quote-email-content.js +154 -0
  111. package/src/lib/utils/quote-email-content.js.map +1 -0
@@ -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(String(section?.customer?.email ?? ''))\n setSubject(CommerceModel.quoteEmailDefaultSubject(quote, businessName, quoteId))\n setMessage('')\n setOpen(true)\n }, [section, 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","customer","email","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,44 @@
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
+ import type { ProductOptionsFieldProps } from './product-options-field';
18
+ /** A product as its public page reads it: its options and the variants they make. */
19
+ interface ProductDetail {
20
+ id: string;
21
+ name: string;
22
+ options: Array<{
23
+ name: string;
24
+ values: string[];
25
+ }>;
26
+ variants: Array<{
27
+ id: string;
28
+ options?: Record<string, string>;
29
+ }>;
30
+ }
31
+ /** The variant whose options are exactly the ones picked, else none. */
32
+ export declare function variantForOptions(detail: Pick<ProductDetail, 'variants' | 'options'>, picked: Readonly<Record<string, string>>): string | null;
33
+ /**
34
+ * The Product and options field's body (AGL-4098): a picker of the store's
35
+ * products (or the one the author chose), then one picker per option. It
36
+ * posts the readable line under its name and the exact choice beside it; a
37
+ * real, invisible input carries the line, so a required field with nothing
38
+ * picked stops the form like any required field.
39
+ *
40
+ * On the canvas and in Preview it draws with the store's products but sends
41
+ * nothing, like every form field there.
42
+ */
43
+ declare const ProductOptionsFieldBody: import("react").ForwardRefExoticComponent<ProductOptionsFieldProps & import("react").RefAttributes<HTMLDivElement>>;
44
+ export default ProductOptionsFieldBody;
@@ -0,0 +1,260 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
+ /**
5
+ * @license
6
+ * Copyright 2026 Aglyn LLC
7
+ *
8
+ * Licensed under the Apache License, Version 2.0 (the "License");
9
+ * you may not use this file except in compliance with the License.
10
+ * You may obtain a copy of the License at
11
+ *
12
+ * http://www.apache.org/licenses/LICENSE-2.0
13
+ *
14
+ * Unless required by applicable law or agreed to in writing, software
15
+ * distributed under the License is distributed on an "AS IS" BASIS,
16
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ * See the License for the specific language governing permissions and
18
+ * limitations under the License.
19
+ */ import * as Aglyn from "@aglyn/aglyn";
20
+ import { formFieldDataKey } from "../model/commerce-product-field.js";
21
+ import { encodeProductFieldValue, productFieldLabel } from "../model/commerce-quote-request.js";
22
+ import Box from "@mui/material/Box";
23
+ import FormControl from "@mui/material/FormControl";
24
+ import FormHelperText from "@mui/material/FormHelperText";
25
+ import FormLabel from "@mui/material/FormLabel";
26
+ import MenuItem from "@mui/material/MenuItem";
27
+ import Stack from "@mui/material/Stack";
28
+ import TextField from "@mui/material/TextField";
29
+ import { forwardRef, useEffect, useId, useMemo, useRef, useState } from "react";
30
+ /** The most products the picker lists. */ const PICKER_LIMIT = 100;
31
+ /** The variant whose options are exactly the ones picked, else none. */ export function variantForOptions(detail, picked) {
32
+ var _ref, _ref1;
33
+ var _detail_variants_;
34
+ if (!detail.options.length) return (_ref = (_detail_variants_ = detail.variants[0]) == null ? void 0 : _detail_variants_.id) != null ? _ref : null;
35
+ if (detail.options.some((option)=>!picked[option.name])) return null;
36
+ const match = detail.variants.find((variant)=>detail.options.every((option)=>{
37
+ var _variant_options;
38
+ return ((_variant_options = variant.options) == null ? void 0 : _variant_options[option.name]) === picked[option.name];
39
+ }));
40
+ return (_ref1 = match == null ? void 0 : match.id) != null ? _ref1 : null;
41
+ }
42
+ /**
43
+ * The Product and options field's body (AGL-4098): a picker of the store's
44
+ * products (or the one the author chose), then one picker per option. It
45
+ * posts the readable line under its name and the exact choice beside it; a
46
+ * real, invisible input carries the line, so a required field with nothing
47
+ * picked stops the form like any required field.
48
+ *
49
+ * On the canvas and in Preview it draws with the store's products but sends
50
+ * nothing, like every form field there.
51
+ */ const ProductOptionsFieldBody = /*#__PURE__*/ forwardRef((props, ref)=>{
52
+ var _ref, _ref1;
53
+ var _products_find;
54
+ const { name: rawName, label, productId: fixedProductId, required } = props, rest = _object_without_properties_loose(props, [
55
+ "name",
56
+ "label",
57
+ "productId",
58
+ "required"
59
+ ]);
60
+ const name = String(rawName != null ? rawName : '').trim() || 'product';
61
+ const { hostId } = Aglyn.useSite();
62
+ const inputId = useId();
63
+ const answerRef = useRef(null);
64
+ const [products, setProducts] = useState(null);
65
+ const [productId, setProductId] = useState(fixedProductId != null ? fixedProductId : '');
66
+ const [detail, setDetail] = useState(null);
67
+ const [picked, setPicked] = useState({});
68
+ // The store's products, or the one the author chose.
69
+ useEffect(()=>{
70
+ if (!hostId) return;
71
+ let active = true;
72
+ const query = new URLSearchParams(_extends({
73
+ hostId
74
+ }, fixedProductId ? {
75
+ ids: fixedProductId
76
+ } : {
77
+ sort: 'name',
78
+ limit: String(PICKER_LIMIT)
79
+ }));
80
+ void fetch(`/api/commerce/catalog?${query.toString()}`).then((response)=>response.ok ? response.json() : {
81
+ items: []
82
+ }).then((payload)=>{
83
+ var _ref;
84
+ var _items_;
85
+ if (!active) return;
86
+ const items = Array.isArray(payload == null ? void 0 : payload.items) ? payload.items : [];
87
+ setProducts(items);
88
+ if (fixedProductId) setProductId((_ref = (_items_ = items[0]) == null ? void 0 : _items_.id) != null ? _ref : '');
89
+ }).catch(()=>active && setProducts([]));
90
+ return ()=>{
91
+ active = false;
92
+ };
93
+ }, [
94
+ hostId,
95
+ fixedProductId
96
+ ]);
97
+ // The chosen product's options.
98
+ const slug = (_ref = products == null ? void 0 : (_products_find = products.find((product)=>product.id === productId)) == null ? void 0 : _products_find.slug) != null ? _ref : '';
99
+ useEffect(()=>{
100
+ setDetail(null);
101
+ setPicked({});
102
+ if (!hostId || !slug) return;
103
+ let active = true;
104
+ void fetch(`/api/commerce/product?hostId=${encodeURIComponent(hostId)}&slug=${encodeURIComponent(slug)}`).then((response)=>response.ok ? response.json() : null).then((payload)=>{
105
+ var _product_options, _product_variants;
106
+ if (!active || !(payload == null ? void 0 : payload.product)) return;
107
+ const product = payload.product;
108
+ setDetail(_extends({}, product, {
109
+ options: (_product_options = product.options) != null ? _product_options : [],
110
+ variants: (_product_variants = product.variants) != null ? _product_variants : []
111
+ }));
112
+ }).catch(()=>undefined);
113
+ return ()=>{
114
+ active = false;
115
+ };
116
+ }, [
117
+ hostId,
118
+ slug
119
+ ]);
120
+ const variantId = detail ? variantForOptions(detail, picked) : null;
121
+ const complete = Boolean(detail && variantId);
122
+ const answer = complete && detail ? productFieldLabel(detail.name, picked) : '';
123
+ const value = complete && detail ? encodeProductFieldValue(_extends({
124
+ productId: detail.id
125
+ }, variantId && variantId !== 'default' ? {
126
+ variantId
127
+ } : {}, Object.keys(picked).length ? {
128
+ options: picked
129
+ } : {})) : '';
130
+ // Every change is announced, as a typed answer is, to anything listening on the form.
131
+ const announced = useRef(answer);
132
+ useEffect(()=>{
133
+ var _answerRef_current;
134
+ if (announced.current === answer) return;
135
+ announced.current = answer;
136
+ (_answerRef_current = answerRef.current) == null ? void 0 : _answerRef_current.dispatchEvent(new Event('change', {
137
+ bubbles: true
138
+ }));
139
+ }, [
140
+ answer
141
+ ]);
142
+ const listed = useMemo(()=>products != null ? products : [], [
143
+ products
144
+ ]);
145
+ // On the canvas there is no site to list from: it says what the visitor will pick from.
146
+ const placeholder = !hostId ? 'Your store’s products' : products === null ? 'Loading products…' : listed.length ? 'Choose a product' : 'No products to choose from yet';
147
+ return /*#__PURE__*/ _jsxs(FormControl, _extends({
148
+ ref: ref,
149
+ fullWidth: true,
150
+ required: Boolean(required)
151
+ }, rest, {
152
+ children: [
153
+ /*#__PURE__*/ _jsx(FormLabel, {
154
+ htmlFor: inputId,
155
+ children: label || 'Product'
156
+ }),
157
+ /*#__PURE__*/ _jsxs(Stack, {
158
+ spacing: 1.5,
159
+ sx: {
160
+ mt: 1,
161
+ position: 'relative'
162
+ },
163
+ children: [
164
+ fixedProductId ? null : /*#__PURE__*/ _jsx(TextField, {
165
+ id: inputId,
166
+ select: true,
167
+ size: "small",
168
+ fullWidth: true,
169
+ value: productId,
170
+ onChange: (event)=>setProductId(event.target.value),
171
+ disabled: !listed.length,
172
+ slotProps: {
173
+ select: {
174
+ displayEmpty: true,
175
+ renderValue: (selected)=>{
176
+ var _ref;
177
+ var _listed_find;
178
+ return (_ref = (_listed_find = listed.find((product)=>product.id === selected)) == null ? void 0 : _listed_find.name) != null ? _ref : /*#__PURE__*/ _jsx(Box, {
179
+ component: "span",
180
+ sx: {
181
+ color: 'text.secondary'
182
+ },
183
+ children: placeholder
184
+ });
185
+ }
186
+ }
187
+ },
188
+ children: listed.map((product)=>/*#__PURE__*/ _jsx(MenuItem, {
189
+ value: product.id,
190
+ children: product.name
191
+ }, product.id))
192
+ }),
193
+ fixedProductId && detail ? /*#__PURE__*/ _jsx(FormHelperText, {
194
+ sx: {
195
+ mx: 0
196
+ },
197
+ children: detail.name
198
+ }) : null,
199
+ ((_ref1 = detail == null ? void 0 : detail.options) != null ? _ref1 : []).map((option)=>{
200
+ var _picked_option_name;
201
+ return /*#__PURE__*/ _jsx(TextField, {
202
+ select: true,
203
+ size: "small",
204
+ fullWidth: true,
205
+ label: option.name,
206
+ value: (_picked_option_name = picked[option.name]) != null ? _picked_option_name : '',
207
+ onChange: (event)=>setPicked((current)=>_extends({}, current, {
208
+ [option.name]: event.target.value
209
+ })),
210
+ children: option.values.map((choice)=>/*#__PURE__*/ _jsx(MenuItem, {
211
+ value: choice,
212
+ children: choice
213
+ }, choice))
214
+ }, option.name);
215
+ }),
216
+ detail && Object.keys(picked).length === detail.options.length && detail.options.length && !variantId ? /*#__PURE__*/ _jsx(FormHelperText, {
217
+ error: true,
218
+ sx: {
219
+ mx: 0
220
+ },
221
+ children: 'That combination isn’t one we offer. Pick another.'
222
+ }) : null,
223
+ /*#__PURE__*/ _jsx(Box, {
224
+ component: "input",
225
+ ref: answerRef,
226
+ type: "text",
227
+ name: name,
228
+ value: answer,
229
+ required: Boolean(required),
230
+ onChange: ()=>undefined,
231
+ tabIndex: -1,
232
+ "aria-hidden": "true",
233
+ autoComplete: "off",
234
+ sx: {
235
+ position: 'absolute',
236
+ left: 0,
237
+ bottom: 0,
238
+ width: 1,
239
+ height: '1px',
240
+ p: 0,
241
+ border: 0,
242
+ opacity: 0,
243
+ pointerEvents: 'none'
244
+ }
245
+ }),
246
+ value ? /*#__PURE__*/ _jsx("input", {
247
+ type: "hidden",
248
+ name: formFieldDataKey(name),
249
+ value: value,
250
+ readOnly: true
251
+ }) : null
252
+ ]
253
+ })
254
+ ]
255
+ }));
256
+ });
257
+ ProductOptionsFieldBody.displayName = 'AglynProductOptionsFieldBody';
258
+ export default ProductOptionsFieldBody;
259
+
260
+ //# sourceMappingURL=product-options-field-body.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/product-options-field-body.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\nimport * as Aglyn from '@aglyn/aglyn'\nimport { formFieldDataKey } from '../model/commerce-product-field'\nimport { encodeProductFieldValue, productFieldLabel } from '../model/commerce-quote-request'\nimport Box from '@mui/material/Box'\nimport FormControl from '@mui/material/FormControl'\nimport FormHelperText from '@mui/material/FormHelperText'\nimport FormLabel from '@mui/material/FormLabel'\nimport MenuItem from '@mui/material/MenuItem'\nimport Stack from '@mui/material/Stack'\nimport TextField from '@mui/material/TextField'\nimport { forwardRef, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport type { ProductOptionsFieldProps } from './product-options-field'\n\n/** A product as the store's public catalog lists it. */\ninterface ListedProduct {\n id: string\n name: string\n slug: string\n}\n\n/** A product as its public page reads it: its options and the variants they make. */\ninterface ProductDetail {\n id: string\n name: string\n options: Array<{ name: string; values: string[] }>\n variants: Array<{ id: string; options?: Record<string, string> }>\n}\n\n/** The most products the picker lists. */\nconst PICKER_LIMIT = 100\n\n/** The variant whose options are exactly the ones picked, else none. */\nexport function variantForOptions(\n detail: Pick<ProductDetail, 'variants' | 'options'>,\n picked: Readonly<Record<string, string>>,\n): string | null {\n if (!detail.options.length) return detail.variants[0]?.id ?? null\n if (detail.options.some((option) => !picked[option.name])) return null\n const match = detail.variants.find((variant) =>\n detail.options.every((option) => variant.options?.[option.name] === picked[option.name]),\n )\n return match?.id ?? null\n}\n\n/**\n * The Product and options field's body (AGL-4098): a picker of the store's\n * products (or the one the author chose), then one picker per option. It\n * posts the readable line under its name and the exact choice beside it; a\n * real, invisible input carries the line, so a required field with nothing\n * picked stops the form like any required field.\n *\n * On the canvas and in Preview it draws with the store's products but sends\n * nothing, like every form field there.\n */\nconst ProductOptionsFieldBody = forwardRef<HTMLDivElement, ProductOptionsFieldProps>((props, ref) => {\n const { name: rawName, label, productId: fixedProductId, required, ...rest } = props\n const name = String(rawName ?? '').trim() || 'product'\n const { hostId } = Aglyn.useSite()\n const inputId = useId()\n const answerRef = useRef<HTMLInputElement>(null)\n const [products, setProducts] = useState<ListedProduct[] | null>(null)\n const [productId, setProductId] = useState(fixedProductId ?? '')\n const [detail, setDetail] = useState<ProductDetail | null>(null)\n const [picked, setPicked] = useState<Record<string, string>>({})\n\n // The store's products, or the one the author chose.\n useEffect(() => {\n if (!hostId) return\n let active = true\n const query = new URLSearchParams({\n hostId,\n ...(fixedProductId ? { ids: fixedProductId } : { sort: 'name', limit: String(PICKER_LIMIT) }),\n })\n void fetch(`/api/commerce/catalog?${query.toString()}`)\n .then((response) => (response.ok ? response.json() : { items: [] }))\n .then((payload) => {\n if (!active) return\n const items = Array.isArray(payload?.items) ? (payload.items as ListedProduct[]) : []\n setProducts(items)\n if (fixedProductId) setProductId(items[0]?.id ?? '')\n })\n .catch(() => active && setProducts([]))\n return () => {\n active = false\n }\n }, [hostId, fixedProductId])\n\n // The chosen product's options.\n const slug = products?.find((product) => product.id === productId)?.slug ?? ''\n useEffect(() => {\n setDetail(null)\n setPicked({})\n if (!hostId || !slug) return\n let active = true\n void fetch(`/api/commerce/product?hostId=${encodeURIComponent(hostId)}&slug=${encodeURIComponent(slug)}`)\n .then((response) => (response.ok ? response.json() : null))\n .then((payload) => {\n if (!active || !payload?.product) return\n const product = payload.product as ProductDetail\n setDetail({ ...product, options: product.options ?? [], variants: product.variants ?? [] })\n })\n .catch(() => undefined)\n return () => {\n active = false\n }\n }, [hostId, slug])\n\n const variantId = detail ? variantForOptions(detail, picked) : null\n const complete = Boolean(detail && variantId)\n const answer = complete && detail ? productFieldLabel(detail.name, picked) : ''\n const value = complete && detail\n ? encodeProductFieldValue({\n productId: detail.id,\n ...(variantId && variantId !== 'default' ? { variantId } : {}),\n ...(Object.keys(picked).length ? { options: picked } : {}),\n // Add-ons are chosen on the storefront from slice 8 on: they ride here as `modifiers`.\n })\n : ''\n\n // Every change is announced, as a typed answer is, to anything listening on the form.\n const announced = useRef(answer)\n useEffect(() => {\n if (announced.current === answer) return\n announced.current = answer\n answerRef.current?.dispatchEvent(new Event('change', { bubbles: true }))\n }, [answer])\n\n const listed = useMemo(() => products ?? [], [products])\n // On the canvas there is no site to list from: it says what the visitor will pick from.\n const placeholder = !hostId\n ? 'Your store’s products'\n : products === null\n ? 'Loading products…'\n : listed.length\n ? 'Choose a product'\n : 'No products to choose from yet'\n\n return (\n <FormControl ref={ref} fullWidth required={Boolean(required)} {...rest}>\n <FormLabel htmlFor={inputId}>{label || 'Product'}</FormLabel>\n <Stack spacing={1.5} sx={{ mt: 1, position: 'relative' }}>\n {fixedProductId ? null : (\n <TextField\n id={inputId}\n select\n size=\"small\"\n fullWidth\n value={productId}\n onChange={(event) => setProductId(event.target.value)}\n disabled={!listed.length}\n slotProps={{\n select: {\n displayEmpty: true,\n renderValue: (selected: unknown) =>\n listed.find((product) => product.id === selected)?.name ?? (\n <Box component=\"span\" sx={{ color: 'text.secondary' }}>\n {placeholder}\n </Box>\n ),\n },\n }}\n >\n {listed.map((product) => (\n <MenuItem key={product.id} value={product.id}>\n {product.name}\n </MenuItem>\n ))}\n </TextField>\n )}\n {fixedProductId && detail ? <FormHelperText sx={{ mx: 0 }}>{detail.name}</FormHelperText> : null}\n {(detail?.options ?? []).map((option) => (\n <TextField\n key={option.name}\n select\n size=\"small\"\n fullWidth\n label={option.name}\n value={picked[option.name] ?? ''}\n onChange={(event) => setPicked((current) => ({ ...current, [option.name]: event.target.value }))}\n >\n {option.values.map((choice) => (\n <MenuItem key={choice} value={choice}>\n {choice}\n </MenuItem>\n ))}\n </TextField>\n ))}\n {detail && Object.keys(picked).length === detail.options.length && detail.options.length && !variantId ? (\n <FormHelperText error sx={{ mx: 0 }}>\n {'That combination isn’t one we offer. Pick another.'}\n </FormHelperText>\n ) : null}\n {/* The answer the form sends and the browser checks, and the exact choice beside it. */}\n <Box\n component=\"input\"\n ref={answerRef}\n type=\"text\"\n name={name}\n value={answer}\n required={Boolean(required)}\n onChange={() => undefined}\n tabIndex={-1}\n aria-hidden=\"true\"\n autoComplete=\"off\"\n sx={{ position: 'absolute', left: 0, bottom: 0, width: 1, height: '1px', p: 0, border: 0, opacity: 0, pointerEvents: 'none' }}\n />\n {value ? <input type=\"hidden\" name={formFieldDataKey(name)} value={value} readOnly /> : null}\n </Stack>\n </FormControl>\n )\n})\nProductOptionsFieldBody.displayName = 'AglynProductOptionsFieldBody'\n\nexport default ProductOptionsFieldBody\n"],"names":["Aglyn","formFieldDataKey","encodeProductFieldValue","productFieldLabel","Box","FormControl","FormHelperText","FormLabel","MenuItem","Stack","TextField","forwardRef","useEffect","useId","useMemo","useRef","useState","PICKER_LIMIT","variantForOptions","detail","picked","options","length","variants","id","some","option","name","match","find","variant","every","ProductOptionsFieldBody","props","ref","products","rawName","label","productId","fixedProductId","required","rest","String","trim","hostId","useSite","inputId","answerRef","setProducts","setProductId","setDetail","setPicked","active","query","URLSearchParams","ids","sort","limit","fetch","toString","then","response","ok","json","items","payload","Array","isArray","catch","slug","product","encodeURIComponent","undefined","variantId","complete","Boolean","answer","value","Object","keys","announced","current","dispatchEvent","Event","bubbles","listed","placeholder","fullWidth","htmlFor","spacing","sx","mt","position","select","size","onChange","event","target","disabled","slotProps","displayEmpty","renderValue","selected","component","color","map","mx","values","choice","error","type","tabIndex","aria-hidden","autoComplete","left","bottom","width","height","p","border","opacity","pointerEvents","input","readOnly","displayName"],"mappings":"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"}