@aglyn/plugins-commerce 1.0.0-beta.289 → 1.0.0-beta.290
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 +2 -2
- package/package.json +13 -13
- package/src/lib/components/console/commerce-pdf-download.d.ts +30 -0
- package/src/lib/components/console/commerce-pdf-download.js +53 -0
- package/src/lib/components/console/commerce-pdf-download.js.map +1 -0
- package/src/lib/components/console/host-quotes-card.component.js +20 -4
- package/src/lib/components/console/host-quotes-card.component.js.map +1 -1
- package/src/lib/components/console/order-invoice.component.d.ts +6 -4
- package/src/lib/components/console/order-invoice.component.js +67 -11
- package/src/lib/components/console/order-invoice.component.js.map +1 -1
- package/src/lib/components/console/quote-editor-dialog.component.js +53 -2
- package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/quote-proofs.component.d.ts +37 -0
- package/src/lib/components/console/quote-proofs.component.js +626 -0
- package/src/lib/components/console/quote-proofs.component.js.map +1 -0
- package/src/lib/components/quote-page-body.js +29 -1
- package/src/lib/components/quote-page-body.js.map +1 -1
- package/src/lib/components/quote-proof-panel.d.ts +42 -0
- package/src/lib/components/quote-proof-panel.js +436 -0
- package/src/lib/components/quote-proof-panel.js.map +1 -0
- package/src/lib/model/commerce-quote-pay.d.ts +27 -0
- package/src/lib/model/commerce-quote-pay.js +36 -3
- package/src/lib/model/commerce-quote-pay.js.map +1 -1
- package/src/lib/model/commerce-quote-proofs.d.ts +159 -0
- package/src/lib/model/commerce-quote-proofs.js +348 -0
- package/src/lib/model/commerce-quote-proofs.js.map +1 -0
- package/src/lib/model/commerce-quotes.d.ts +59 -3
- package/src/lib/model/commerce-quotes.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/server/commerce-pdf.d.ts +100 -0
- package/src/lib/server/commerce-pdf.js +283 -0
- package/src/lib/server/commerce-pdf.js.map +1 -0
- package/src/lib/server/order-print.d.ts +6 -0
- package/src/lib/server/order-print.js +22 -5
- package/src/lib/server/order-print.js.map +1 -1
- package/src/lib/server/pdf-response.d.ts +31 -0
- package/src/lib/server/pdf-response.js +30 -0
- package/src/lib/server/pdf-response.js.map +1 -0
- package/src/lib/server/pos-reader-splash.d.ts +2 -18
- package/src/lib/server/pos-reader-splash.js +8 -93
- package/src/lib/server/pos-reader-splash.js.map +1 -1
- package/src/lib/server/quote-pay.d.ts +31 -4
- package/src/lib/server/quote-pay.js +11 -11
- package/src/lib/server/quote-pay.js.map +1 -1
- package/src/lib/server/quote-proofs.d.ts +51 -0
- package/src/lib/server/quote-proofs.js +500 -0
- package/src/lib/server/quote-proofs.js.map +1 -0
- package/src/lib/server/quote-uploads.d.ts +33 -0
- package/src/lib/server/quote-uploads.js +29 -0
- package/src/lib/server/quote-uploads.js.map +1 -0
- package/src/lib/server/quote.d.ts +14 -0
- package/src/lib/server/quote.js +336 -3
- package/src/lib/server/quote.js.map +1 -1
- package/src/lib/server/site-image-source.d.ts +38 -0
- package/src/lib/server/site-image-source.js +130 -0
- package/src/lib/server/site-image-source.js.map +1 -0
- package/src/lib/server.js +19 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/utils/commerce-document-pdf.d.ts +77 -0
- package/src/lib/utils/commerce-document-pdf.js +608 -0
- package/src/lib/utils/commerce-document-pdf.js.map +1 -0
- package/src/lib/utils/order-invoice.d.ts +52 -3
- package/src/lib/utils/order-invoice.js +116 -64
- package/src/lib/utils/order-invoice.js.map +1 -1
package/README.md
CHANGED
|
@@ -54,8 +54,8 @@ All console pages and cards are code-split.
|
|
|
54
54
|
|
|
55
55
|
`@aglyn/plugins-commerce/server` pulls in `firebase-admin` and Stripe and is kept out of the client entry point.
|
|
56
56
|
|
|
57
|
-
- `registerCommerceApi()` (the `tenantApi` surface) registers the storefront routes under `commerce/` (catalog, product, related, reviews, cart, cart checkout, checkout, download, feed, newsletter, restock notifications, order analytics, order status, the customer's quote page
|
|
58
|
-
- `registerCommerceConsoleApi()` (the `consoleApi` surface) registers the merchant routes: cancel, fulfill and refund an order, draft orders, quotes
|
|
57
|
+
- `registerCommerceApi()` (the `tenantApi` surface) registers the storefront routes under `commerce/` (catalog, product, related, reviews, cart, cart checkout, checkout, download, feed, newsletter, restock notifications, order analytics, order status, the customer's quote page, its Pay button and its PDF (`quote-view`, `quote-pay`, `quote-pdf`), its proofs, the customer's artwork upload and their answer (`quote-proof`, `quote-upload`, `quote-respond`), reservations, gate, member feed, stream, subscription portal) and the account routes under `membership/` (register, login, logout, recover, reset, account, content, wishlist). It also registers a site page resolver and enricher, the commerce tax profile and a product card reader on core seams.
|
|
58
|
+
- `registerCommerceConsoleApi()` (the `consoleApi` surface) registers the merchant routes: cancel, fulfill and refund an order, draft orders, quotes with their pay links, PDFs, proofs and revisions, POS orders, gift cards, member posts, a site member's password help and removal (`membership/admin-password`, `membership/admin-remove`), Stripe Connect, supplier updates, and the abandoned-checkout and restock processors. It also adds a handler on the platform billing webhook and order figure readers.
|
|
59
59
|
- `registerCommerceConsoleApi()` also registers what an AI build may make in a store, on core's resource-drafts and AI-capability seams: the `product` draft writer and build op, and the `store-settings` writer with the `store-shipping`, `store-pickup`, `store-delivery` and `store-tax` build ops (`server/store-setup-ai-*.ts`). Those four set shipping zones and rates, pickup, local delivery and sales tax exactly as the console's cards save them, live, using only amounts and rates the request states, and never a carrier or tax-service account. `model/store-setup-content.ts` holds the pure read-and-apply step.
|
|
60
60
|
- Loading the module registers scheduled plugin jobs: `abandoned-checkout-recovery`, `back-in-stock-alerts`, `review-request-emails`, `stock-decrement-reconciliation`, `supplier-webhook-delivery`, `payment-method-domains` and `pos-reader-splash-refresh` (card reader start-up screens after a logo change). This is why `package.json` lists `./src/lib/server.*` under `sideEffects`.
|
|
61
61
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-commerce",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.290",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
37
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.290",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.290",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.290",
|
|
31
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.290",
|
|
32
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.290",
|
|
33
|
+
"@aglyn/shared-util-email": "1.0.0-beta.290",
|
|
34
|
+
"@aglyn/shared-util-http": "1.0.0-beta.290",
|
|
35
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.290",
|
|
36
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.290",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.290",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.290",
|
|
39
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.290",
|
|
40
40
|
"@stripe/react-stripe-js": "^6.12.0",
|
|
41
41
|
"@stripe/stripe-js": "^9.17.0",
|
|
42
42
|
"@swc/helpers": "0.5.23",
|
|
@@ -0,0 +1,30 @@
|
|
|
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 { authorizedFetch } from '@aglyn/shared-util-http/authorized-token';
|
|
18
|
+
/**
|
|
19
|
+
* Saving a quote's or an invoice's PDF from the console (AGL-4095): the
|
|
20
|
+
* server makes the file (`commerce/quote` `pdf`, `commerce/order-print`
|
|
21
|
+
* `kind=invoice-pdf`), and the browser saves it under the name the answer
|
|
22
|
+
* gives.
|
|
23
|
+
*/
|
|
24
|
+
/** The file name an answer's `Content-Disposition` gives, or `fallback`. */
|
|
25
|
+
export declare function pdfFilenameFrom(disposition: string | null | undefined, fallback: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Asks for a PDF with the signed-in account's token and saves it. Resolves to
|
|
28
|
+
* null when it was saved, or to the message to show when it was refused.
|
|
29
|
+
*/
|
|
30
|
+
export declare function downloadCommercePdf(user: Parameters<typeof authorizedFetch>[0], path: string, init: RequestInit, fallbackName: string): Promise<string | null>;
|
|
@@ -0,0 +1,53 @@
|
|
|
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 { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
18
|
+
/**
|
|
19
|
+
* Saving a quote's or an invoice's PDF from the console (AGL-4095): the
|
|
20
|
+
* server makes the file (`commerce/quote` `pdf`, `commerce/order-print`
|
|
21
|
+
* `kind=invoice-pdf`), and the browser saves it under the name the answer
|
|
22
|
+
* gives.
|
|
23
|
+
*/ /** The file name an answer's `Content-Disposition` gives, or `fallback`. */ export function pdfFilenameFrom(disposition, fallback) {
|
|
24
|
+
var _exec_, _exec;
|
|
25
|
+
const named = (_exec = /filename="?([^";]+)"?/i.exec(String(disposition != null ? disposition : ''))) == null ? void 0 : (_exec_ = _exec[1]) == null ? void 0 : _exec_.trim();
|
|
26
|
+
return named && /\.pdf$/i.test(named) ? named : fallback;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Asks for a PDF with the signed-in account's token and saves it. Resolves to
|
|
30
|
+
* null when it was saved, or to the message to show when it was refused.
|
|
31
|
+
*/ export async function downloadCommercePdf(user, path, init, fallbackName) {
|
|
32
|
+
let response;
|
|
33
|
+
try {
|
|
34
|
+
response = await authorizedFetch(user, path, init);
|
|
35
|
+
} catch (unused) {
|
|
36
|
+
return 'The PDF could not be made. Check your connection and try again.';
|
|
37
|
+
}
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
var _ref;
|
|
40
|
+
const payload = await response.json().catch(()=>({}));
|
|
41
|
+
return String((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'The PDF could not be made');
|
|
42
|
+
}
|
|
43
|
+
const blob = await response.blob();
|
|
44
|
+
const url = URL.createObjectURL(blob);
|
|
45
|
+
const link = document.createElement('a');
|
|
46
|
+
link.href = url;
|
|
47
|
+
link.download = pdfFilenameFrom(response.headers.get('content-disposition'), fallbackName);
|
|
48
|
+
link.click();
|
|
49
|
+
setTimeout(()=>URL.revokeObjectURL(url), 1000);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=commerce-pdf-download.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/commerce-pdf-download.ts"],"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 { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\n/**\n * Saving a quote's or an invoice's PDF from the console (AGL-4095): the\n * server makes the file (`commerce/quote` `pdf`, `commerce/order-print`\n * `kind=invoice-pdf`), and the browser saves it under the name the answer\n * gives.\n */\n\n/** The file name an answer's `Content-Disposition` gives, or `fallback`. */\nexport function pdfFilenameFrom(disposition: string | null | undefined, fallback: string): string {\n const named = /filename=\"?([^\";]+)\"?/i.exec(String(disposition ?? ''))?.[1]?.trim()\n return named && /\\.pdf$/i.test(named) ? named : fallback\n}\n\n/**\n * Asks for a PDF with the signed-in account's token and saves it. Resolves to\n * null when it was saved, or to the message to show when it was refused.\n */\nexport async function downloadCommercePdf(\n user: Parameters<typeof authorizedFetch>[0],\n path: string,\n init: RequestInit,\n fallbackName: string,\n): Promise<string | null> {\n let response: Response\n try {\n response = await authorizedFetch(user, path, init)\n } catch {\n return 'The PDF could not be made. Check your connection and try again.'\n }\n if (!response.ok) {\n const payload = await response.json().catch(() => ({}))\n return String(payload?.error ?? 'The PDF could not be made')\n }\n const blob = await response.blob()\n const url = URL.createObjectURL(blob)\n const link = document.createElement('a')\n link.href = url\n link.download = pdfFilenameFrom(response.headers.get('content-disposition'), fallbackName)\n link.click()\n setTimeout(() => URL.revokeObjectURL(url), 1000)\n return null\n}\n"],"names":["authorizedFetch","pdfFilenameFrom","disposition","fallback","named","exec","String","trim","test","downloadCommercePdf","user","path","init","fallbackName","response","ok","payload","json","catch","error","blob","url","URL","createObjectURL","link","document","createElement","href","download","headers","get","click","setTimeout","revokeObjectURL"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;AAEA,SAASA,eAAe,QAAQ,2CAA0C;AAE1E;;;;;CAKC,GAED,0EAA0E,GAC1E,OAAO,SAASC,gBAAgBC,WAAsC,EAAEC,QAAgB;QACxE,QAAA;IAAd,MAAMC,SAAQ,QAAA,yBAAyBC,IAAI,CAACC,OAAOJ,sBAAAA,cAAe,0BAApD,SAAA,KAA0D,CAAC,EAAE,qBAA7D,OAA+DK,IAAI;IACjF,OAAOH,SAAS,UAAUI,IAAI,CAACJ,SAASA,QAAQD;AAClD;AAEA;;;CAGC,GACD,OAAO,eAAeM,oBACpBC,IAA2C,EAC3CC,IAAY,EACZC,IAAiB,EACjBC,YAAoB;IAEpB,IAAIC;IACJ,IAAI;QACFA,WAAW,MAAMd,gBAAgBU,MAAMC,MAAMC;IAC/C,EAAE,eAAM;QACN,OAAO;IACT;IACA,IAAI,CAACE,SAASC,EAAE,EAAE;;QAChB,MAAMC,UAAU,MAAMF,SAASG,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;QACpD,OAAOZ,eAAOU,2BAAAA,QAASG,KAAK,mBAAI;IAClC;IACA,MAAMC,OAAO,MAAMN,SAASM,IAAI;IAChC,MAAMC,MAAMC,IAAIC,eAAe,CAACH;IAChC,MAAMI,OAAOC,SAASC,aAAa,CAAC;IACpCF,KAAKG,IAAI,GAAGN;IACZG,KAAKI,QAAQ,GAAG3B,gBAAgBa,SAASe,OAAO,CAACC,GAAG,CAAC,wBAAwBjB;IAC7EW,KAAKO,KAAK;IACVC,WAAW,IAAMV,IAAIW,eAAe,CAACZ,MAAM;IAC3C,OAAO;AACT"}
|
|
@@ -26,7 +26,7 @@ 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 { useMemo, useState } from "react";
|
|
29
|
+
import { useEffect, useMemo, useState } from "react";
|
|
30
30
|
import QuoteEditorDialog from "./quote-editor-dialog.component.js";
|
|
31
31
|
/**
|
|
32
32
|
* How many quotes the list reads. The whole set is read and the grid sorts
|
|
@@ -35,8 +35,8 @@ import QuoteEditorDialog from "./quote-editor-dialog.component.js";
|
|
|
35
35
|
/** The products the editor's picker offers. */ const PRODUCT_WINDOW = 200;
|
|
36
36
|
const REFUSAL = commerceSitePermissionRefusal('commerce.quotes');
|
|
37
37
|
/** One quote as its row draws and sorts it. */ function quoteListRow(quote) {
|
|
38
|
-
var _quote_lineItems, _ref, _ref1, _ref2;
|
|
39
|
-
var _quote_quote, _items_, _quote_totals, _quote_quote1;
|
|
38
|
+
var _quote_lineItems, _ref, _ref1, _ref2, _ref3;
|
|
39
|
+
var _quote_quote, _items_, _quote_totals, _quote_quote1, _CommerceModel_latestQuoteResponse;
|
|
40
40
|
const customer = (_quote_quote = quote.quote) == null ? void 0 : _quote_quote.customer;
|
|
41
41
|
const status = CommerceModel.quoteDisplayStatus(quote);
|
|
42
42
|
const items = (_quote_lineItems = quote.lineItems) != null ? _quote_lineItems : [];
|
|
@@ -47,7 +47,9 @@ const REFUSAL = commerceSitePermissionRefusal('commerce.quotes');
|
|
|
47
47
|
statusKey: status.key,
|
|
48
48
|
statusLabel: status.label,
|
|
49
49
|
totalCents: Number((_ref1 = (_quote_totals = quote.totals) == null ? void 0 : _quote_totals.totalCents) != null ? _ref1 : 0),
|
|
50
|
-
|
|
50
|
+
// The customer's latest answer counts as an update (AGL-4096), so a quote
|
|
51
|
+
// they just approved or asked changes to sorts to the top.
|
|
52
|
+
updatedAtMs: Math.max(Number((_ref2 = (_quote_quote1 = quote.quote) == null ? void 0 : _quote_quote1.updatedAtMs) != null ? _ref2 : 0), Number((_ref3 = (_CommerceModel_latestQuoteResponse = CommerceModel.latestQuoteResponse(quote.quote)) == null ? void 0 : _CommerceModel_latestQuoteResponse.atMs) != null ? _ref3 : 0))
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
@@ -82,6 +84,20 @@ const REFUSAL = commerceSitePermissionRefusal('commerce.quotes');
|
|
|
82
84
|
]);
|
|
83
85
|
/** The quote open in the editor: its id, or `new`. */ const [openId, setOpenId] = useState(null);
|
|
84
86
|
const openQuote = openId && openId !== 'new' ? (_quotes_rows_find = quotes.rows.find((row)=>row.$id === openId)) != null ? _quotes_rows_find : null : null;
|
|
87
|
+
// A manager notice opens its quote (AGL-4096): `?quote=<id>` on the Orders
|
|
88
|
+
// page, once that quote has loaded.
|
|
89
|
+
const [linkedId, setLinkedId] = useState(()=>{
|
|
90
|
+
var _get;
|
|
91
|
+
return typeof window === 'undefined' ? '' : (_get = new URLSearchParams(window.location.search).get('quote')) != null ? _get : '';
|
|
92
|
+
});
|
|
93
|
+
useEffect(()=>{
|
|
94
|
+
if (!linkedId || !quotes.rows.some((row)=>row.$id === linkedId)) return;
|
|
95
|
+
setOpenId(linkedId);
|
|
96
|
+
setLinkedId('');
|
|
97
|
+
}, [
|
|
98
|
+
linkedId,
|
|
99
|
+
quotes.rows
|
|
100
|
+
]);
|
|
85
101
|
const columns = useMemo(()=>[
|
|
86
102
|
{
|
|
87
103
|
field: 'number',
|
|
@@ -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 { 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 updatedAtMs: Number(quote.quote?.updatedAtMs ?? 0),\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 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","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","HostQuotesCard","props","quotes","hostId","canEdit","canSend","firestore","data","quoteDocs","idField","undefined","rows","Array","from","productDocs","products","filter","product","openId","setOpenId","openQuote","find","row","$id","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 { 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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"}
|
|
@@ -21,10 +21,12 @@ export interface OrderInvoiceButtonProps {
|
|
|
21
21
|
order: CommerceModel.HostOrder;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* "Invoice" (AGL-3611), beside "Packing slip" in the order dialog:
|
|
25
|
-
* as a printable bill,
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* "Invoice" (AGL-3611), beside "Packing slip" in the order dialog: a menu of
|
|
25
|
+
* "Print invoice", the order as a printable bill, and "Download PDF"
|
|
26
|
+
* (AGL-4095), the branded invoice the server makes in the site's colors with
|
|
27
|
+
* its logo (`commerce/order-print?kind=invoice-pdf`, "Manage orders"). The
|
|
28
|
+
* store's name, currency and footer are read when one is clicked, not while
|
|
29
|
+
* the dialog is open, so the dialog listens to nothing more.
|
|
28
30
|
*/
|
|
29
31
|
export declare function OrderInvoiceButton(props: OrderInvoiceButtonProps): import("react").JSX.Element;
|
|
30
32
|
export declare namespace OrderInvoiceButton {
|
|
@@ -14,22 +14,57 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
19
|
+
import { useFirestore, useUser } from "@aglyn/tenant-feature-instance";
|
|
20
|
+
import { Button, Menu, MenuItem } from "@mui/material";
|
|
20
21
|
import { doc, getDoc } from "firebase/firestore";
|
|
21
22
|
import { useCallback, useState } from "react";
|
|
22
23
|
import { buildOrderInvoiceHtml, invoiceStoreFrom } from "../../utils/order-invoice.js";
|
|
24
|
+
import { downloadCommercePdf } from "./commerce-pdf-download.js";
|
|
23
25
|
/**
|
|
24
|
-
* "Invoice" (AGL-3611), beside "Packing slip" in the order dialog:
|
|
25
|
-
* as a printable bill,
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* "Invoice" (AGL-3611), beside "Packing slip" in the order dialog: a menu of
|
|
27
|
+
* "Print invoice", the order as a printable bill, and "Download PDF"
|
|
28
|
+
* (AGL-4095), the branded invoice the server makes in the site's colors with
|
|
29
|
+
* its logo (`commerce/order-print?kind=invoice-pdf`, "Manage orders"). The
|
|
30
|
+
* store's name, currency and footer are read when one is clicked, not while
|
|
31
|
+
* the dialog is open, so the dialog listens to nothing more.
|
|
28
32
|
*/ export function OrderInvoiceButton(props) {
|
|
29
33
|
const { hostId, orderId, order } = props;
|
|
30
34
|
const firestore = useFirestore();
|
|
35
|
+
const { data: user } = useUser();
|
|
36
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
31
37
|
const [busy, setBusy] = useState(false);
|
|
38
|
+
const [anchor, setAnchor] = useState(null);
|
|
39
|
+
const handleDownload = useCallback(async ()=>{
|
|
40
|
+
setAnchor(null);
|
|
41
|
+
setBusy(true);
|
|
42
|
+
try {
|
|
43
|
+
var _order_number;
|
|
44
|
+
const query = new URLSearchParams({
|
|
45
|
+
hostId,
|
|
46
|
+
orderId,
|
|
47
|
+
kind: 'invoice-pdf'
|
|
48
|
+
});
|
|
49
|
+
const refused = await downloadCommercePdf(user, `/api/commerce/order-print?${query.toString()}`, {
|
|
50
|
+
method: 'GET'
|
|
51
|
+
}, `invoice-${(_order_number = order.number) != null ? _order_number : orderId.slice(-6)}.pdf`);
|
|
52
|
+
if (refused) enqueueSnackbar(refused, {
|
|
53
|
+
variant: 'error',
|
|
54
|
+
allowDuplicate: true
|
|
55
|
+
});
|
|
56
|
+
} finally{
|
|
57
|
+
setBusy(false);
|
|
58
|
+
}
|
|
59
|
+
}, [
|
|
60
|
+
user,
|
|
61
|
+
hostId,
|
|
62
|
+
orderId,
|
|
63
|
+
order.number,
|
|
64
|
+
enqueueSnackbar
|
|
65
|
+
]);
|
|
32
66
|
const handleInvoice = useCallback(async ()=>{
|
|
67
|
+
setAnchor(null);
|
|
33
68
|
// Opened before the reads, inside the click, so a popup blocker sees the
|
|
34
69
|
// user's gesture.
|
|
35
70
|
const win = window.open('', '_blank', 'width=720,height=900');
|
|
@@ -61,10 +96,31 @@ import { buildOrderInvoiceHtml, invoiceStoreFrom } from "../../utils/order-invoi
|
|
|
61
96
|
orderId,
|
|
62
97
|
order
|
|
63
98
|
]);
|
|
64
|
-
return /*#__PURE__*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
99
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
100
|
+
children: [
|
|
101
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
102
|
+
disabled: busy,
|
|
103
|
+
"aria-haspopup": "menu",
|
|
104
|
+
"aria-expanded": anchor ? 'true' : undefined,
|
|
105
|
+
onClick: (event)=>setAnchor(event.currentTarget),
|
|
106
|
+
children: 'Invoice'
|
|
107
|
+
}),
|
|
108
|
+
/*#__PURE__*/ _jsxs(Menu, {
|
|
109
|
+
anchorEl: anchor,
|
|
110
|
+
open: Boolean(anchor),
|
|
111
|
+
onClose: ()=>setAnchor(null),
|
|
112
|
+
children: [
|
|
113
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
114
|
+
onClick: ()=>void handleInvoice(),
|
|
115
|
+
children: 'Print invoice'
|
|
116
|
+
}),
|
|
117
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
118
|
+
onClick: ()=>void handleDownload(),
|
|
119
|
+
children: 'Download PDF'
|
|
120
|
+
})
|
|
121
|
+
]
|
|
122
|
+
})
|
|
123
|
+
]
|
|
68
124
|
});
|
|
69
125
|
}
|
|
70
126
|
OrderInvoiceButton.displayName = 'OrderInvoiceButton';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/order-invoice.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 { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { Button } from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport type * as CommerceModel from '../../model'\nimport { buildOrderInvoiceHtml, invoiceStoreFrom } from '../../utils/order-invoice'\n\nexport interface OrderInvoiceButtonProps {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n}\n\n/**\n * \"Invoice\" (AGL-3611), beside \"Packing slip\" in the order dialog:
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/order-invoice.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 { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { Button, Menu, MenuItem } from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport type * as CommerceModel from '../../model'\nimport { buildOrderInvoiceHtml, invoiceStoreFrom } from '../../utils/order-invoice'\nimport { downloadCommercePdf } from './commerce-pdf-download'\n\nexport interface OrderInvoiceButtonProps {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n}\n\n/**\n * \"Invoice\" (AGL-3611), beside \"Packing slip\" in the order dialog: a menu of\n * \"Print invoice\", the order as a printable bill, and \"Download PDF\"\n * (AGL-4095), the branded invoice the server makes in the site's colors with\n * its logo (`commerce/order-print?kind=invoice-pdf`, \"Manage orders\"). The\n * store's name, currency and footer are read when one is clicked, not while\n * the dialog is open, so the dialog listens to nothing more.\n */\nexport function OrderInvoiceButton(props: OrderInvoiceButtonProps) {\n const { 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 [anchor, setAnchor] = useState<HTMLElement | null>(null)\n\n const handleDownload = useCallback(async () => {\n setAnchor(null)\n setBusy(true)\n try {\n const query = new URLSearchParams({ hostId, orderId, kind: 'invoice-pdf' })\n const refused = await downloadCommercePdf(\n user,\n `/api/commerce/order-print?${query.toString()}`,\n { method: 'GET' },\n `invoice-${order.number ?? orderId.slice(-6)}.pdf`,\n )\n if (refused) enqueueSnackbar(refused, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusy(false)\n }\n }, [user, hostId, orderId, order.number, enqueueSnackbar])\n\n const handleInvoice = useCallback(async () => {\n setAnchor(null)\n // Opened before the reads, inside the click, so a popup blocker sees the\n // user's gesture.\n const win = window.open('', '_blank', 'width=720,height=900')\n if (!win) return\n setBusy(true)\n try {\n const [host, store] = await Promise.all([\n getDoc(doc(firestore, 'hosts', hostId)).catch(() => null),\n getDoc(doc(firestore, 'hosts', hostId, 'settings', 'store')).catch(() => null),\n ])\n const html = buildOrderInvoiceHtml({\n order,\n orderId,\n store: invoiceStoreFrom(\n host?.data() as Record<string, unknown> | undefined,\n store?.data() as Record<string, unknown> | undefined,\n ),\n })\n // Every value in `html` is escaped by the builder (AGL-2283).\n win.document.write(html)\n win.document.close()\n // Print from the opener, not an injected script: the popup inherits\n // the console's CSP (AGL-523).\n win.focus()\n win.print()\n } finally {\n setBusy(false)\n }\n }, [firestore, hostId, orderId, order])\n\n return (\n <>\n <Button\n disabled={busy}\n aria-haspopup=\"menu\"\n aria-expanded={anchor ? 'true' : undefined}\n onClick={(event) => setAnchor(event.currentTarget)}\n >\n {'Invoice'}\n </Button>\n <Menu anchorEl={anchor} open={Boolean(anchor)} onClose={() => setAnchor(null)}>\n <MenuItem onClick={() => void handleInvoice()}>{'Print invoice'}</MenuItem>\n <MenuItem onClick={() => void handleDownload()}>{'Download PDF'}</MenuItem>\n </Menu>\n </>\n )\n}\nOrderInvoiceButton.displayName = 'OrderInvoiceButton'\n\nexport default OrderInvoiceButton\n"],"names":["useSnackbar","useFirestore","useUser","Button","Menu","MenuItem","doc","getDoc","useCallback","useState","buildOrderInvoiceHtml","invoiceStoreFrom","downloadCommercePdf","OrderInvoiceButton","props","hostId","orderId","order","firestore","data","user","enqueueSnackbar","busy","setBusy","anchor","setAnchor","handleDownload","query","URLSearchParams","kind","refused","toString","method","number","slice","variant","allowDuplicate","handleInvoice","win","window","open","host","store","Promise","all","catch","html","document","write","close","focus","print","disabled","aria-haspopup","aria-expanded","undefined","onClick","event","currentTarget","anchorEl","Boolean","onClose","displayName"],"mappings":"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"}
|
|
@@ -27,7 +27,9 @@ import { useFirestore, useFirestoreDoc, useStoreSettingsHref, useUser } from "@a
|
|
|
27
27
|
import { Alert, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, FormControlLabel, List, ListItemButton, ListItemText, MenuItem, Paper, Stack, Switch, TextField, Typography } from "@mui/material";
|
|
28
28
|
import { doc } from "firebase/firestore";
|
|
29
29
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
30
|
+
import { downloadCommercePdf } from "./commerce-pdf-download.js";
|
|
30
31
|
import { QuotePayLinkSection } from "./quote-pay-link.component.js";
|
|
32
|
+
import { QuoteProofsSection } from "./quote-proofs.component.js";
|
|
31
33
|
const REFUSAL = commerceSitePermissionRefusal('commerce.quotes');
|
|
32
34
|
const SEARCH_DEBOUNCE_MS = 250;
|
|
33
35
|
const money = (cents)=>posMoney(cents);
|
|
@@ -205,8 +207,9 @@ function addressBody(address) {
|
|
|
205
207
|
const [form, setForm] = useState(()=>formFromQuote(quote, storeDefault));
|
|
206
208
|
// The version this form was opened on. A change saved elsewhere while it is
|
|
207
209
|
// open arrives on `quote`; the save still names this one, so the server
|
|
208
|
-
// refuses rather than overwrite that change.
|
|
209
|
-
|
|
210
|
+
// refuses rather than overwrite that change. A proof added or a revision
|
|
211
|
+
// started here (AGL-4096) moves it, and the save names the new one.
|
|
212
|
+
const [baseUpdatedAtMs, setBaseUpdatedAtMs] = useState(()=>{
|
|
210
213
|
var _quote_quote;
|
|
211
214
|
return quote == null ? void 0 : (_quote_quote = quote.quote) == null ? void 0 : _quote_quote.updatedAtMs;
|
|
212
215
|
});
|
|
@@ -393,6 +396,36 @@ function addressBody(address) {
|
|
|
393
396
|
enqueueSnackbar,
|
|
394
397
|
onClose
|
|
395
398
|
]);
|
|
399
|
+
const [downloading, setDownloading] = useState(false);
|
|
400
|
+
const downloadPdf = useCallback(async ()=>{
|
|
401
|
+
if (!quote) return;
|
|
402
|
+
setDownloading(true);
|
|
403
|
+
try {
|
|
404
|
+
var _quote_number;
|
|
405
|
+
const refused = await downloadCommercePdf(user, '/api/commerce/quote', {
|
|
406
|
+
method: 'POST',
|
|
407
|
+
headers: {
|
|
408
|
+
'Content-Type': 'application/json'
|
|
409
|
+
},
|
|
410
|
+
body: JSON.stringify({
|
|
411
|
+
action: 'pdf',
|
|
412
|
+
hostId,
|
|
413
|
+
quoteId: quote.$id
|
|
414
|
+
})
|
|
415
|
+
}, `quote-${(_quote_number = quote.number) != null ? _quote_number : quote.$id.slice(-6)}.pdf`);
|
|
416
|
+
if (refused) enqueueSnackbar(refused, {
|
|
417
|
+
variant: 'error',
|
|
418
|
+
allowDuplicate: true
|
|
419
|
+
});
|
|
420
|
+
} finally{
|
|
421
|
+
setDownloading(false);
|
|
422
|
+
}
|
|
423
|
+
}, [
|
|
424
|
+
user,
|
|
425
|
+
quote,
|
|
426
|
+
hostId,
|
|
427
|
+
enqueueSnackbar
|
|
428
|
+
]);
|
|
396
429
|
const readOnly = !canEdit || busy;
|
|
397
430
|
const paid = quote ? !CommerceModel.isUnpaidQuote(quote) : false;
|
|
398
431
|
const title = quote ? CommerceModel.formatQuoteNumber(quote, quote.$id) : 'New quote';
|
|
@@ -443,6 +476,15 @@ function addressBody(address) {
|
|
|
443
476
|
canSend: canSend,
|
|
444
477
|
edited: edited
|
|
445
478
|
}) : null,
|
|
479
|
+
quote ? /*#__PURE__*/ _jsx(QuoteProofsSection, {
|
|
480
|
+
hostId: hostId,
|
|
481
|
+
quoteId: quote.$id,
|
|
482
|
+
quote: quote,
|
|
483
|
+
canEdit: canEdit,
|
|
484
|
+
onEdited: (updatedAtMs)=>{
|
|
485
|
+
if (updatedAtMs) setBaseUpdatedAtMs(updatedAtMs);
|
|
486
|
+
}
|
|
487
|
+
}) : null,
|
|
446
488
|
taxBasis.kind === 'none' && taxBasis.reason === 'undecided' ? /*#__PURE__*/ _jsx(PrerequisiteNotice, {
|
|
447
489
|
href: taxesHref != null ? taxesHref : undefined,
|
|
448
490
|
actionLabel: "Open Taxes",
|
|
@@ -677,6 +719,15 @@ function addressBody(address) {
|
|
|
677
719
|
children: 'Delete'
|
|
678
720
|
})
|
|
679
721
|
}) : null,
|
|
722
|
+
quote ? /*#__PURE__*/ _jsx(BlockedControl, {
|
|
723
|
+
blocked: !canEdit || edited,
|
|
724
|
+
reason: edited ? 'Save your changes first: the PDF shows the quote as it was last saved.' : REFUSAL,
|
|
725
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
726
|
+
disabled: busy || downloading,
|
|
727
|
+
onClick: ()=>void downloadPdf(),
|
|
728
|
+
children: downloading ? 'Making PDF…' : 'Download PDF'
|
|
729
|
+
})
|
|
730
|
+
}) : null,
|
|
680
731
|
/*#__PURE__*/ _jsx(Button, {
|
|
681
732
|
onClick: onClose,
|
|
682
733
|
disabled: busy,
|