@aglyn/plugins-commerce 1.0.0-beta.291 → 1.0.0-beta.293
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 +1 -0
- 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/console/host-quotes-card.component.js +11 -9
- package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
- package/src/lib/components/console/invoice-send.component.js +1 -1
- package/src/lib/components/console/invoice-send.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-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-send.component.js +1 -4
- package/src/lib/components/console/quote-send.component.js.map +1 -1
- 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/declarations.console-server.js +15 -0
- package/src/lib/declarations.console-server.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 +7 -0
- package/src/lib/model/commerce-invoices.js +11 -0
- package/src/lib/model/commerce-invoices.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +6 -3
- 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-quote-email.d.ts +14 -0
- package/src/lib/model/commerce-quote-email.js +25 -9
- package/src/lib/model/commerce-quote-email.js.map +1 -1
- package/src/lib/model/commerce-quote-proofs.d.ts +7 -0
- package/src/lib/model/commerce-quote-proofs.js +15 -0
- package/src/lib/model/commerce-quote-proofs.js.map +1 -1
- 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/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/server/billing-webhook.js +64 -43
- 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/local-fulfillment.js +5 -1
- package/src/lib/server/local-fulfillment.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.js.map +1 -1
- package/src/lib/server/quote-link-token.d.ts +4 -2
- package/src/lib/server/quote-link-token.js +4 -2
- package/src/lib/server/quote-link-token.js.map +1 -1
- package/src/lib/server/quote-pay.d.ts +5 -1
- package/src/lib/server/quote-pay.js +5 -2
- package/src/lib/server/quote-pay.js.map +1 -1
- package/src/lib/server/quote-proofs.d.ts +9 -1
- package/src/lib/server/quote-proofs.js +92 -23
- package/src/lib/server/quote-proofs.js.map +1 -1
- package/src/lib/server/quote-upload-keys.d.ts +50 -0
- package/src/lib/server/quote-upload-keys.js +86 -0
- package/src/lib/server/quote-upload-keys.js.map +1 -0
- package/src/lib/server/quote-upload-sweep.d.ts +30 -0
- package/src/lib/server/quote-upload-sweep.js +117 -0
- package/src/lib/server/quote-upload-sweep.js.map +1 -0
- 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/quote-email-content.d.ts +1 -0
- package/src/lib/utils/quote-email-content.js +7 -4
- package/src/lib/utils/quote-email-content.js.map +1 -1
|
@@ -26,7 +26,8 @@ import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component"
|
|
|
26
26
|
import { ceilingedWindow, collectionCeiling, useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
|
|
27
27
|
import { Button, Chip, Stack, Typography } from "@mui/material";
|
|
28
28
|
import { collection, limit, query, where } from "firebase/firestore";
|
|
29
|
-
import {
|
|
29
|
+
import { useSearchParams } from "next/navigation";
|
|
30
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
30
31
|
import QuoteEditorDialog from "./quote-editor-dialog.component.js";
|
|
31
32
|
/**
|
|
32
33
|
* How many quotes the list reads. The whole set is read and the grid sorts
|
|
@@ -58,7 +59,7 @@ const REFUSAL = commerceSitePermissionRefusal('commerce.quotes');
|
|
|
58
59
|
* this reads `hosts/{hostId}/orders` for that channel alone; the Orders list
|
|
59
60
|
* above never shows one until it is paid.
|
|
60
61
|
*/ export function HostQuotesCard(props) {
|
|
61
|
-
var _quotes_rows_find;
|
|
62
|
+
var _quotes_rows_find, _ref;
|
|
62
63
|
const { hostId, canEdit, canSend } = props;
|
|
63
64
|
const firestore = useFirestore();
|
|
64
65
|
const { data: quoteDocs, status } = useFirestoreCollection(()=>query(collection(firestore, 'hosts', hostId, 'orders'), where('channel', '==', 'quote'), limit(QUOTE_CEILING + 1)), [
|
|
@@ -85,15 +86,16 @@ const REFUSAL = commerceSitePermissionRefusal('commerce.quotes');
|
|
|
85
86
|
/** The quote open in the editor: its id, or `new`. */ const [openId, setOpenId] = useState(null);
|
|
86
87
|
const openQuote = openId && openId !== 'new' ? (_quotes_rows_find = quotes.rows.find((row)=>row.$id === openId)) != null ? _quotes_rows_find : null : null;
|
|
87
88
|
// A manager notice opens its quote (AGL-4096): `?quote=<id>` on the Orders
|
|
88
|
-
// page, once that quote has loaded.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
// page, once that quote has loaded. Read from the router, so a notice
|
|
90
|
+
// followed while this page is already open opens its quote too; once per
|
|
91
|
+
// id, so closing the dialog does not reopen it.
|
|
92
|
+
const searchParams = useSearchParams();
|
|
93
|
+
const linkedId = (_ref = searchParams == null ? void 0 : searchParams.get('quote')) != null ? _ref : '';
|
|
94
|
+
const opened = useRef('');
|
|
93
95
|
useEffect(()=>{
|
|
94
|
-
if (!linkedId || !quotes.rows.some((row)=>row.$id === linkedId)) return;
|
|
96
|
+
if (!linkedId || opened.current === linkedId || !quotes.rows.some((row)=>row.$id === linkedId)) return;
|
|
97
|
+
opened.current = linkedId;
|
|
95
98
|
setOpenId(linkedId);
|
|
96
|
-
setLinkedId('');
|
|
97
99
|
}, [
|
|
98
100
|
linkedId,
|
|
99
101
|
quotes.rows
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/host-quotes-card.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 { posMoney } from '../../model/commerce-pos-ops'\nimport { commerceSitePermissionRefusal } from '../../model/plugin-permissions'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { BlockedControl } from '@aglyn/shared-ui-jsx/components/blocked-control.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, limit, query, where } from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport QuoteEditorDialog, { type QuoteRow } from './quote-editor-dialog.component'\n\nexport interface HostQuotesCardProps {\n hostId: string\n /** Holds \"Create and edit quotes\" (`commerce.quotes`). */\n canEdit: boolean\n /** Holds \"Send quotes and invoices to customers\" (`commerce.quotes.send`). */\n canSend: boolean\n}\n\n/**\n * How many quotes the list reads. The whole set is read and the grid sorts\n * it by any column; a store past this many is told the list stops short.\n */\nconst QUOTE_CEILING = 500\n\n/** The products the editor's picker offers. */\nconst PRODUCT_WINDOW = 200\n\nconst REFUSAL = commerceSitePermissionRefusal('commerce.quotes')\n\ninterface QuoteListRow extends QuoteRow {\n customerLabel: string\n firstItem: string\n moreItems: number\n statusKey: ReturnType<typeof CommerceModel.quoteDisplayStatus>['key']\n statusLabel: string\n totalCents: number\n updatedAtMs: number\n}\n\n/** One quote as its row draws and sorts it. */\nfunction quoteListRow(quote: QuoteRow): QuoteListRow {\n const customer = quote.quote?.customer\n const status = CommerceModel.quoteDisplayStatus(quote)\n const items = quote.lineItems ?? []\n return {\n ...quote,\n customerLabel: customer?.name || customer?.company || customer?.email || '',\n firstItem: items[0]?.name ?? '',\n moreItems: Math.max(0, items.length - 1),\n statusKey: status.key,\n statusLabel: status.label,\n totalCents: Number(quote.totals?.totalCents ?? 0),\n // The customer's latest answer counts as an update (AGL-4096), so a quote\n // they just approved or asked changes to sorts to the top.\n updatedAtMs: Math.max(\n Number(quote.quote?.updatedAtMs ?? 0),\n Number(CommerceModel.latestQuoteResponse(quote.quote)?.atMs ?? 0),\n ),\n }\n}\n\n/**\n * Quotes (AGL-4093): prices put to a customer before they buy, beside the\n * orders they become. A quote is an unpaid order on the `quote` channel, so\n * this reads `hosts/{hostId}/orders` for that channel alone; the Orders list\n * above never shows one until it is paid.\n */\nexport function HostQuotesCard(props: HostQuotesCardProps) {\n const { hostId, canEdit, canSend } = props\n const firestore = useFirestore()\n const { data: quoteDocs, status } = useFirestoreCollection<QuoteRow>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'orders'),\n where('channel', '==', 'quote'),\n limit(QUOTE_CEILING + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const quotes = useMemo(() => ceilingedWindow<QuoteRow>(quoteDocs ?? undefined, QUOTE_CEILING), [quoteDocs])\n const rows = useMemo(() => Array.from(quotes.rows, quoteListRow), [quotes])\n\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 () => ceilingedWindow<Record<string, any>>(productDocs ?? undefined, PRODUCT_WINDOW).rows.filter((product) => !product['deletedAt']),\n [productDocs],\n )\n\n /** The quote open in the editor: its id, or `new`. */\n const [openId, setOpenId] = useState<string | null>(null)\n const openQuote = openId && openId !== 'new' ? (quotes.rows.find((row) => row.$id === openId) ?? null) : null\n\n // A manager notice opens its quote (AGL-4096): `?quote=<id>` on the Orders\n // page, once that quote has loaded.\n const [linkedId, setLinkedId] = useState(() =>\n typeof window === 'undefined' ? '' : (new URLSearchParams(window.location.search).get('quote') ?? ''),\n )\n useEffect(() => {\n if (!linkedId || !quotes.rows.some((row) => row.$id === linkedId)) return\n setOpenId(linkedId)\n setLinkedId('')\n }, [linkedId, quotes.rows])\n\n const columns = useMemo<GridColDef[]>(\n () => [\n {\n field: 'number',\n headerName: 'Quote',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {CommerceModel.formatQuoteNumber(row, row.$id)}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {row.moreItems ? `${row.firstItem} and more` : row.firstItem}\n </Typography>\n </Stack>\n ),\n },\n {\n field: 'customerLabel',\n headerName: 'Customer',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Typography variant=\"body2\" noWrap>\n {row.customerLabel || '—'}\n </Typography>\n ),\n },\n {\n field: 'totalCents',\n headerName: 'Total',\n type: 'number',\n width: 140,\n align: 'right',\n headerAlign: 'right',\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Stack sx={{ alignItems: 'flex-end', minWidth: 0, width: 1 }}>\n <Typography variant=\"body2\">{posMoney(row.totalCents)}</Typography>\n {row.quote?.tax?.kind === 'at-payment' && CommerceModel.isUnpaidQuote(row) ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {'plus tax at payment'}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'statusLabel',\n headerName: 'Status',\n width: 180,\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={row.statusLabel}\n color={CommerceModel.QUOTE_STATUS_COLOR[row.statusKey]}\n />\n ),\n },\n {\n field: 'updatedAtMs',\n headerName: 'Updated',\n type: 'number',\n width: 140,\n align: 'left',\n headerAlign: 'left',\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Typography variant=\"body2\" noWrap>\n {row.updatedAtMs ? new Date(row.updatedAtMs).toLocaleDateString() : '—'}\n </Typography>\n ),\n },\n ],\n [],\n )\n\n const empty = status === 'success' && rows.length === 0\n\n return (\n <CardDisplay\n header={'Quotes'}\n subheader={'Prices for a customer’s order before they buy'}\n help={pluginDocsHelp('quotes', {\n anchor: '#make-a-quote',\n title: 'Quotes',\n excerpt:\n 'Put a price to a customer with your products, items typed for the job and anything ' +\n 'included. A quote becomes an order when it is paid.',\n })}\n HeaderProps={{\n action: (\n <BlockedControl blocked={!canEdit} reason={REFUSAL}>\n <Button size=\"small\" variant=\"contained\" onClick={() => setOpenId('new')}>\n {'New quote'}\n </Button>\n </BlockedControl>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n {empty ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No quotes yet. A quote prices a customer’s order before they buy: your products, ' +\n 'items typed for the job, and anything you include at no charge.'}\n </Typography>\n ) : (\n <Stack spacing={1}>\n {quotes.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'This store has more quotes than the list shows at once.'}\n </Typography>\n ) : null}\n <ListTable\n aria-label=\"Quotes\"\n rows={rows}\n columns={columns}\n loading={status === 'loading'}\n onOpen={(id) => setOpenId(id)}\n quickFilter\n initialState={{ sorting: { sortModel: [{ field: 'updatedAtMs', sort: 'desc' }] } }}\n noRowsLabel=\"No quotes match\"\n />\n </Stack>\n )}\n {/* A quote deleted while open closes its editor rather than turn it into a new one. */}\n {openId === 'new' || openQuote ? (\n <QuoteEditorDialog\n key={openId}\n hostId={hostId}\n quote={openQuote}\n products={products}\n canEdit={canEdit}\n canSend={canSend}\n onClose={() => setOpenId(null)}\n />\n ) : null}\n </CardDisplay>\n )\n}\nHostQuotesCard.displayName = 'HostQuotesCard'\n\nexport default HostQuotesCard\n"],"names":["CommerceModel","posMoney","commerceSitePermissionRefusal","pluginDocsHelp","CardDisplay","BlockedControl","ListTable","ceilingedWindow","collectionCeiling","useFirestore","useFirestoreCollection","Button","Chip","Stack","Typography","collection","limit","query","where","useEffect","useMemo","useState","QuoteEditorDialog","QUOTE_CEILING","PRODUCT_WINDOW","REFUSAL","quoteListRow","quote","items","customer","status","quoteDisplayStatus","lineItems","customerLabel","name","company","email","firstItem","moreItems","Math","max","length","statusKey","key","statusLabel","label","totalCents","Number","totals","updatedAtMs","latestQuoteResponse","atMs","HostQuotesCard","props","quotes","hostId","canEdit","canSend","firestore","data","quoteDocs","idField","undefined","rows","Array","from","productDocs","products","filter","product","openId","setOpenId","openQuote","find","row","$id","linkedId","setLinkedId","window","URLSearchParams","location","search","get","some","columns","field","headerName","flex","minWidth","renderCell","sx","variant","noWrap","formatQuoteNumber","color","type","width","align","headerAlign","alignItems","tax","kind","isUnpaidQuote","size","QUOTE_STATUS_COLOR","Date","toLocaleDateString","empty","header","subheader","help","anchor","title","excerpt","HeaderProps","action","blocked","reason","onClick","contentGutterX","contentGutterY","spacing","truncated","aria-label","loading","onOpen","id","quickFilter","initialState","sorting","sortModel","sort","noRowsLabel","onClose","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/host-quotes-card.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 { posMoney } from '../../model/commerce-pos-ops'\nimport { commerceSitePermissionRefusal } from '../../model/plugin-permissions'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { BlockedControl } from '@aglyn/shared-ui-jsx/components/blocked-control.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, limit, query, where } from 'firebase/firestore'\nimport { useSearchParams } from 'next/navigation'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport QuoteEditorDialog, { type QuoteRow } from './quote-editor-dialog.component'\n\nexport interface HostQuotesCardProps {\n hostId: string\n /** Holds \"Create and edit quotes\" (`commerce.quotes`). */\n canEdit: boolean\n /** Holds \"Send quotes and invoices to customers\" (`commerce.quotes.send`). */\n canSend: boolean\n}\n\n/**\n * How many quotes the list reads. The whole set is read and the grid sorts\n * it by any column; a store past this many is told the list stops short.\n */\nconst QUOTE_CEILING = 500\n\n/** The products the editor's picker offers. */\nconst PRODUCT_WINDOW = 200\n\nconst REFUSAL = commerceSitePermissionRefusal('commerce.quotes')\n\ninterface QuoteListRow extends QuoteRow {\n customerLabel: string\n firstItem: string\n moreItems: number\n statusKey: ReturnType<typeof CommerceModel.quoteDisplayStatus>['key']\n statusLabel: string\n totalCents: number\n updatedAtMs: number\n}\n\n/** One quote as its row draws and sorts it. */\nfunction quoteListRow(quote: QuoteRow): QuoteListRow {\n const customer = quote.quote?.customer\n const status = CommerceModel.quoteDisplayStatus(quote)\n const items = quote.lineItems ?? []\n return {\n ...quote,\n customerLabel: customer?.name || customer?.company || customer?.email || '',\n firstItem: items[0]?.name ?? '',\n moreItems: Math.max(0, items.length - 1),\n statusKey: status.key,\n statusLabel: status.label,\n totalCents: Number(quote.totals?.totalCents ?? 0),\n // The customer's latest answer counts as an update (AGL-4096), so a quote\n // they just approved or asked changes to sorts to the top.\n updatedAtMs: Math.max(\n Number(quote.quote?.updatedAtMs ?? 0),\n Number(CommerceModel.latestQuoteResponse(quote.quote)?.atMs ?? 0),\n ),\n }\n}\n\n/**\n * Quotes (AGL-4093): prices put to a customer before they buy, beside the\n * orders they become. A quote is an unpaid order on the `quote` channel, so\n * this reads `hosts/{hostId}/orders` for that channel alone; the Orders list\n * above never shows one until it is paid.\n */\nexport function HostQuotesCard(props: HostQuotesCardProps) {\n const { hostId, canEdit, canSend } = props\n const firestore = useFirestore()\n const { data: quoteDocs, status } = useFirestoreCollection<QuoteRow>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'orders'),\n where('channel', '==', 'quote'),\n limit(QUOTE_CEILING + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const quotes = useMemo(() => ceilingedWindow<QuoteRow>(quoteDocs ?? undefined, QUOTE_CEILING), [quoteDocs])\n const rows = useMemo(() => Array.from(quotes.rows, quoteListRow), [quotes])\n\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 () => ceilingedWindow<Record<string, any>>(productDocs ?? undefined, PRODUCT_WINDOW).rows.filter((product) => !product['deletedAt']),\n [productDocs],\n )\n\n /** The quote open in the editor: its id, or `new`. */\n const [openId, setOpenId] = useState<string | null>(null)\n const openQuote = openId && openId !== 'new' ? (quotes.rows.find((row) => row.$id === openId) ?? null) : null\n\n // A manager notice opens its quote (AGL-4096): `?quote=<id>` on the Orders\n // page, once that quote has loaded. Read from the router, so a notice\n // followed while this page is already open opens its quote too; once per\n // id, so closing the dialog does not reopen it.\n const searchParams = useSearchParams()\n const linkedId = searchParams?.get('quote') ?? ''\n const opened = useRef('')\n useEffect(() => {\n if (!linkedId || opened.current === linkedId || !quotes.rows.some((row) => row.$id === linkedId)) return\n opened.current = linkedId\n setOpenId(linkedId)\n }, [linkedId, quotes.rows])\n\n const columns = useMemo<GridColDef[]>(\n () => [\n {\n field: 'number',\n headerName: 'Quote',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {CommerceModel.formatQuoteNumber(row, row.$id)}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {row.moreItems ? `${row.firstItem} and more` : row.firstItem}\n </Typography>\n </Stack>\n ),\n },\n {\n field: 'customerLabel',\n headerName: 'Customer',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Typography variant=\"body2\" noWrap>\n {row.customerLabel || '—'}\n </Typography>\n ),\n },\n {\n field: 'totalCents',\n headerName: 'Total',\n type: 'number',\n width: 140,\n align: 'right',\n headerAlign: 'right',\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Stack sx={{ alignItems: 'flex-end', minWidth: 0, width: 1 }}>\n <Typography variant=\"body2\">{posMoney(row.totalCents)}</Typography>\n {row.quote?.tax?.kind === 'at-payment' && CommerceModel.isUnpaidQuote(row) ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {'plus tax at payment'}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'statusLabel',\n headerName: 'Status',\n width: 180,\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={row.statusLabel}\n color={CommerceModel.QUOTE_STATUS_COLOR[row.statusKey]}\n />\n ),\n },\n {\n field: 'updatedAtMs',\n headerName: 'Updated',\n type: 'number',\n width: 140,\n align: 'left',\n headerAlign: 'left',\n renderCell: ({ row }: { row: QuoteListRow }) => (\n <Typography variant=\"body2\" noWrap>\n {row.updatedAtMs ? new Date(row.updatedAtMs).toLocaleDateString() : '—'}\n </Typography>\n ),\n },\n ],\n [],\n )\n\n const empty = status === 'success' && rows.length === 0\n\n return (\n <CardDisplay\n header={'Quotes'}\n subheader={'Prices for a customer’s order before they buy'}\n help={pluginDocsHelp('quotes', {\n anchor: '#make-a-quote',\n title: 'Quotes',\n excerpt:\n 'Put a price to a customer with your products, items typed for the job and anything ' +\n 'included. A quote becomes an order when it is paid.',\n })}\n HeaderProps={{\n action: (\n <BlockedControl blocked={!canEdit} reason={REFUSAL}>\n <Button size=\"small\" variant=\"contained\" onClick={() => setOpenId('new')}>\n {'New quote'}\n </Button>\n </BlockedControl>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n {empty ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No quotes yet. A quote prices a customer’s order before they buy: your products, ' +\n 'items typed for the job, and anything you include at no charge.'}\n </Typography>\n ) : (\n <Stack spacing={1}>\n {quotes.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'This store has more quotes than the list shows at once.'}\n </Typography>\n ) : null}\n <ListTable\n aria-label=\"Quotes\"\n rows={rows}\n columns={columns}\n loading={status === 'loading'}\n onOpen={(id) => setOpenId(id)}\n quickFilter\n initialState={{ sorting: { sortModel: [{ field: 'updatedAtMs', sort: 'desc' }] } }}\n noRowsLabel=\"No quotes match\"\n />\n </Stack>\n )}\n {/* A quote deleted while open closes its editor rather than turn it into a new one. */}\n {openId === 'new' || openQuote ? (\n <QuoteEditorDialog\n key={openId}\n hostId={hostId}\n quote={openQuote}\n products={products}\n canEdit={canEdit}\n canSend={canSend}\n onClose={() => setOpenId(null)}\n />\n ) : null}\n </CardDisplay>\n )\n}\nHostQuotesCard.displayName = 'HostQuotesCard'\n\nexport default HostQuotesCard\n"],"names":["CommerceModel","posMoney","commerceSitePermissionRefusal","pluginDocsHelp","CardDisplay","BlockedControl","ListTable","ceilingedWindow","collectionCeiling","useFirestore","useFirestoreCollection","Button","Chip","Stack","Typography","collection","limit","query","where","useSearchParams","useEffect","useMemo","useRef","useState","QuoteEditorDialog","QUOTE_CEILING","PRODUCT_WINDOW","REFUSAL","quoteListRow","quote","items","customer","status","quoteDisplayStatus","lineItems","customerLabel","name","company","email","firstItem","moreItems","Math","max","length","statusKey","key","statusLabel","label","totalCents","Number","totals","updatedAtMs","latestQuoteResponse","atMs","HostQuotesCard","props","quotes","hostId","canEdit","canSend","firestore","data","quoteDocs","idField","undefined","rows","Array","from","productDocs","products","filter","product","openId","setOpenId","openQuote","find","row","$id","searchParams","linkedId","get","opened","current","some","columns","field","headerName","flex","minWidth","renderCell","sx","variant","noWrap","formatQuoteNumber","color","type","width","align","headerAlign","alignItems","tax","kind","isUnpaidQuote","size","QUOTE_STATUS_COLOR","Date","toLocaleDateString","empty","header","subheader","help","anchor","title","excerpt","HeaderProps","action","blocked","reason","onClick","contentGutterX","contentGutterY","spacing","truncated","aria-label","loading","onOpen","id","quickFilter","initialState","sorting","sortModel","sort","noRowsLabel","onClose","displayName"],"mappings":"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"}
|
|
@@ -69,7 +69,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
69
69
|
useEffect(()=>{
|
|
70
70
|
if (!open) return;
|
|
71
71
|
let live = true;
|
|
72
|
-
setTo(CommerceModel.
|
|
72
|
+
setTo(CommerceModel.invoiceSendPrefill(order));
|
|
73
73
|
setSubject(CommerceModel.invoiceEmailDefaultSubject(order, '', orderId));
|
|
74
74
|
setMessage('');
|
|
75
75
|
void getDoc(doc(firestore, 'hosts', hostId)).then((snapshot)=>{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/invoice-send.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport * as CommerceModel from '../../model'\nimport { siteContactEmail, resolveHostToken, type HostTokenSource } from '@aglyn/aglyn/app-utils/host-tokens'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\n/** `Oct 29, 2026, 3:04 PM`. */\nconst sentAt = (atMs: number) =>\n new Date(atMs).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })\n\n/**\n * The order's (or the quote's) last invoice email, as the console says it:\n * `Invoice sent Oct 29, 2026, 3:04 PM to jordan@example.com.` Null when none\n * was sent.\n */\nexport function invoiceSentLine(order: Pick<CommerceModel.HostOrder, 'invoiceSends'>): string | null {\n const last = CommerceModel.lastInvoiceSend(order)\n if (!last) return null\n return `${last.paid ? 'Paid invoice' : 'Invoice'} sent ${sentAt(Number(last.atMs))} to ${last.to || 'the customer'}.`\n}\n\nexport interface InvoiceSendDialogProps {\n open: boolean\n onClose: () => void\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n}\n\n/**\n * Send invoice (AGL-4099), for an order or a quote: a short form (To,\n * Subject, Message) that emails the invoice in the site's look with the\n * invoice PDF attached. Unpaid, the email carries a Pay button to the order's\n * lasting pay link on the business's site; paid, it is the customer's record.\n * A quote's invoice lands in the quote's conversation, and an order's in its\n * own. The site's name and contact email are read once when the form opens,\n * so the dialog that offers it listens to nothing more.\n */\nexport function InvoiceSendDialog(props: InvoiceSendDialogProps) {\n const { open, onClose, hostId, orderId, order } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const [busy, setBusy] = useState(false)\n const [to, setTo] = useState('')\n const [subject, setSubject] = useState('')\n const [message, setMessage] = useState('')\n const [contact, setContact] = useState<string | null>(null)\n\n /*\n * One attempt per form (AGL-1697's rule): a retried Send presents the same\n * key and is answered from the first, so the customer never gets two; a\n * changed form is a new attempt.\n */\n const attemptKey = useRef('')\n useEffect(() => {\n attemptKey.current = ''\n }, [to, subject, message])\n\n useEffect(() => {\n if (!open) return\n let live = true\n setTo(CommerceModel.invoiceSendRecipient(order, ''))\n setSubject(CommerceModel.invoiceEmailDefaultSubject(order, '', orderId))\n setMessage('')\n void getDoc(doc(firestore, 'hosts', hostId))\n .then((snapshot) => {\n if (!live) return\n const host = (snapshot.data() ?? {}) as Record<string, unknown>\n const businessName = String(resolveHostToken('businessName', host as HostTokenSource) ?? '').trim()\n setSubject(CommerceModel.invoiceEmailDefaultSubject(order, businessName, orderId))\n const listed = siteContactEmail(host as never)\n setContact(listed && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(listed) ? listed : null)\n })\n .catch(() => undefined)\n return () => {\n live = false\n }\n // Filled once per opening; edits after that are the member's.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open])\n\n const paid = CommerceModel.invoiceIsPaid(order)\n const refused = CommerceModel.invoiceSendRefusal(order, orderId, to.trim())\n // The address is the form's to fix; every other refusal is the order's, said up front.\n const blocker = refused && refused !== 'no-address' && refused !== 'bad-address' ? refused : null\n const threaded = Boolean(CommerceModel.invoiceEmailThread(order))\n const number = CommerceModel.formatInvoiceNumber(order, orderId)\n\n const send = useCallback(async () => {\n setBusy(true)\n try {\n if (!attemptKey.current) {\n attemptKey.current = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`\n }\n const response = await authorizedFetch(user, '/api/commerce/order-invoice-send', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Idempotency-Key': attemptKey.current },\n body: JSON.stringify({ hostId, orderId, to, subject, message }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(payload?.error ?? 'The invoice could not be sent', { variant: 'error', allowDuplicate: true })\n }\n enqueueSnackbar(`${number} sent to ${String(payload.to ?? to)}`, { variant: 'success', persist: false })\n onClose()\n } finally {\n setBusy(false)\n }\n }, [user, hostId, orderId, to, subject, message, number, enqueueSnackbar, onClose])\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} maxWidth=\"sm\" fullWidth aria-labelledby=\"invoice-send-title\">\n <DialogTitle id=\"invoice-send-title\">{`Send invoice: ${number}`}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n {blocker ? (\n <Typography variant=\"body2\" color=\"error\">\n {CommerceModel.invoiceSendRefusalMessage(blocker)}\n </Typography>\n ) : null}\n <TextField\n label=\"To\"\n type=\"email\"\n value={to}\n onChange={(event) => setTo(event.target.value)}\n required\n fullWidth\n disabled={busy}\n />\n <TextField\n label=\"Subject\"\n value={subject}\n onChange={(event) => setSubject(event.target.value)}\n fullWidth\n disabled={busy}\n slotProps={{ htmlInput: { maxLength: CommerceModel.QUOTE_MAX_SEND_SUBJECT } }}\n helperText={\n threaded\n ? 'Keep the subject the same to keep these emails in one conversation in your customer’s inbox.'\n : undefined\n }\n />\n <TextField\n label=\"Message\"\n value={message}\n onChange={(event) => setMessage(event.target.value)}\n multiline\n minRows={4}\n fullWidth\n disabled={busy}\n slotProps={{ htmlInput: { maxLength: CommerceModel.QUOTE_MAX_SEND_MESSAGE } }}\n helperText=\"Shown at the top of the email, in your words.\"\n />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {(paid\n ? 'The email shows the invoice as paid, with the date it was paid, for your customer’s records. '\n : 'The email shows the invoice number, date, total and what is due, with a button to pay it by card on your site. The button keeps working until it is paid. ') +\n `The invoice is attached as a PDF. Replies go to ${contact || 'your email'}.`}\n </Typography>\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" disabled={busy || !to.trim() || Boolean(blocker)} onClick={() => void send()}>\n {busy ? 'Sending…' : 'Send'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nInvoiceSendDialog.displayName = 'InvoiceSendDialog'\n\nexport default InvoiceSendDialog\n"],"names":["CommerceModel","siteContactEmail","resolveHostToken","useSnackbar","authorizedFetch","useFirestore","useUser","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","doc","getDoc","useCallback","useEffect","useRef","useState","sentAt","atMs","Date","toLocaleString","dateStyle","timeStyle","invoiceSentLine","order","last","lastInvoiceSend","paid","Number","to","InvoiceSendDialog","props","open","onClose","hostId","orderId","firestore","data","user","enqueueSnackbar","busy","setBusy","setTo","subject","setSubject","message","setMessage","contact","setContact","attemptKey","current","live","invoiceSendRecipient","invoiceEmailDefaultSubject","then","snapshot","host","businessName","String","trim","listed","test","catch","undefined","invoiceIsPaid","refused","invoiceSendRefusal","blocker","threaded","Boolean","invoiceEmailThread","number","formatInvoiceNumber","send","payload","globalThis","crypto","randomUUID","now","Math","random","toString","slice","response","method","headers","body","JSON","stringify","json","ok","error","variant","allowDuplicate","persist","maxWidth","fullWidth","aria-labelledby","id","spacing","sx","pt","color","invoiceSendRefusalMessage","label","type","value","onChange","event","target","required","disabled","slotProps","htmlInput","maxLength","QUOTE_MAX_SEND_SUBJECT","helperText","multiline","minRows","QUOTE_MAX_SEND_MESSAGE","onClick","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/invoice-send.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport * as CommerceModel from '../../model'\nimport { siteContactEmail, resolveHostToken, type HostTokenSource } from '@aglyn/aglyn/app-utils/host-tokens'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\n/** `Oct 29, 2026, 3:04 PM`. */\nconst sentAt = (atMs: number) =>\n new Date(atMs).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' })\n\n/**\n * The order's (or the quote's) last invoice email, as the console says it:\n * `Invoice sent Oct 29, 2026, 3:04 PM to jordan@example.com.` Null when none\n * was sent.\n */\nexport function invoiceSentLine(order: Pick<CommerceModel.HostOrder, 'invoiceSends'>): string | null {\n const last = CommerceModel.lastInvoiceSend(order)\n if (!last) return null\n return `${last.paid ? 'Paid invoice' : 'Invoice'} sent ${sentAt(Number(last.atMs))} to ${last.to || 'the customer'}.`\n}\n\nexport interface InvoiceSendDialogProps {\n open: boolean\n onClose: () => void\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n}\n\n/**\n * Send invoice (AGL-4099), for an order or a quote: a short form (To,\n * Subject, Message) that emails the invoice in the site's look with the\n * invoice PDF attached. Unpaid, the email carries a Pay button to the order's\n * lasting pay link on the business's site; paid, it is the customer's record.\n * A quote's invoice lands in the quote's conversation, and an order's in its\n * own. The site's name and contact email are read once when the form opens,\n * so the dialog that offers it listens to nothing more.\n */\nexport function InvoiceSendDialog(props: InvoiceSendDialogProps) {\n const { open, onClose, hostId, orderId, order } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const [busy, setBusy] = useState(false)\n const [to, setTo] = useState('')\n const [subject, setSubject] = useState('')\n const [message, setMessage] = useState('')\n const [contact, setContact] = useState<string | null>(null)\n\n /*\n * One attempt per form (AGL-1697's rule): a retried Send presents the same\n * key and is answered from the first, so the customer never gets two; a\n * changed form is a new attempt.\n */\n const attemptKey = useRef('')\n useEffect(() => {\n attemptKey.current = ''\n }, [to, subject, message])\n\n useEffect(() => {\n if (!open) return\n let live = true\n setTo(CommerceModel.invoiceSendPrefill(order))\n setSubject(CommerceModel.invoiceEmailDefaultSubject(order, '', orderId))\n setMessage('')\n void getDoc(doc(firestore, 'hosts', hostId))\n .then((snapshot) => {\n if (!live) return\n const host = (snapshot.data() ?? {}) as Record<string, unknown>\n const businessName = String(resolveHostToken('businessName', host as HostTokenSource) ?? '').trim()\n setSubject(CommerceModel.invoiceEmailDefaultSubject(order, businessName, orderId))\n const listed = siteContactEmail(host as never)\n setContact(listed && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(listed) ? listed : null)\n })\n .catch(() => undefined)\n return () => {\n live = false\n }\n // Filled once per opening; edits after that are the member's.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open])\n\n const paid = CommerceModel.invoiceIsPaid(order)\n const refused = CommerceModel.invoiceSendRefusal(order, orderId, to.trim())\n // The address is the form's to fix; every other refusal is the order's, said up front.\n const blocker = refused && refused !== 'no-address' && refused !== 'bad-address' ? refused : null\n const threaded = Boolean(CommerceModel.invoiceEmailThread(order))\n const number = CommerceModel.formatInvoiceNumber(order, orderId)\n\n const send = useCallback(async () => {\n setBusy(true)\n try {\n if (!attemptKey.current) {\n attemptKey.current = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`\n }\n const response = await authorizedFetch(user, '/api/commerce/order-invoice-send', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Idempotency-Key': attemptKey.current },\n body: JSON.stringify({ hostId, orderId, to, subject, message }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(payload?.error ?? 'The invoice could not be sent', { variant: 'error', allowDuplicate: true })\n }\n enqueueSnackbar(`${number} sent to ${String(payload.to ?? to)}`, { variant: 'success', persist: false })\n onClose()\n } finally {\n setBusy(false)\n }\n }, [user, hostId, orderId, to, subject, message, number, enqueueSnackbar, onClose])\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} maxWidth=\"sm\" fullWidth aria-labelledby=\"invoice-send-title\">\n <DialogTitle id=\"invoice-send-title\">{`Send invoice: ${number}`}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n {blocker ? (\n <Typography variant=\"body2\" color=\"error\">\n {CommerceModel.invoiceSendRefusalMessage(blocker)}\n </Typography>\n ) : null}\n <TextField\n label=\"To\"\n type=\"email\"\n value={to}\n onChange={(event) => setTo(event.target.value)}\n required\n fullWidth\n disabled={busy}\n />\n <TextField\n label=\"Subject\"\n value={subject}\n onChange={(event) => setSubject(event.target.value)}\n fullWidth\n disabled={busy}\n slotProps={{ htmlInput: { maxLength: CommerceModel.QUOTE_MAX_SEND_SUBJECT } }}\n helperText={\n threaded\n ? 'Keep the subject the same to keep these emails in one conversation in your customer’s inbox.'\n : undefined\n }\n />\n <TextField\n label=\"Message\"\n value={message}\n onChange={(event) => setMessage(event.target.value)}\n multiline\n minRows={4}\n fullWidth\n disabled={busy}\n slotProps={{ htmlInput: { maxLength: CommerceModel.QUOTE_MAX_SEND_MESSAGE } }}\n helperText=\"Shown at the top of the email, in your words.\"\n />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {(paid\n ? 'The email shows the invoice as paid, with the date it was paid, for your customer’s records. '\n : 'The email shows the invoice number, date, total and what is due, with a button to pay it by card on your site. The button keeps working until it is paid. ') +\n `The invoice is attached as a PDF. Replies go to ${contact || 'your email'}.`}\n </Typography>\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" disabled={busy || !to.trim() || Boolean(blocker)} onClick={() => void send()}>\n {busy ? 'Sending…' : 'Send'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nInvoiceSendDialog.displayName = 'InvoiceSendDialog'\n\nexport default InvoiceSendDialog\n"],"names":["CommerceModel","siteContactEmail","resolveHostToken","useSnackbar","authorizedFetch","useFirestore","useUser","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","doc","getDoc","useCallback","useEffect","useRef","useState","sentAt","atMs","Date","toLocaleString","dateStyle","timeStyle","invoiceSentLine","order","last","lastInvoiceSend","paid","Number","to","InvoiceSendDialog","props","open","onClose","hostId","orderId","firestore","data","user","enqueueSnackbar","busy","setBusy","setTo","subject","setSubject","message","setMessage","contact","setContact","attemptKey","current","live","invoiceSendPrefill","invoiceEmailDefaultSubject","then","snapshot","host","businessName","String","trim","listed","test","catch","undefined","invoiceIsPaid","refused","invoiceSendRefusal","blocker","threaded","Boolean","invoiceEmailThread","number","formatInvoiceNumber","send","payload","globalThis","crypto","randomUUID","now","Math","random","toString","slice","response","method","headers","body","JSON","stringify","json","ok","error","variant","allowDuplicate","persist","maxWidth","fullWidth","aria-labelledby","id","spacing","sx","pt","color","invoiceSendRefusalMessage","label","type","value","onChange","event","target","required","disabled","slotProps","htmlInput","maxLength","QUOTE_MAX_SEND_SUBJECT","helperText","multiline","minRows","QUOTE_MAX_SEND_MESSAGE","onClick","displayName"],"mappings":"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"}
|
|
@@ -80,7 +80,7 @@ function soldOut(variant) {
|
|
|
80
80
|
]);
|
|
81
81
|
if (!product) return null;
|
|
82
82
|
const variant = options.length > 0 ? variantFor(product, picked) : product.variants.find((item)=>item.id === variantId);
|
|
83
|
-
const chosen = CommerceModel.resolveLineModifiers(product, modifiers);
|
|
83
|
+
const chosen = CommerceModel.resolveLineModifiers(product, modifiers, variant == null ? void 0 : variant.id);
|
|
84
84
|
const unitCents = variant ? Math.round(Number(variant.priceUsd) * 100) + (chosen.ok ? chosen.extraCents : 0) : 0;
|
|
85
85
|
const missing = groups.find((group)=>modifiers.filter((pick)=>pick.groupId === group.id).length < group.min);
|
|
86
86
|
const canAdd = Boolean(variant) && chosen.ok && quantity >= 1 && !(props.blockSoldOut && soldOut(variant));
|
|
@@ -228,7 +228,8 @@ function soldOut(variant) {
|
|
|
228
228
|
children: group.options.map((option)=>{
|
|
229
229
|
const selected = modifiers.some((pick)=>pick.groupId === group.id && pick.optionId === option.id);
|
|
230
230
|
return /*#__PURE__*/ _jsx(Chip, {
|
|
231
|
-
label:
|
|
231
|
+
label: // A choice priced by variant shows its price on this one (AGL-4100).
|
|
232
|
+
CommerceModel.modifierOptionPriceCents(option, variant == null ? void 0 : variant.id) ? `${option.name} +${money(CommerceModel.modifierOptionPriceCents(option, variant == null ? void 0 : variant.id))}` : option.name,
|
|
232
233
|
color: selected ? 'primary' : 'default',
|
|
233
234
|
variant: selected ? 'filled' : 'outlined',
|
|
234
235
|
"aria-pressed": selected,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../../libs/plugins/commerce/src/lib/components/console/pos/pos-item-dialog.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'use client'\n\nimport {\n Box,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n IconButton,\n Stack,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n useMediaQuery,\n useTheme,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport * as CommerceModel from '../../../model'\nimport { usd } from './pos-api'\nimport { POS_TOUCH_PX } from './pos-product-grid.component'\n\n/** What the dialog hands back: one basket line's worth of choices. */\nexport interface PosItemChoice {\n variant: CommerceModel.ProductVariant\n modifiers: CommerceModel.ModifierSelection[]\n quantity: number\n}\n\nexport interface PosItemDialogProps {\n /** The product being added or edited; `null` closes the dialog. */\n product: (CommerceModel.HostProduct & { $id: string }) | null\n /** An existing line's choices, when the cashier tapped a basket line. */\n initial?: { variantId?: string; modifiers?: CommerceModel.ModifierSelection[]; quantity: number }\n onClose: () => void\n onConfirm: (choice: PosItemChoice) => void\n /** Editing only: takes the line out of the basket. */\n onRemove?: () => void\n /**\n * How a price reads; the register's dollars by default. The self-service\n * kiosk (AGL-3623) passes the store's currency.\n */\n formatMoney?: (cents: number) => string\n /**\n * A sold-out variant cannot be added, rather than warned about: a kiosk's\n * customer is not holding the goods the way a cashier is (AGL-3623).\n */\n blockSoldOut?: boolean\n /** The most of one line that can be added; 99 at the register. */\n maxQuantity?: number\n}\n\n/** Whether the register must ask before adding: a choice of variant or any modifier. */\nexport function posItemNeedsChoice(product: CommerceModel.HostProduct): boolean {\n return (\n (product.variants?.length ?? 0) > 1 ||\n CommerceModel.productModifierGroups(product).length > 0\n )\n}\n\n/** The variant whose options match every picked value, or undefined. */\nfunction variantFor(\n product: CommerceModel.HostProduct,\n picked: Record<string, string>,\n): CommerceModel.ProductVariant | undefined {\n return product.variants.find((variant) =>\n (product.options ?? []).every((option) => variant.options?.[option.name] === picked[option.name]),\n )\n}\n\nfunction variantLabel(variant: CommerceModel.ProductVariant): string {\n return Object.values(variant.options ?? {}).join(' / ') || 'Default'\n}\n\nfunction soldOut(variant: CommerceModel.ProductVariant | undefined): boolean {\n return variant?.inventory != null && variant.inventory <= 0\n}\n\n/**\n * The register's item sheet (AGL-3607): the size, color or other variant,\n * then each modifier group — required ones marked, \"pick one\" groups as a\n * single toggle row and \"pick up to N\" groups as chips — and the quantity,\n * with the line's price on the Add button as it changes. Full screen on a\n * phone, a dialog on a tablet; every target is at least 44px.\n *\n * The price shown is the register's estimate. The server prices the line\n * again from the product when the sale is charged.\n */\nexport function PosItemDialog(props: PosItemDialogProps) {\n const { product, initial } = props\n const money = props.formatMoney ?? usd\n const maxQuantity = props.maxQuantity ?? 99\n const theme = useTheme()\n const phone = useMediaQuery(theme.breakpoints.down('sm'))\n const groups = useMemo(\n () => (product ? CommerceModel.productModifierGroups(product) : []),\n [product],\n )\n const options = product?.options ?? []\n const [picked, setPicked] = useState<Record<string, string>>({})\n const [variantId, setVariantId] = useState('')\n const [modifiers, setModifiers] = useState<CommerceModel.ModifierSelection[]>([])\n const [quantity, setQuantity] = useState(1)\n\n useEffect(() => {\n if (!product) return\n const start =\n product.variants.find((variant) => variant.id === initial?.variantId) ??\n product.variants.find((variant) => !soldOut(variant)) ??\n product.variants[0]\n setVariantId(start?.id ?? '')\n setPicked({ ...(start?.options ?? {}) })\n setModifiers(initial?.modifiers ?? [])\n setQuantity(initial?.quantity ?? 1)\n }, [product, initial])\n\n if (!product) return null\n\n const variant =\n options.length > 0\n ? variantFor(product, picked)\n : product.variants.find((item) => item.id === variantId)\n const chosen = CommerceModel.resolveLineModifiers(product, modifiers)\n const unitCents = variant\n ? Math.round(Number(variant.priceUsd) * 100) + (chosen.ok ? chosen.extraCents : 0)\n : 0\n const missing = groups.find(\n (group) => modifiers.filter((pick) => pick.groupId === group.id).length < group.min,\n )\n const canAdd =\n Boolean(variant) && chosen.ok && quantity >= 1 && !(props.blockSoldOut && soldOut(variant))\n\n const toggle = (group: CommerceModel.ProductModifierGroup, optionId: string) => {\n setModifiers((current) => {\n const inGroup = current.filter((pick) => pick.groupId === group.id)\n const has = inGroup.some((pick) => pick.optionId === optionId)\n if (has) return current.filter((pick) => !(pick.groupId === group.id && pick.optionId === optionId))\n // A pick-one group swaps its choice; a pick-many group stops at its max.\n if (group.max === 1) {\n return [...current.filter((pick) => pick.groupId !== group.id), { groupId: group.id, optionId }]\n }\n if (inGroup.length >= group.max) return current\n return [...current, { groupId: group.id, optionId }]\n })\n }\n\n return (\n <Dialog open onClose={props.onClose} fullScreen={phone} fullWidth maxWidth=\"sm\">\n <DialogTitle>{product.name}</DialogTitle>\n <DialogContent dividers>\n <Stack spacing={3}>\n {options.length > 0\n ? options.map((option) => (\n <Box key={option.name} role=\"group\" aria-label={option.name}>\n <Typography variant=\"subtitle1\" component=\"h3\" sx={{ mb: 1 }}>\n {option.name}\n </Typography>\n <ToggleButtonGroup\n exclusive\n value={picked[option.name] ?? null}\n onChange={(_event, value: string | null) => {\n if (value) setPicked((current) => ({ ...current, [option.name]: value }))\n }}\n sx={{ flexWrap: 'wrap', gap: 1 }}\n >\n {option.values.map((value) => (\n <ToggleButton key={value} value={value} sx={{ minHeight: POS_TOUCH_PX, minWidth: 88 }}>\n {value}\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n </Box>\n ))\n : product.variants.length > 1 ? (\n <Box role=\"group\" aria-label=\"Variant\">\n <ToggleButtonGroup\n exclusive\n value={variantId || null}\n onChange={(_event, value: string | null) => value && setVariantId(value)}\n sx={{ flexWrap: 'wrap', gap: 1 }}\n >\n {product.variants.map((item) => (\n <ToggleButton key={item.id} value={item.id} sx={{ minHeight: POS_TOUCH_PX, minWidth: 88 }}>\n {`${variantLabel(item)} · ${money(Math.round(Number(item.priceUsd) * 100))}`}\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n </Box>\n ) : null}\n {options.length > 0 && !variant ? (\n <Typography variant=\"body2\" color=\"warning.main\">\n {'That combination is not sold. Pick another.'}\n </Typography>\n ) : null}\n {variant && soldOut(variant) ? (\n <Typography variant=\"body2\" color=\"warning.main\">\n {props.blockSoldOut ? 'Sold out' : 'Out of stock: selling it takes the count below zero.'}\n </Typography>\n ) : null}\n {groups.map((group) => {\n const count = modifiers.filter((pick) => pick.groupId === group.id).length\n const rule = CommerceModel.modifierGroupRequired(group)\n ? group.max === 1\n ? 'Required · pick 1'\n : `Required · pick ${group.min} to ${group.max}`\n : group.max === 1\n ? 'Optional · pick 1'\n : `Optional · up to ${group.max}`\n return (\n <Box key={group.id} role=\"group\" aria-label={group.name}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'baseline', mb: 1 }}>\n <Typography variant=\"subtitle1\" component=\"h3\">\n {group.name}\n </Typography>\n <Typography\n variant=\"caption\"\n color={count < group.min ? 'warning.main' : 'text.secondary'}\n >\n {rule}\n </Typography>\n </Stack>\n <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>\n {group.options.map((option) => {\n const selected = modifiers.some(\n (pick) => pick.groupId === group.id && pick.optionId === option.id,\n )\n return (\n <Chip\n key={option.id}\n label={option.priceCents ? `${option.name} +${money(option.priceCents)}` : option.name}\n color={selected ? 'primary' : 'default'}\n variant={selected ? 'filled' : 'outlined'}\n aria-pressed={selected}\n onClick={() => toggle(group, option.id)}\n disabled={!selected && group.max > 1 && count >= group.max}\n sx={{ minHeight: POS_TOUCH_PX, px: 1 }}\n />\n )\n })}\n </Box>\n </Box>\n )\n })}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"subtitle1\" component=\"h3\" sx={{ flex: 1 }}>\n {'Quantity'}\n </Typography>\n <IconButton\n aria-label=\"One fewer\"\n onClick={() => setQuantity((value) => Math.max(1, value - 1))}\n sx={{ width: POS_TOUCH_PX, height: POS_TOUCH_PX, border: 1, borderColor: 'divider' }}\n >\n {'−'}\n </IconButton>\n <TextField\n value={quantity}\n onChange={(event) => {\n const next = Math.round(Number(event.target.value.replace(/\\D/g, '')))\n setQuantity(Math.max(1, Math.min(maxQuantity, next || 1)))\n }}\n sx={{ width: 72 }}\n slotProps={{\n htmlInput: { inputMode: 'numeric', 'aria-label': 'Quantity', style: { textAlign: 'center' } },\n }}\n />\n <IconButton\n aria-label=\"One more\"\n onClick={() => setQuantity((value) => Math.min(maxQuantity, value + 1))}\n sx={{ width: POS_TOUCH_PX, height: POS_TOUCH_PX, border: 1, borderColor: 'divider' }}\n >\n {'+'}\n </IconButton>\n </Stack>\n </Stack>\n </DialogContent>\n <DialogActions sx={{ p: 2, gap: 1 }}>\n {props.onRemove ? (\n <Button color=\"error\" onClick={props.onRemove} sx={{ minHeight: POS_TOUCH_PX, mr: 'auto' }}>\n {'Remove'}\n </Button>\n ) : null}\n <Button onClick={props.onClose} sx={{ minHeight: POS_TOUCH_PX }}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n disabled={!canAdd}\n onClick={() => {\n if (!variant || !chosen.ok) return\n props.onConfirm({ variant, modifiers, quantity })\n }}\n sx={{ minHeight: POS_TOUCH_PX, minWidth: 160 }}\n >\n {missing\n ? `Choose ${missing.name.toLowerCase()}`\n : `${props.onRemove ? 'Update' : 'Add'} · ${money(unitCents * quantity)}`}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nPosItemDialog.displayName = 'PosItemDialog'\n"],"names":["Box","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","IconButton","Stack","TextField","ToggleButton","ToggleButtonGroup","Typography","useMediaQuery","useTheme","useEffect","useMemo","useState","CommerceModel","usd","POS_TOUCH_PX","posItemNeedsChoice","product","variants","length","productModifierGroups","variantFor","picked","find","variant","options","every","option","name","variantLabel","Object","values","join","soldOut","inventory","PosItemDialog","props","initial","money","formatMoney","maxQuantity","theme","phone","breakpoints","down","groups","setPicked","variantId","setVariantId","modifiers","setModifiers","quantity","setQuantity","start","id","item","chosen","resolveLineModifiers","unitCents","Math","round","Number","priceUsd","ok","extraCents","missing","group","filter","pick","groupId","min","canAdd","Boolean","blockSoldOut","toggle","optionId","current","inGroup","has","some","max","open","onClose","fullScreen","fullWidth","maxWidth","dividers","spacing","map","role","aria-label","component","sx","mb","exclusive","value","onChange","_event","flexWrap","gap","minHeight","minWidth","color","count","rule","modifierGroupRequired","direction","alignItems","display","selected","label","priceCents","aria-pressed","onClick","disabled","px","flex","width","height","border","borderColor","event","next","target","replace","slotProps","htmlInput","inputMode","style","textAlign","p","onRemove","mr","onConfirm","toLowerCase","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../libs/plugins/commerce/src/lib/components/console/pos/pos-item-dialog.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'use client'\n\nimport {\n Box,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n IconButton,\n Stack,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n useMediaQuery,\n useTheme,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport * as CommerceModel from '../../../model'\nimport { usd } from './pos-api'\nimport { POS_TOUCH_PX } from './pos-product-grid.component'\n\n/** What the dialog hands back: one basket line's worth of choices. */\nexport interface PosItemChoice {\n variant: CommerceModel.ProductVariant\n modifiers: CommerceModel.ModifierSelection[]\n quantity: number\n}\n\nexport interface PosItemDialogProps {\n /** The product being added or edited; `null` closes the dialog. */\n product: (CommerceModel.HostProduct & { $id: string }) | null\n /** An existing line's choices, when the cashier tapped a basket line. */\n initial?: { variantId?: string; modifiers?: CommerceModel.ModifierSelection[]; quantity: number }\n onClose: () => void\n onConfirm: (choice: PosItemChoice) => void\n /** Editing only: takes the line out of the basket. */\n onRemove?: () => void\n /**\n * How a price reads; the register's dollars by default. The self-service\n * kiosk (AGL-3623) passes the store's currency.\n */\n formatMoney?: (cents: number) => string\n /**\n * A sold-out variant cannot be added, rather than warned about: a kiosk's\n * customer is not holding the goods the way a cashier is (AGL-3623).\n */\n blockSoldOut?: boolean\n /** The most of one line that can be added; 99 at the register. */\n maxQuantity?: number\n}\n\n/** Whether the register must ask before adding: a choice of variant or any modifier. */\nexport function posItemNeedsChoice(product: CommerceModel.HostProduct): boolean {\n return (\n (product.variants?.length ?? 0) > 1 ||\n CommerceModel.productModifierGroups(product).length > 0\n )\n}\n\n/** The variant whose options match every picked value, or undefined. */\nfunction variantFor(\n product: CommerceModel.HostProduct,\n picked: Record<string, string>,\n): CommerceModel.ProductVariant | undefined {\n return product.variants.find((variant) =>\n (product.options ?? []).every((option) => variant.options?.[option.name] === picked[option.name]),\n )\n}\n\nfunction variantLabel(variant: CommerceModel.ProductVariant): string {\n return Object.values(variant.options ?? {}).join(' / ') || 'Default'\n}\n\nfunction soldOut(variant: CommerceModel.ProductVariant | undefined): boolean {\n return variant?.inventory != null && variant.inventory <= 0\n}\n\n/**\n * The register's item sheet (AGL-3607): the size, color or other variant,\n * then each modifier group — required ones marked, \"pick one\" groups as a\n * single toggle row and \"pick up to N\" groups as chips — and the quantity,\n * with the line's price on the Add button as it changes. Full screen on a\n * phone, a dialog on a tablet; every target is at least 44px.\n *\n * The price shown is the register's estimate. The server prices the line\n * again from the product when the sale is charged.\n */\nexport function PosItemDialog(props: PosItemDialogProps) {\n const { product, initial } = props\n const money = props.formatMoney ?? usd\n const maxQuantity = props.maxQuantity ?? 99\n const theme = useTheme()\n const phone = useMediaQuery(theme.breakpoints.down('sm'))\n const groups = useMemo(\n () => (product ? CommerceModel.productModifierGroups(product) : []),\n [product],\n )\n const options = product?.options ?? []\n const [picked, setPicked] = useState<Record<string, string>>({})\n const [variantId, setVariantId] = useState('')\n const [modifiers, setModifiers] = useState<CommerceModel.ModifierSelection[]>([])\n const [quantity, setQuantity] = useState(1)\n\n useEffect(() => {\n if (!product) return\n const start =\n product.variants.find((variant) => variant.id === initial?.variantId) ??\n product.variants.find((variant) => !soldOut(variant)) ??\n product.variants[0]\n setVariantId(start?.id ?? '')\n setPicked({ ...(start?.options ?? {}) })\n setModifiers(initial?.modifiers ?? [])\n setQuantity(initial?.quantity ?? 1)\n }, [product, initial])\n\n if (!product) return null\n\n const variant =\n options.length > 0\n ? variantFor(product, picked)\n : product.variants.find((item) => item.id === variantId)\n const chosen = CommerceModel.resolveLineModifiers(product, modifiers, variant?.id)\n const unitCents = variant\n ? Math.round(Number(variant.priceUsd) * 100) + (chosen.ok ? chosen.extraCents : 0)\n : 0\n const missing = groups.find(\n (group) => modifiers.filter((pick) => pick.groupId === group.id).length < group.min,\n )\n const canAdd =\n Boolean(variant) && chosen.ok && quantity >= 1 && !(props.blockSoldOut && soldOut(variant))\n\n const toggle = (group: CommerceModel.ProductModifierGroup, optionId: string) => {\n setModifiers((current) => {\n const inGroup = current.filter((pick) => pick.groupId === group.id)\n const has = inGroup.some((pick) => pick.optionId === optionId)\n if (has) return current.filter((pick) => !(pick.groupId === group.id && pick.optionId === optionId))\n // A pick-one group swaps its choice; a pick-many group stops at its max.\n if (group.max === 1) {\n return [...current.filter((pick) => pick.groupId !== group.id), { groupId: group.id, optionId }]\n }\n if (inGroup.length >= group.max) return current\n return [...current, { groupId: group.id, optionId }]\n })\n }\n\n return (\n <Dialog open onClose={props.onClose} fullScreen={phone} fullWidth maxWidth=\"sm\">\n <DialogTitle>{product.name}</DialogTitle>\n <DialogContent dividers>\n <Stack spacing={3}>\n {options.length > 0\n ? options.map((option) => (\n <Box key={option.name} role=\"group\" aria-label={option.name}>\n <Typography variant=\"subtitle1\" component=\"h3\" sx={{ mb: 1 }}>\n {option.name}\n </Typography>\n <ToggleButtonGroup\n exclusive\n value={picked[option.name] ?? null}\n onChange={(_event, value: string | null) => {\n if (value) setPicked((current) => ({ ...current, [option.name]: value }))\n }}\n sx={{ flexWrap: 'wrap', gap: 1 }}\n >\n {option.values.map((value) => (\n <ToggleButton key={value} value={value} sx={{ minHeight: POS_TOUCH_PX, minWidth: 88 }}>\n {value}\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n </Box>\n ))\n : product.variants.length > 1 ? (\n <Box role=\"group\" aria-label=\"Variant\">\n <ToggleButtonGroup\n exclusive\n value={variantId || null}\n onChange={(_event, value: string | null) => value && setVariantId(value)}\n sx={{ flexWrap: 'wrap', gap: 1 }}\n >\n {product.variants.map((item) => (\n <ToggleButton key={item.id} value={item.id} sx={{ minHeight: POS_TOUCH_PX, minWidth: 88 }}>\n {`${variantLabel(item)} · ${money(Math.round(Number(item.priceUsd) * 100))}`}\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n </Box>\n ) : null}\n {options.length > 0 && !variant ? (\n <Typography variant=\"body2\" color=\"warning.main\">\n {'That combination is not sold. Pick another.'}\n </Typography>\n ) : null}\n {variant && soldOut(variant) ? (\n <Typography variant=\"body2\" color=\"warning.main\">\n {props.blockSoldOut ? 'Sold out' : 'Out of stock: selling it takes the count below zero.'}\n </Typography>\n ) : null}\n {groups.map((group) => {\n const count = modifiers.filter((pick) => pick.groupId === group.id).length\n const rule = CommerceModel.modifierGroupRequired(group)\n ? group.max === 1\n ? 'Required · pick 1'\n : `Required · pick ${group.min} to ${group.max}`\n : group.max === 1\n ? 'Optional · pick 1'\n : `Optional · up to ${group.max}`\n return (\n <Box key={group.id} role=\"group\" aria-label={group.name}>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'baseline', mb: 1 }}>\n <Typography variant=\"subtitle1\" component=\"h3\">\n {group.name}\n </Typography>\n <Typography\n variant=\"caption\"\n color={count < group.min ? 'warning.main' : 'text.secondary'}\n >\n {rule}\n </Typography>\n </Stack>\n <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>\n {group.options.map((option) => {\n const selected = modifiers.some(\n (pick) => pick.groupId === group.id && pick.optionId === option.id,\n )\n return (\n <Chip\n key={option.id}\n label={\n // A choice priced by variant shows its price on this one (AGL-4100).\n CommerceModel.modifierOptionPriceCents(option, variant?.id)\n ? `${option.name} +${money(CommerceModel.modifierOptionPriceCents(option, variant?.id))}`\n : option.name\n }\n color={selected ? 'primary' : 'default'}\n variant={selected ? 'filled' : 'outlined'}\n aria-pressed={selected}\n onClick={() => toggle(group, option.id)}\n disabled={!selected && group.max > 1 && count >= group.max}\n sx={{ minHeight: POS_TOUCH_PX, px: 1 }}\n />\n )\n })}\n </Box>\n </Box>\n )\n })}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"subtitle1\" component=\"h3\" sx={{ flex: 1 }}>\n {'Quantity'}\n </Typography>\n <IconButton\n aria-label=\"One fewer\"\n onClick={() => setQuantity((value) => Math.max(1, value - 1))}\n sx={{ width: POS_TOUCH_PX, height: POS_TOUCH_PX, border: 1, borderColor: 'divider' }}\n >\n {'−'}\n </IconButton>\n <TextField\n value={quantity}\n onChange={(event) => {\n const next = Math.round(Number(event.target.value.replace(/\\D/g, '')))\n setQuantity(Math.max(1, Math.min(maxQuantity, next || 1)))\n }}\n sx={{ width: 72 }}\n slotProps={{\n htmlInput: { inputMode: 'numeric', 'aria-label': 'Quantity', style: { textAlign: 'center' } },\n }}\n />\n <IconButton\n aria-label=\"One more\"\n onClick={() => setQuantity((value) => Math.min(maxQuantity, value + 1))}\n sx={{ width: POS_TOUCH_PX, height: POS_TOUCH_PX, border: 1, borderColor: 'divider' }}\n >\n {'+'}\n </IconButton>\n </Stack>\n </Stack>\n </DialogContent>\n <DialogActions sx={{ p: 2, gap: 1 }}>\n {props.onRemove ? (\n <Button color=\"error\" onClick={props.onRemove} sx={{ minHeight: POS_TOUCH_PX, mr: 'auto' }}>\n {'Remove'}\n </Button>\n ) : null}\n <Button onClick={props.onClose} sx={{ minHeight: POS_TOUCH_PX }}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n disabled={!canAdd}\n onClick={() => {\n if (!variant || !chosen.ok) return\n props.onConfirm({ variant, modifiers, quantity })\n }}\n sx={{ minHeight: POS_TOUCH_PX, minWidth: 160 }}\n >\n {missing\n ? `Choose ${missing.name.toLowerCase()}`\n : `${props.onRemove ? 'Update' : 'Add'} · ${money(unitCents * quantity)}`}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nPosItemDialog.displayName = 'PosItemDialog'\n"],"names":["Box","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","IconButton","Stack","TextField","ToggleButton","ToggleButtonGroup","Typography","useMediaQuery","useTheme","useEffect","useMemo","useState","CommerceModel","usd","POS_TOUCH_PX","posItemNeedsChoice","product","variants","length","productModifierGroups","variantFor","picked","find","variant","options","every","option","name","variantLabel","Object","values","join","soldOut","inventory","PosItemDialog","props","initial","money","formatMoney","maxQuantity","theme","phone","breakpoints","down","groups","setPicked","variantId","setVariantId","modifiers","setModifiers","quantity","setQuantity","start","id","item","chosen","resolveLineModifiers","unitCents","Math","round","Number","priceUsd","ok","extraCents","missing","group","filter","pick","groupId","min","canAdd","Boolean","blockSoldOut","toggle","optionId","current","inGroup","has","some","max","open","onClose","fullScreen","fullWidth","maxWidth","dividers","spacing","map","role","aria-label","component","sx","mb","exclusive","value","onChange","_event","flexWrap","gap","minHeight","minWidth","color","count","rule","modifierGroupRequired","direction","alignItems","display","selected","label","modifierOptionPriceCents","aria-pressed","onClick","disabled","px","flex","width","height","border","borderColor","event","next","target","replace","slotProps","htmlInput","inputMode","style","textAlign","p","onRemove","mr","onConfirm","toLowerCase","displayName"],"mappings":"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"}
|
|
@@ -83,7 +83,8 @@ import { useKioskIdle } from "./pos-kiosk/use-kiosk-idle.js";
|
|
|
83
83
|
}, label ? {
|
|
84
84
|
label
|
|
85
85
|
} : {}, {
|
|
86
|
-
|
|
86
|
+
// A choice priced by variant adds its price on this one (AGL-4100).
|
|
87
|
+
unitCents: variant.priceCents + picked.reduce((sum, option)=>sum + CommerceModel.modifierOptionPriceCents(option, variant.id), 0)
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
/**
|