@aglyn/plugins-commerce 1.0.0-beta.244 → 1.0.0-beta.246
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/package.json +13 -13
- package/src/lib/components/commerce-console-page.js +10 -0
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/product-zones.d.ts +7 -1
- package/src/lib/components/console/product-zones.js.map +1 -1
- package/src/lib/components/console/products-hub-zone.component.d.ts +3 -2
- package/src/lib/components/console/products-hub-zone.component.js +9 -1
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
- package/src/lib/components/console/review-settings-card.component.d.ts +14 -0
- package/src/lib/components/console/review-settings-card.component.js +168 -0
- package/src/lib/components/console/review-settings-card.component.js.map +1 -0
- package/src/lib/components/console/reviews-moderation-card.component.d.ts +14 -0
- package/src/lib/components/console/reviews-moderation-card.component.js +85 -11
- package/src/lib/components/console/reviews-moderation-card.component.js.map +1 -1
- package/src/lib/components/product-reviews.d.ts +29 -1
- package/src/lib/components/product-reviews.js +97 -12
- package/src/lib/components/product-reviews.js.map +1 -1
- package/src/lib/model/commerce.d.ts +11 -2
- package/src/lib/model/commerce.js +14 -2
- package/src/lib/model/commerce.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/model/product-reviews.d.ts +124 -0
- package/src/lib/model/product-reviews.js +133 -0
- package/src/lib/model/product-reviews.js.map +1 -0
- package/src/lib/model/store-setup-content.d.ts +145 -0
- package/src/lib/model/store-setup-content.js +813 -0
- package/src/lib/model/store-setup-content.js.map +1 -0
- package/src/lib/server/membership-reset.js +8 -2
- package/src/lib/server/membership-reset.js.map +1 -1
- package/src/lib/server/order-event-triggers.d.ts +2 -0
- package/src/lib/server/order-event-triggers.js +18 -1
- package/src/lib/server/order-event-triggers.js.map +1 -1
- package/src/lib/server/review-requests.d.ts +102 -0
- package/src/lib/server/review-requests.js +346 -0
- package/src/lib/server/review-requests.js.map +1 -0
- package/src/lib/server/review-tokens.d.ts +82 -0
- package/src/lib/server/review-tokens.js +119 -0
- package/src/lib/server/review-tokens.js.map +1 -0
- package/src/lib/server/reviews.d.ts +36 -3
- package/src/lib/server/reviews.js +310 -52
- package/src/lib/server/reviews.js.map +1 -1
- package/src/lib/server/store-setup-ai-capability.d.ts +61 -0
- package/src/lib/server/store-setup-ai-capability.js +343 -0
- package/src/lib/server/store-setup-ai-capability.js.map +1 -0
- package/src/lib/server/store-setup-ai-writer.console.d.ts +27 -0
- package/src/lib/server/store-setup-ai-writer.console.js +44 -0
- package/src/lib/server/store-setup-ai-writer.console.js.map +1 -0
- package/src/lib/server/store-setup-ai-writer.d.ts +37 -0
- package/src/lib/server/store-setup-ai-writer.js +174 -0
- package/src/lib/server/store-setup-ai-writer.js.map +1 -0
- package/src/lib/server.js +27 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tenant-emails.js +58 -0
- package/src/lib/tenant-emails.js.map +1 -1
package/README.md
CHANGED
|
@@ -56,7 +56,10 @@ All console pages and cards are code-split.
|
|
|
56
56
|
|
|
57
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, 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
58
|
- `registerCommerceConsoleApi()` (the `consoleApi` surface) registers the merchant routes: cancel, fulfill and refund an order, draft orders, 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
|
+
- Loading the module registers scheduled plugin jobs: `abandoned-checkout-recovery`, `back-in-stock-alerts`, `review-request-emails`, `stock-decrement-reconciliation` and `supplier-webhook-delivery`. This is why `package.json` lists `./src/lib/server.*` under `sideEffects`.
|
|
61
|
+
|
|
62
|
+
- Product reviews are verified-buyer by default. `commerce/reviews` (POST) marks a review verified only for a signed-in site member whose proven email has an order that still entitles the product, or for a single-use link from the review-request email (`server/review-tokens.ts`: hashed, 60 days, bound to order, product and email). A typed email never verifies. Verified reviews publish at once; under the store's `anyone` policy (`settings/store.reviews.policy`) the rest wait as `pending`, and under `verified` they get 403. `GET commerce/reviews?mode=eligibility` tells the storefront block which form to draw. The POST is rate limited per address and per member. The `order.fulfilled`, `order.delivered` and `order.paid` subscriber stamps an order for its review request, and the hourly `review-request-emails` job sends it (`server/review-requests.ts`, catalog key `review-request`). Merchants hide a review only for a content reason (`REVIEW_MODERATION_REASONS`), which the Firestore rules enforce.
|
|
60
63
|
|
|
61
64
|
Routes are served by the host app's API dispatcher under `/api/`, for example `/api/commerce/catalog`.
|
|
62
65
|
|
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.246",
|
|
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.246",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.246",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.246",
|
|
31
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.246",
|
|
32
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.246",
|
|
33
|
+
"@aglyn/shared-util-email": "1.0.0-beta.246",
|
|
34
|
+
"@aglyn/shared-util-http": "1.0.0-beta.246",
|
|
35
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.246",
|
|
36
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.246",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.246",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.246",
|
|
39
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.246",
|
|
40
40
|
"@stripe/react-stripe-js": "^6.12.0",
|
|
41
41
|
"@stripe/stripe-js": "^9.17.0",
|
|
42
42
|
"@swc/helpers": "0.5.23",
|
|
@@ -42,6 +42,7 @@ import ReservationsCard from "./console/reservations-card.component.js";
|
|
|
42
42
|
import ReturnSettingsCard from "./console/return-settings-card.component.js";
|
|
43
43
|
import ReturnsCard from "./console/returns-card.component.js";
|
|
44
44
|
import ReviewsModerationCard from "./console/reviews-moderation-card.component.js";
|
|
45
|
+
import ReviewSettingsCard from "./console/review-settings-card.component.js";
|
|
45
46
|
import ShippingSettingsCard from "./console/shipping-settings-card.component.js";
|
|
46
47
|
import ShipStationCard from "./console/shipstation-card.component.js";
|
|
47
48
|
import ShippingEasyCard from "./console/shippingeasy-card.component.js";
|
|
@@ -272,6 +273,15 @@ import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot
|
|
|
272
273
|
hostId: hostId
|
|
273
274
|
})
|
|
274
275
|
},
|
|
276
|
+
// Who may review, and the review-request email (AGL-3736).
|
|
277
|
+
{
|
|
278
|
+
size: {
|
|
279
|
+
xs: 12
|
|
280
|
+
},
|
|
281
|
+
children: /*#__PURE__*/ _jsx(ReviewSettingsCard, {
|
|
282
|
+
hostId: hostId
|
|
283
|
+
})
|
|
284
|
+
},
|
|
275
285
|
{
|
|
276
286
|
size: {
|
|
277
287
|
xs: 12
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/commerce-console-page.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 type { ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { GridItems } from '@aglyn/shared-ui-jsx'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport type { ReactNode } from 'react'\nimport type { CommerceConsoleSectionId } from './commerce-console-sections'\nimport CatalogOrganizationCard from './console/catalog-organization-card.component'\nimport CommerceAnalyticsCard from './console/commerce-analytics-card.component'\nimport CustomerNotificationsCard from './console/customer-notifications-card.component'\nimport DiscountsCard from './console/discounts-card.component'\nimport GiftCardsCard from './console/gift-cards-card.component'\nimport HostCouponsCard from './console/host-coupons-card.component'\nimport HostOrdersCard from './console/host-orders-card.component'\nimport LocationsCard from './console/locations-card.component'\nimport LocalDeliverySettingsCard from './console/local-delivery-settings-card.component'\nimport PickupDeliveryQueueCard from './console/pickup-delivery-queue-card.component'\nimport MemberPostsCard from './console/member-posts-card.component'\nimport OrderWebhooksCard from './console/order-webhooks-card.component'\nimport PaymentsSettingsCard from './console/payments-settings-card.component'\nimport PosShiftHistoryCard from './console/pos-ops/pos-shift-history-card.component'\nimport PosStaffPinsCard from './console/pos-ops/pos-staff-pins-card.component'\nimport PaymentMethodsCard from './console/payment-methods-card.component'\nimport ProductsHubCard from './console/products-hub-card.component'\nimport RecoveryQueueCard from './console/recovery-queue-card.component'\nimport RegistersCard from './console/registers-card.component'\nimport RegisterHardwareCards from './console/register-hardware-card.component'\nimport PosDevicesCard from './console/pos-devices-card.component'\nimport ReservationsCard from './console/reservations-card.component'\nimport ReturnSettingsCard from './console/return-settings-card.component'\nimport ReturnsCard from './console/returns-card.component'\nimport ReviewsModerationCard from './console/reviews-moderation-card.component'\nimport ShippingSettingsCard from './console/shipping-settings-card.component'\nimport ShipStationCard from './console/shipstation-card.component'\nimport ShippingEasyCard from './console/shippingeasy-card.component'\nimport StockMovementsCard from './console/stock-movements-card.component'\nimport StoreSettingsCard from './console/store-settings-card.component'\nimport StorefrontTaxSummaryCard from './console/storefront-tax-summary-card.component'\nimport SuppliersCard from './console/suppliers-card.component'\nimport TaxSettingsCard from './console/tax-settings-card.component'\nimport { COMMERCE_PROMOTIONS_ZONE, COMMERCE_SETTINGS_ZONE } from './console/store-zones'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\n\n/**\n * The `commerceSettings` zone (AGL-3612): other plugins' settings for the\n * store — label settings and carrier accounts — under the store's own.\n */\nfunction CommerceSettingsZone(props: { hostId: string }) {\n const WidgetSlot = useConsoleWidgetSlot()\n return WidgetSlot ? (\n <WidgetSlot slot={COMMERCE_SETTINGS_ZONE.id} hostId={props.hostId} orgId={undefined} />\n ) : null\n}\n\n/**\n * The `commercePromotions` zone (AGL-3640): other plugins' programs beside\n * the store's own money-off surfaces — a rewards program first.\n */\nfunction CommercePromotionsZone(props: { hostId: string }) {\n const WidgetSlot = useConsoleWidgetSlot()\n return WidgetSlot ? (\n <WidgetSlot slot={COMMERCE_PROMOTIONS_ZONE.id} hostId={props.hostId} orgId={undefined} />\n ) : null\n}\n\n/**\n * The body of one commerce section, built only when that section is the one\n * being read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT all six every render, and each card opens its Firestore\n * listens on mount — which is the entire cost this page exists to stop paying.\n * Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: CommerceConsoleSectionId,\n hostId: string,\n): ReactNode {\n switch (section) {\n case 'catalog':\n return (\n <GridItems\n spacing={3}\n items={[\n { size: { xs: 12 }, children: <ProductsHubCard hostId={hostId} /> },\n {\n size: { xs: 12 },\n children: <CatalogOrganizationCard hostId={hostId} />,\n },\n // The ledger every stock writer fills and nothing displayed\n // (AGL-2341). Directly under the catalog, because the question\n // it answers — \"why does this count disagree with the shelf\" —\n // is asked while looking at the count.\n {\n size: { xs: 12 },\n children: <StockMovementsCard hostId={hostId} />,\n },\n { size: { xs: 12 }, children: <MemberPostsCard hostId={hostId} /> },\n ]}\n />\n )\n case 'orders':\n return (\n <GridItems\n spacing={3}\n items={[\n { size: { xs: 12 }, children: <HostOrdersCard hostId={hostId} /> },\n // What to prepare, hand over and drive out (AGL-3624). Renders\n // nothing for a store with no pickup and no local delivery.\n {\n size: { xs: 12 },\n children: <PickupDeliveryQueueCard hostId={hostId} />,\n },\n // The two queues that feed orders rather than record them\n // (AGL-2227): checkouts that stalled and shoppers waiting on\n // stock. Beneath the orders list because both are pre-order.\n {\n size: { xs: 12 },\n children: <RecoveryQueueCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <PosShiftHistoryCard hostId={hostId} />,\n },\n ]}\n />\n )\n case 'returns':\n // Returns (AGL-3611); `?return={id}` opens one on arrival.\n return <ReturnsCard hostId={hostId} />\n case 'promotions':\n return (\n <GridItems\n spacing={3}\n items={[\n { size: { xs: 12 }, children: <DiscountsCard hostId={hostId} /> },\n { size: { xs: 12 }, children: <HostCouponsCard hostId={hostId} /> },\n // Store credit lives with the other money-off surfaces\n // (AGL-2226) — it had no console anywhere before this.\n { size: { xs: 12 }, children: <GiftCardsCard hostId={hostId} /> },\n {\n size: { xs: 12 },\n children: <ReviewsModerationCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <CommercePromotionsZone hostId={hostId} />,\n },\n ]}\n />\n )\n case 'reservations':\n return <ReservationsCard hostId={hostId} />\n case 'settings':\n return (\n <GridItems\n spacing={3}\n items={[\n {\n size: { xs: 12 },\n children: <PaymentsSettingsCard hostId={hostId} />,\n },\n // What shoppers may pay with (AGL-3629), beside the account the\n // money lands in.\n {\n size: { xs: 12 },\n children: <PaymentMethodsCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <StoreSettingsCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <ReturnSettingsCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <CustomerNotificationsCard hostId={hostId} />,\n },\n { size: { xs: 12 }, children: <TaxSettingsCard hostId={hostId} /> },\n { size: { xs: 12 }, children: <LocationsCard hostId={hostId} /> },\n { size: { xs: 12 }, children: <RegistersCard hostId={hostId} /> },\n { size: { xs: 12 }, children: <PosStaffPinsCard hostId={hostId} /> },\n // A register's printers, cash drawer and status (AGL-3619), one\n // card per register, after the list that names them.\n {\n size: { xs: 12 },\n children: <RegisterHardwareCards hostId={hostId} />,\n },\n { size: { xs: 12 }, children: <PosDevicesCard hostId={hostId} /> },\n {\n size: { xs: 12 },\n children: <ShippingSettingsCard hostId={hostId} />,\n },\n // The store's own delivery (AGL-3624); pickup is per location,\n // on the Inventory locations card above.\n {\n size: { xs: 12 },\n children: <LocalDeliverySettingsCard hostId={hostId} />,\n },\n { size: { xs: 12 }, children: <SuppliersCard hostId={hostId} /> },\n { size: { xs: 12 }, children: <ShipStationCard hostId={hostId} /> },\n // ShippingEasy, with the merchant's own keys (AGL-3633).\n { size: { xs: 12 }, children: <ShippingEasyCard hostId={hostId} /> },\n // The store's own endpoints for order events (AGL-3611). Renders\n // nothing for a person who may not manage them.\n {\n size: { xs: 12 },\n children: <OrderWebhooksCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <CommerceSettingsZone hostId={hostId} />,\n },\n ]}\n />\n )\n case 'analytics':\n return (\n // Two cards since AGL-2440. The tax summary lives here rather\n // than under Settings → Tax because that tab CONFIGURES tax and\n // this REPORTS what was collected — a merchant reading a figure\n // they may file from should not be one field away from changing\n // the rate that produced it.\n <GridItems\n spacing={3}\n items={[\n {\n size: { xs: 12 },\n children: <CommerceAnalyticsCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <StorefrontTaxSummaryCard hostId={hostId} />,\n },\n ]}\n />\n )\n default:\n return null\n }\n}\n\n/**\n * Commerce console page (AGL-395): the Products management surface, owned by\n * the commerce plugin and rendered by the shell's generic plugin route. The\n * product editor's media browser is supplied by the shell's\n * ConsoleMediaPickerProvider (the media library is org/session coupled and\n * stays in the app).\n *\n * ## Sections are ROUTES (AGL-2501)\n *\n * The six sections hold thirty-two Firestore listens between them, and a tab\n * strip mounts every panel: opening Catalog also subscribed the orders,\n * promotions, reservations, settings and analytics cards. The `limit()` on\n * those queries is the bill — several ask for 500, and one load reached a\n * ceiling of ~4,460 documents to render one tab.\n *\n * `HubTabs lazy` bought that back as a flag somebody has to remember. A URL\n * per section makes it structural instead: the page builds one section's body\n * and the others do not exist to subscribe. Three things come with it that the\n * tab version faked — a section is linkable, the back button walks sections,\n * and which one is open is a fact about the URL rather than state kept in sync\n * with it. `commerce-console-read-cost.spec.tsx` meters it at the Firestore\n * boundary and refuses a section that listens before it is opened.\n *\n * The shell resolves the section, gates it and hands back the rail's hrefs;\n * this page only has to say what each section contains.\n */\nexport function CommerceConsolePage(props: ConsolePluginPageProps) {\n const { hostId, section, sections, basePath } = props\n\n /*\n * Nothing, deliberately, while the redirect is in flight.\n *\n * Rendering the default section here instead would issue that section's\n * listens on a URL that is about to be replaced — paying for Catalog on\n * every arrival at `/products`, which is every click of the nav tab.\n */\n if (!section || !sections?.length || !basePath) return null\n\n return (\n <HubSections sections={sections}>\n {sectionBody(section as CommerceConsoleSectionId, hostId)}\n </HubSections>\n )\n}\nCommerceConsolePage.displayName = 'CommerceConsolePage'\n\nexport default CommerceConsolePage\n"],"names":["GridItems","HubSections","CatalogOrganizationCard","CommerceAnalyticsCard","CustomerNotificationsCard","DiscountsCard","GiftCardsCard","HostCouponsCard","HostOrdersCard","LocationsCard","LocalDeliverySettingsCard","PickupDeliveryQueueCard","MemberPostsCard","OrderWebhooksCard","PaymentsSettingsCard","PosShiftHistoryCard","PosStaffPinsCard","PaymentMethodsCard","ProductsHubCard","RecoveryQueueCard","RegistersCard","RegisterHardwareCards","PosDevicesCard","ReservationsCard","ReturnSettingsCard","ReturnsCard","ReviewsModerationCard","ShippingSettingsCard","ShipStationCard","ShippingEasyCard","StockMovementsCard","StoreSettingsCard","StorefrontTaxSummaryCard","SuppliersCard","TaxSettingsCard","COMMERCE_PROMOTIONS_ZONE","COMMERCE_SETTINGS_ZONE","useConsoleWidgetSlot","CommerceSettingsZone","props","WidgetSlot","slot","id","hostId","orgId","undefined","CommercePromotionsZone","sectionBody","section","spacing","items","size","xs","children","CommerceConsolePage","sections","basePath","length","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/components/commerce-console-page.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 type { ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { GridItems } from '@aglyn/shared-ui-jsx'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport type { ReactNode } from 'react'\nimport type { CommerceConsoleSectionId } from './commerce-console-sections'\nimport CatalogOrganizationCard from './console/catalog-organization-card.component'\nimport CommerceAnalyticsCard from './console/commerce-analytics-card.component'\nimport CustomerNotificationsCard from './console/customer-notifications-card.component'\nimport DiscountsCard from './console/discounts-card.component'\nimport GiftCardsCard from './console/gift-cards-card.component'\nimport HostCouponsCard from './console/host-coupons-card.component'\nimport HostOrdersCard from './console/host-orders-card.component'\nimport LocationsCard from './console/locations-card.component'\nimport LocalDeliverySettingsCard from './console/local-delivery-settings-card.component'\nimport PickupDeliveryQueueCard from './console/pickup-delivery-queue-card.component'\nimport MemberPostsCard from './console/member-posts-card.component'\nimport OrderWebhooksCard from './console/order-webhooks-card.component'\nimport PaymentsSettingsCard from './console/payments-settings-card.component'\nimport PosShiftHistoryCard from './console/pos-ops/pos-shift-history-card.component'\nimport PosStaffPinsCard from './console/pos-ops/pos-staff-pins-card.component'\nimport PaymentMethodsCard from './console/payment-methods-card.component'\nimport ProductsHubCard from './console/products-hub-card.component'\nimport RecoveryQueueCard from './console/recovery-queue-card.component'\nimport RegistersCard from './console/registers-card.component'\nimport RegisterHardwareCards from './console/register-hardware-card.component'\nimport PosDevicesCard from './console/pos-devices-card.component'\nimport ReservationsCard from './console/reservations-card.component'\nimport ReturnSettingsCard from './console/return-settings-card.component'\nimport ReturnsCard from './console/returns-card.component'\nimport ReviewsModerationCard from './console/reviews-moderation-card.component'\nimport ReviewSettingsCard from './console/review-settings-card.component'\nimport ShippingSettingsCard from './console/shipping-settings-card.component'\nimport ShipStationCard from './console/shipstation-card.component'\nimport ShippingEasyCard from './console/shippingeasy-card.component'\nimport StockMovementsCard from './console/stock-movements-card.component'\nimport StoreSettingsCard from './console/store-settings-card.component'\nimport StorefrontTaxSummaryCard from './console/storefront-tax-summary-card.component'\nimport SuppliersCard from './console/suppliers-card.component'\nimport TaxSettingsCard from './console/tax-settings-card.component'\nimport { COMMERCE_PROMOTIONS_ZONE, COMMERCE_SETTINGS_ZONE } from './console/store-zones'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\n\n/**\n * The `commerceSettings` zone (AGL-3612): other plugins' settings for the\n * store — label settings and carrier accounts — under the store's own.\n */\nfunction CommerceSettingsZone(props: { hostId: string }) {\n const WidgetSlot = useConsoleWidgetSlot()\n return WidgetSlot ? (\n <WidgetSlot slot={COMMERCE_SETTINGS_ZONE.id} hostId={props.hostId} orgId={undefined} />\n ) : null\n}\n\n/**\n * The `commercePromotions` zone (AGL-3640): other plugins' programs beside\n * the store's own money-off surfaces — a rewards program first.\n */\nfunction CommercePromotionsZone(props: { hostId: string }) {\n const WidgetSlot = useConsoleWidgetSlot()\n return WidgetSlot ? (\n <WidgetSlot slot={COMMERCE_PROMOTIONS_ZONE.id} hostId={props.hostId} orgId={undefined} />\n ) : null\n}\n\n/**\n * The body of one commerce section, built only when that section is the one\n * being read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT all six every render, and each card opens its Firestore\n * listens on mount — which is the entire cost this page exists to stop paying.\n * Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: CommerceConsoleSectionId,\n hostId: string,\n): ReactNode {\n switch (section) {\n case 'catalog':\n return (\n <GridItems\n spacing={3}\n items={[\n { size: { xs: 12 }, children: <ProductsHubCard hostId={hostId} /> },\n {\n size: { xs: 12 },\n children: <CatalogOrganizationCard hostId={hostId} />,\n },\n // The ledger every stock writer fills and nothing displayed\n // (AGL-2341). Directly under the catalog, because the question\n // it answers — \"why does this count disagree with the shelf\" —\n // is asked while looking at the count.\n {\n size: { xs: 12 },\n children: <StockMovementsCard hostId={hostId} />,\n },\n { size: { xs: 12 }, children: <MemberPostsCard hostId={hostId} /> },\n ]}\n />\n )\n case 'orders':\n return (\n <GridItems\n spacing={3}\n items={[\n { size: { xs: 12 }, children: <HostOrdersCard hostId={hostId} /> },\n // What to prepare, hand over and drive out (AGL-3624). Renders\n // nothing for a store with no pickup and no local delivery.\n {\n size: { xs: 12 },\n children: <PickupDeliveryQueueCard hostId={hostId} />,\n },\n // The two queues that feed orders rather than record them\n // (AGL-2227): checkouts that stalled and shoppers waiting on\n // stock. Beneath the orders list because both are pre-order.\n {\n size: { xs: 12 },\n children: <RecoveryQueueCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <PosShiftHistoryCard hostId={hostId} />,\n },\n ]}\n />\n )\n case 'returns':\n // Returns (AGL-3611); `?return={id}` opens one on arrival.\n return <ReturnsCard hostId={hostId} />\n case 'promotions':\n return (\n <GridItems\n spacing={3}\n items={[\n { size: { xs: 12 }, children: <DiscountsCard hostId={hostId} /> },\n { size: { xs: 12 }, children: <HostCouponsCard hostId={hostId} /> },\n // Store credit lives with the other money-off surfaces\n // (AGL-2226) — it had no console anywhere before this.\n { size: { xs: 12 }, children: <GiftCardsCard hostId={hostId} /> },\n {\n size: { xs: 12 },\n children: <ReviewsModerationCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <CommercePromotionsZone hostId={hostId} />,\n },\n ]}\n />\n )\n case 'reservations':\n return <ReservationsCard hostId={hostId} />\n case 'settings':\n return (\n <GridItems\n spacing={3}\n items={[\n {\n size: { xs: 12 },\n children: <PaymentsSettingsCard hostId={hostId} />,\n },\n // What shoppers may pay with (AGL-3629), beside the account the\n // money lands in.\n {\n size: { xs: 12 },\n children: <PaymentMethodsCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <StoreSettingsCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <ReturnSettingsCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <CustomerNotificationsCard hostId={hostId} />,\n },\n // Who may review, and the review-request email (AGL-3736).\n {\n size: { xs: 12 },\n children: <ReviewSettingsCard hostId={hostId} />,\n },\n { size: { xs: 12 }, children: <TaxSettingsCard hostId={hostId} /> },\n { size: { xs: 12 }, children: <LocationsCard hostId={hostId} /> },\n { size: { xs: 12 }, children: <RegistersCard hostId={hostId} /> },\n { size: { xs: 12 }, children: <PosStaffPinsCard hostId={hostId} /> },\n // A register's printers, cash drawer and status (AGL-3619), one\n // card per register, after the list that names them.\n {\n size: { xs: 12 },\n children: <RegisterHardwareCards hostId={hostId} />,\n },\n { size: { xs: 12 }, children: <PosDevicesCard hostId={hostId} /> },\n {\n size: { xs: 12 },\n children: <ShippingSettingsCard hostId={hostId} />,\n },\n // The store's own delivery (AGL-3624); pickup is per location,\n // on the Inventory locations card above.\n {\n size: { xs: 12 },\n children: <LocalDeliverySettingsCard hostId={hostId} />,\n },\n { size: { xs: 12 }, children: <SuppliersCard hostId={hostId} /> },\n { size: { xs: 12 }, children: <ShipStationCard hostId={hostId} /> },\n // ShippingEasy, with the merchant's own keys (AGL-3633).\n { size: { xs: 12 }, children: <ShippingEasyCard hostId={hostId} /> },\n // The store's own endpoints for order events (AGL-3611). Renders\n // nothing for a person who may not manage them.\n {\n size: { xs: 12 },\n children: <OrderWebhooksCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <CommerceSettingsZone hostId={hostId} />,\n },\n ]}\n />\n )\n case 'analytics':\n return (\n // Two cards since AGL-2440. The tax summary lives here rather\n // than under Settings → Tax because that tab CONFIGURES tax and\n // this REPORTS what was collected — a merchant reading a figure\n // they may file from should not be one field away from changing\n // the rate that produced it.\n <GridItems\n spacing={3}\n items={[\n {\n size: { xs: 12 },\n children: <CommerceAnalyticsCard hostId={hostId} />,\n },\n {\n size: { xs: 12 },\n children: <StorefrontTaxSummaryCard hostId={hostId} />,\n },\n ]}\n />\n )\n default:\n return null\n }\n}\n\n/**\n * Commerce console page (AGL-395): the Products management surface, owned by\n * the commerce plugin and rendered by the shell's generic plugin route. The\n * product editor's media browser is supplied by the shell's\n * ConsoleMediaPickerProvider (the media library is org/session coupled and\n * stays in the app).\n *\n * ## Sections are ROUTES (AGL-2501)\n *\n * The six sections hold thirty-two Firestore listens between them, and a tab\n * strip mounts every panel: opening Catalog also subscribed the orders,\n * promotions, reservations, settings and analytics cards. The `limit()` on\n * those queries is the bill — several ask for 500, and one load reached a\n * ceiling of ~4,460 documents to render one tab.\n *\n * `HubTabs lazy` bought that back as a flag somebody has to remember. A URL\n * per section makes it structural instead: the page builds one section's body\n * and the others do not exist to subscribe. Three things come with it that the\n * tab version faked — a section is linkable, the back button walks sections,\n * and which one is open is a fact about the URL rather than state kept in sync\n * with it. `commerce-console-read-cost.spec.tsx` meters it at the Firestore\n * boundary and refuses a section that listens before it is opened.\n *\n * The shell resolves the section, gates it and hands back the rail's hrefs;\n * this page only has to say what each section contains.\n */\nexport function CommerceConsolePage(props: ConsolePluginPageProps) {\n const { hostId, section, sections, basePath } = props\n\n /*\n * Nothing, deliberately, while the redirect is in flight.\n *\n * Rendering the default section here instead would issue that section's\n * listens on a URL that is about to be replaced — paying for Catalog on\n * every arrival at `/products`, which is every click of the nav tab.\n */\n if (!section || !sections?.length || !basePath) return null\n\n return (\n <HubSections sections={sections}>\n {sectionBody(section as CommerceConsoleSectionId, hostId)}\n </HubSections>\n )\n}\nCommerceConsolePage.displayName = 'CommerceConsolePage'\n\nexport default CommerceConsolePage\n"],"names":["GridItems","HubSections","CatalogOrganizationCard","CommerceAnalyticsCard","CustomerNotificationsCard","DiscountsCard","GiftCardsCard","HostCouponsCard","HostOrdersCard","LocationsCard","LocalDeliverySettingsCard","PickupDeliveryQueueCard","MemberPostsCard","OrderWebhooksCard","PaymentsSettingsCard","PosShiftHistoryCard","PosStaffPinsCard","PaymentMethodsCard","ProductsHubCard","RecoveryQueueCard","RegistersCard","RegisterHardwareCards","PosDevicesCard","ReservationsCard","ReturnSettingsCard","ReturnsCard","ReviewsModerationCard","ReviewSettingsCard","ShippingSettingsCard","ShipStationCard","ShippingEasyCard","StockMovementsCard","StoreSettingsCard","StorefrontTaxSummaryCard","SuppliersCard","TaxSettingsCard","COMMERCE_PROMOTIONS_ZONE","COMMERCE_SETTINGS_ZONE","useConsoleWidgetSlot","CommerceSettingsZone","props","WidgetSlot","slot","id","hostId","orgId","undefined","CommercePromotionsZone","sectionBody","section","spacing","items","size","xs","children","CommerceConsolePage","sections","basePath","length","displayName"],"mappings":"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"}
|
|
@@ -137,7 +137,11 @@ export interface ConsoleProductSummary {
|
|
|
137
137
|
/** Whether the product has a photo. */
|
|
138
138
|
hasPhoto: boolean;
|
|
139
139
|
}
|
|
140
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* A new product a widget asks the hub to create, always as a draft, at its
|
|
142
|
+
* suggested price when it carries one in range, else the default price
|
|
143
|
+
* (`proposedProductPriceUsd`, Zach 2026-10-12).
|
|
144
|
+
*/
|
|
141
145
|
export interface ConsoleProposedProduct {
|
|
142
146
|
name: string;
|
|
143
147
|
type: ConsoleProductType;
|
|
@@ -146,6 +150,8 @@ export interface ConsoleProposedProduct {
|
|
|
146
150
|
options: ConsoleProductOption[];
|
|
147
151
|
seoTitle: string;
|
|
148
152
|
seoDescription: string;
|
|
153
|
+
/** The suggested price for every variant, in dollars; absent or `null` is the default price. */
|
|
154
|
+
priceUsd?: number | null;
|
|
149
155
|
}
|
|
150
156
|
/** A discount a widget asks the hub to create, always switched off. */
|
|
151
157
|
export interface ConsoleProposedDiscount {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/product-zones.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\nimport {\n definePluginZone,\n registerPluginZone,\n} from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { BUNDLE_ID } from '../../constants/bundle-common'\n\n/**\n * The zones this plugin hosts on its product surfaces (AGL-2916): the product\n * editor, the products hub and the import wizard's After import step.\n *\n * Each is drawn through the shell's renderer (`useConsoleWidgetSlot`), so a\n * widget there passes the same enablement, entitlement and permission gates as\n * one on a console page. The props are this plugin's words for its catalog,\n * declared on the zone's token; a widget from another plugin restates the\n * shape it reads rather than importing this package. A widget proposes and\n * never writes: every write is this plugin's, with the guards it carries.\n */\n\n/** A product type, as the commerce catalog names it. */\nexport type ConsoleProductType = 'physical' | 'digital' | 'service'\n\n/** A product option axis: its name and the values a shopper picks from. */\nexport interface ConsoleProductOption {\n name: string\n values: string[]\n}\n\n/** A product as the commerce editor holds it: saved, or staged and unsaved. */\nexport interface ConsoleProductDraft {\n /** `null` for a product nobody has saved yet. */\n id: string | null\n name: string\n type: ConsoleProductType\n description: string\n tags: string[]\n categoryIds: string[]\n options: ConsoleProductOption[]\n /** The product's media, in order; the first is the photo the storefront leads with. */\n mediaUrls: string[]\n seoTitle: string\n seoDescription: string\n /**\n * What one unit ships as (AGL-3612): packed size and customs facts, as the\n * editor holds them. A side nobody set is `null`.\n */\n shipping: {\n lengthCm: number | null\n widthCm: number | null\n heightCm: number | null\n hsCode: string\n originCountry: string\n }\n /**\n * What shopping channels ask of it (AGL-3637), as the editor holds it. A\n * field nobody entered is `''`.\n */\n channel: {\n brand: string\n gtin: string\n mpn: string\n condition: '' | 'new' | 'refurbished' | 'used'\n googleProductCategory: string\n }\n}\n\n/**\n * Copy a widget proposes for one product. A field left out is left alone;\n * `optionNames` renames the product's options in order, keeping every\n * variant, and is ignored unless it names each option once.\n */\nexport interface ConsoleProductCopyValues {\n description?: string\n tags?: string[]\n categoryIds?: string[]\n optionNames?: string[]\n seoTitle?: string\n seoDescription?: string\n /**\n * Packed size and customs facts (AGL-3612), merged over the product's; a\n * side set to `null` is cleared.\n */\n shipping?: Partial<{\n lengthCm: number | null\n widthCm: number | null\n heightCm: number | null\n hsCode: string\n originCountry: string\n }>\n /**\n * Shopping-channel facts (AGL-3637), merged over the product's; a field\n * set to `''` is cleared.\n */\n channel?: Partial<{\n brand: string\n gtin: string\n mpn: string\n condition: '' | 'new' | 'refurbished' | 'used'\n googleProductCategory: string\n }>\n}\n\n/**\n * What the `productEditor` zone hands each widget: the product as the editor\n * holds it, and `proposeValues`, which stages copy in the editor as unsaved\n * edits. The zone sits under the product's description, tags and categories,\n * as the editor's search listing hosts `seoFields`. Save product is the\n * write, with the guards it carries.\n */\nexport interface ConsoleProductEditorZoneProps {\n hostId: string\n /** The org the page names; `undefined` where the host does not know it. */\n orgId: string | undefined\n product: ConsoleProductDraft\n /** The site's categories, as the editor's picker lists them. */\n categories: ReadonlyArray<{ id: string; name: string }>\n /**\n * Stages `values` in the editor as unsaved edits under `key`. Save product\n * is what stores them; a widget never writes the product itself.\n */\n proposeValues: (values: ConsoleProductCopyValues, key: string) => void\n}\n\n/** A catalog row as the products hub holds it. */\nexport interface ConsoleProductSummary {\n id: string\n name: string\n status: 'draft' | 'active' | 'archived'\n /** Whether a variant of the product has no price yet. */\n priceMissing: boolean\n /** Whether the product has a photo. */\n hasPhoto: boolean\n}\n\n/** A new product a widget asks the hub to create, always as a draft with no price. */\nexport interface ConsoleProposedProduct {\n name: string\n type: ConsoleProductType\n description: string\n tags: string[]\n options: ConsoleProductOption[]\n seoTitle: string\n seoDescription: string\n}\n\n/** A discount a widget asks the hub to create, always switched off. */\nexport interface ConsoleProposedDiscount {\n name: string\n /** The code a shopper types, or `null` for a discount that applies on its own. */\n code: string | null\n kind: 'percent' | 'fixed' | 'free_shipping'\n valuePct: number | null\n valueCents: number | null\n minSubtotalCents: number | null\n}\n\n/**\n * What the `productsHub` zone hands each widget: the catalog rows the hub\n * holds, the products its latest import created, and the hub's own writes a\n * widget may ask it to make — copy onto saved products, new draft products,\n * categories and switched-off discounts. The zone sits above the catalog\n * table. Each write is the hub's, with its seed, allowance and slug checks; a\n * widget writes nothing itself. Every write resolves once it is stored and\n * throws with a message a person can act on when the hub refuses it: an\n * allowance the plan has no room for, a product that is gone.\n *\n * The creates are safe to ask twice: a product or a category named like one\n * the site has, and a discount with the code (or, with no code, the name) of\n * one the site has, are passed over, so a proposal applied again, by anyone,\n * creates nothing it already created.\n */\nexport interface ConsoleProductsHubZoneProps {\n hostId: string\n /** The org the page names; `undefined` where the host does not know it. */\n orgId: string | undefined\n /** The catalog rows the hub holds, in its order. */\n products: readonly ConsoleProductSummary[]\n /**\n * The products the hub's latest import created in this visit, and the\n * options the `productImport` zone set for them; `null` before any import.\n * `key` is new for each import.\n */\n lastImport: {\n key: string\n productIds: readonly string[]\n options: Readonly<Record<string, boolean>>\n } | null\n /** Writes copy onto a saved product, read fresh from the store. */\n applyProductCopy: (productId: string, values: ConsoleProductCopyValues) => Promise<void>\n /** Creates each product as a draft with no price, and resolves with the ids it created. */\n createProductDrafts: (products: readonly ConsoleProposedProduct[]) => Promise<string[]>\n /** Creates categories by name, and resolves with how many it created. */\n createCategories: (names: readonly string[]) => Promise<number>\n /** Creates each discount switched off, and resolves with how many it created. */\n createDiscountDrafts: (discounts: readonly ConsoleProposedDiscount[]) => Promise<number>\n}\n\n/**\n * What the `productImport` zone hands each widget, in the import wizard's\n * After import step: how many products the import creates, and options a widget sets\n * for what happens to them once they land. The hub hands the options, with\n * the ids of the products the import created, to `productsHub` as its\n * `lastImport`.\n */\nexport interface ConsoleProductImportZoneProps {\n hostId: string\n /** The org the page names; `undefined` where the host does not know it. */\n orgId: string | undefined\n /** How many products the import creates. */\n count: number\n /** The options set for this import, by key. */\n options: Readonly<Record<string, boolean>>\n /** Sets one option; the hub carries it to `productsHub` with what the import created. */\n setOption: (key: string, on: boolean) => void\n}\n\nexport const PRODUCT_EDITOR_ZONE =\n definePluginZone<ConsoleProductEditorZoneProps>('productEditor')\n\nexport const PRODUCTS_HUB_ZONE =\n definePluginZone<ConsoleProductsHubZoneProps>('productsHub')\n\nexport const PRODUCT_IMPORT_ZONE =\n definePluginZone<ConsoleProductImportZoneProps>('productImport')\n\n/**\n * What the `productsCreate` zone hands each widget (AGL-3596): the site and\n * its org. The zone sits beside Add product on the Products page and in the\n * empty catalog: another way to start a product. A widget here writes nothing\n * through the page — anything it proposes is applied through `productsHub`.\n */\nexport interface ConsoleProductsCreateZoneProps {\n hostId: string\n /** The org the page names; `undefined` where the host does not know it. */\n orgId: string | undefined\n}\n\nexport const PRODUCTS_CREATE_ZONE =\n definePluginZone<ConsoleProductsCreateZoneProps>('productsCreate')\n\n/**\n * Declares the four zones, from the console registrar. The ids are the ones\n * widgets have always registered under, so a widget written for them keeps\n * working. Each is `bare`: the section is one item of a layout this plugin\n * draws — among the editor's fields, above the catalog table, in the import\n * wizard's After import step, in the row beside Add product — and a wrapper would add a gap the page\n * already spaces.\n */\nexport function registerCommerceZones(): void {\n const owner = { pluginId: BUNDLE_ID }\n registerPluginZone(\n {\n zone: PRODUCT_EDITOR_ZONE,\n label: 'Product editor',\n surface: 'console',\n layout: 'bare',\n description:\n 'Under a product’s description, tags and categories in the product editor. A widget here proposes copy for the fields through `proposeValues`, staged as unsaved edits; Save product is the write.',\n },\n owner,\n )\n registerPluginZone(\n {\n zone: PRODUCTS_HUB_ZONE,\n label: 'Products hub',\n surface: 'console',\n layout: 'bare',\n description:\n 'Above the catalog table on the Products page. A widget here proposes copy, draft products, categories and switched-off discounts, and asks the hub to write them; every write is the hub’s, with its allowance and slug checks.',\n },\n owner,\n )\n registerPluginZone(\n {\n zone: PRODUCTS_CREATE_ZONE,\n label: 'Start a product',\n surface: 'console',\n layout: 'bare',\n description:\n 'Beside Add product on the Products page, and in the empty catalog: another way to start a product. A widget here is handed the site and its org and writes nothing through the page.',\n },\n owner,\n )\n registerPluginZone(\n {\n zone: PRODUCT_IMPORT_ZONE,\n label: 'Product import',\n surface: 'console',\n layout: 'bare',\n description:\n 'In the product import wizard’s After import step. A widget here sets options for what happens to the imported products once they land; the hub hands them to `productsHub` with the ids the import created.',\n },\n owner,\n )\n}\n"],"names":["definePluginZone","registerPluginZone","BUNDLE_ID","PRODUCT_EDITOR_ZONE","PRODUCTS_HUB_ZONE","PRODUCT_IMPORT_ZONE","PRODUCTS_CREATE_ZONE","registerCommerceZones","owner","pluginId","zone","label","surface","layout","description"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,gBAAgB,EAChBC,kBAAkB,QACb,2CAA0C;AACjD,SAASC,SAAS,QAAQ,mCAA+B;AAmNzD,OAAO,MAAMC,sBACXH,iBAAgD,iBAAgB;AAElE,OAAO,MAAMI,oBACXJ,iBAA8C,eAAc;AAE9D,OAAO,MAAMK,sBACXL,iBAAgD,iBAAgB;AAclE,OAAO,MAAMM,uBACXN,iBAAiD,kBAAiB;AAEpE;;;;;;;CAOC,GACD,OAAO,SAASO;IACd,MAAMC,QAAQ;QAAEC,UAAUP;IAAU;IACpCD,mBACE;QACES,MAAMP;QACNQ,OAAO;QACPC,SAAS;QACTC,QAAQ;QACRC,aACE;IACJ,GACAN;IAEFP,mBACE;QACES,MAAMN;QACNO,OAAO;QACPC,SAAS;QACTC,QAAQ;QACRC,aACE;IACJ,GACAN;IAEFP,mBACE;QACES,MAAMJ;QACNK,OAAO;QACPC,SAAS;QACTC,QAAQ;QACRC,aACE;IACJ,GACAN;IAEFP,mBACE;QACES,MAAML;QACNM,OAAO;QACPC,SAAS;QACTC,QAAQ;QACRC,aACE;IACJ,GACAN;AAEJ"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/product-zones.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\nimport {\n definePluginZone,\n registerPluginZone,\n} from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { BUNDLE_ID } from '../../constants/bundle-common'\n\n/**\n * The zones this plugin hosts on its product surfaces (AGL-2916): the product\n * editor, the products hub and the import wizard's After import step.\n *\n * Each is drawn through the shell's renderer (`useConsoleWidgetSlot`), so a\n * widget there passes the same enablement, entitlement and permission gates as\n * one on a console page. The props are this plugin's words for its catalog,\n * declared on the zone's token; a widget from another plugin restates the\n * shape it reads rather than importing this package. A widget proposes and\n * never writes: every write is this plugin's, with the guards it carries.\n */\n\n/** A product type, as the commerce catalog names it. */\nexport type ConsoleProductType = 'physical' | 'digital' | 'service'\n\n/** A product option axis: its name and the values a shopper picks from. */\nexport interface ConsoleProductOption {\n name: string\n values: string[]\n}\n\n/** A product as the commerce editor holds it: saved, or staged and unsaved. */\nexport interface ConsoleProductDraft {\n /** `null` for a product nobody has saved yet. */\n id: string | null\n name: string\n type: ConsoleProductType\n description: string\n tags: string[]\n categoryIds: string[]\n options: ConsoleProductOption[]\n /** The product's media, in order; the first is the photo the storefront leads with. */\n mediaUrls: string[]\n seoTitle: string\n seoDescription: string\n /**\n * What one unit ships as (AGL-3612): packed size and customs facts, as the\n * editor holds them. A side nobody set is `null`.\n */\n shipping: {\n lengthCm: number | null\n widthCm: number | null\n heightCm: number | null\n hsCode: string\n originCountry: string\n }\n /**\n * What shopping channels ask of it (AGL-3637), as the editor holds it. A\n * field nobody entered is `''`.\n */\n channel: {\n brand: string\n gtin: string\n mpn: string\n condition: '' | 'new' | 'refurbished' | 'used'\n googleProductCategory: string\n }\n}\n\n/**\n * Copy a widget proposes for one product. A field left out is left alone;\n * `optionNames` renames the product's options in order, keeping every\n * variant, and is ignored unless it names each option once.\n */\nexport interface ConsoleProductCopyValues {\n description?: string\n tags?: string[]\n categoryIds?: string[]\n optionNames?: string[]\n seoTitle?: string\n seoDescription?: string\n /**\n * Packed size and customs facts (AGL-3612), merged over the product's; a\n * side set to `null` is cleared.\n */\n shipping?: Partial<{\n lengthCm: number | null\n widthCm: number | null\n heightCm: number | null\n hsCode: string\n originCountry: string\n }>\n /**\n * Shopping-channel facts (AGL-3637), merged over the product's; a field\n * set to `''` is cleared.\n */\n channel?: Partial<{\n brand: string\n gtin: string\n mpn: string\n condition: '' | 'new' | 'refurbished' | 'used'\n googleProductCategory: string\n }>\n}\n\n/**\n * What the `productEditor` zone hands each widget: the product as the editor\n * holds it, and `proposeValues`, which stages copy in the editor as unsaved\n * edits. The zone sits under the product's description, tags and categories,\n * as the editor's search listing hosts `seoFields`. Save product is the\n * write, with the guards it carries.\n */\nexport interface ConsoleProductEditorZoneProps {\n hostId: string\n /** The org the page names; `undefined` where the host does not know it. */\n orgId: string | undefined\n product: ConsoleProductDraft\n /** The site's categories, as the editor's picker lists them. */\n categories: ReadonlyArray<{ id: string; name: string }>\n /**\n * Stages `values` in the editor as unsaved edits under `key`. Save product\n * is what stores them; a widget never writes the product itself.\n */\n proposeValues: (values: ConsoleProductCopyValues, key: string) => void\n}\n\n/** A catalog row as the products hub holds it. */\nexport interface ConsoleProductSummary {\n id: string\n name: string\n status: 'draft' | 'active' | 'archived'\n /** Whether a variant of the product has no price yet. */\n priceMissing: boolean\n /** Whether the product has a photo. */\n hasPhoto: boolean\n}\n\n/**\n * A new product a widget asks the hub to create, always as a draft, at its\n * suggested price when it carries one in range, else the default price\n * (`proposedProductPriceUsd`, Zach 2026-10-12).\n */\nexport interface ConsoleProposedProduct {\n name: string\n type: ConsoleProductType\n description: string\n tags: string[]\n options: ConsoleProductOption[]\n seoTitle: string\n seoDescription: string\n /** The suggested price for every variant, in dollars; absent or `null` is the default price. */\n priceUsd?: number | null\n}\n\n/** A discount a widget asks the hub to create, always switched off. */\nexport interface ConsoleProposedDiscount {\n name: string\n /** The code a shopper types, or `null` for a discount that applies on its own. */\n code: string | null\n kind: 'percent' | 'fixed' | 'free_shipping'\n valuePct: number | null\n valueCents: number | null\n minSubtotalCents: number | null\n}\n\n/**\n * What the `productsHub` zone hands each widget: the catalog rows the hub\n * holds, the products its latest import created, and the hub's own writes a\n * widget may ask it to make — copy onto saved products, new draft products,\n * categories and switched-off discounts. The zone sits above the catalog\n * table. Each write is the hub's, with its seed, allowance and slug checks; a\n * widget writes nothing itself. Every write resolves once it is stored and\n * throws with a message a person can act on when the hub refuses it: an\n * allowance the plan has no room for, a product that is gone.\n *\n * The creates are safe to ask twice: a product or a category named like one\n * the site has, and a discount with the code (or, with no code, the name) of\n * one the site has, are passed over, so a proposal applied again, by anyone,\n * creates nothing it already created.\n */\nexport interface ConsoleProductsHubZoneProps {\n hostId: string\n /** The org the page names; `undefined` where the host does not know it. */\n orgId: string | undefined\n /** The catalog rows the hub holds, in its order. */\n products: readonly ConsoleProductSummary[]\n /**\n * The products the hub's latest import created in this visit, and the\n * options the `productImport` zone set for them; `null` before any import.\n * `key` is new for each import.\n */\n lastImport: {\n key: string\n productIds: readonly string[]\n options: Readonly<Record<string, boolean>>\n } | null\n /** Writes copy onto a saved product, read fresh from the store. */\n applyProductCopy: (productId: string, values: ConsoleProductCopyValues) => Promise<void>\n /** Creates each product as a draft with no price, and resolves with the ids it created. */\n createProductDrafts: (products: readonly ConsoleProposedProduct[]) => Promise<string[]>\n /** Creates categories by name, and resolves with how many it created. */\n createCategories: (names: readonly string[]) => Promise<number>\n /** Creates each discount switched off, and resolves with how many it created. */\n createDiscountDrafts: (discounts: readonly ConsoleProposedDiscount[]) => Promise<number>\n}\n\n/**\n * What the `productImport` zone hands each widget, in the import wizard's\n * After import step: how many products the import creates, and options a widget sets\n * for what happens to them once they land. The hub hands the options, with\n * the ids of the products the import created, to `productsHub` as its\n * `lastImport`.\n */\nexport interface ConsoleProductImportZoneProps {\n hostId: string\n /** The org the page names; `undefined` where the host does not know it. */\n orgId: string | undefined\n /** How many products the import creates. */\n count: number\n /** The options set for this import, by key. */\n options: Readonly<Record<string, boolean>>\n /** Sets one option; the hub carries it to `productsHub` with what the import created. */\n setOption: (key: string, on: boolean) => void\n}\n\nexport const PRODUCT_EDITOR_ZONE =\n definePluginZone<ConsoleProductEditorZoneProps>('productEditor')\n\nexport const PRODUCTS_HUB_ZONE =\n definePluginZone<ConsoleProductsHubZoneProps>('productsHub')\n\nexport const PRODUCT_IMPORT_ZONE =\n definePluginZone<ConsoleProductImportZoneProps>('productImport')\n\n/**\n * What the `productsCreate` zone hands each widget (AGL-3596): the site and\n * its org. The zone sits beside Add product on the Products page and in the\n * empty catalog: another way to start a product. A widget here writes nothing\n * through the page — anything it proposes is applied through `productsHub`.\n */\nexport interface ConsoleProductsCreateZoneProps {\n hostId: string\n /** The org the page names; `undefined` where the host does not know it. */\n orgId: string | undefined\n}\n\nexport const PRODUCTS_CREATE_ZONE =\n definePluginZone<ConsoleProductsCreateZoneProps>('productsCreate')\n\n/**\n * Declares the four zones, from the console registrar. The ids are the ones\n * widgets have always registered under, so a widget written for them keeps\n * working. Each is `bare`: the section is one item of a layout this plugin\n * draws — among the editor's fields, above the catalog table, in the import\n * wizard's After import step, in the row beside Add product — and a wrapper would add a gap the page\n * already spaces.\n */\nexport function registerCommerceZones(): void {\n const owner = { pluginId: BUNDLE_ID }\n registerPluginZone(\n {\n zone: PRODUCT_EDITOR_ZONE,\n label: 'Product editor',\n surface: 'console',\n layout: 'bare',\n description:\n 'Under a product’s description, tags and categories in the product editor. A widget here proposes copy for the fields through `proposeValues`, staged as unsaved edits; Save product is the write.',\n },\n owner,\n )\n registerPluginZone(\n {\n zone: PRODUCTS_HUB_ZONE,\n label: 'Products hub',\n surface: 'console',\n layout: 'bare',\n description:\n 'Above the catalog table on the Products page. A widget here proposes copy, draft products, categories and switched-off discounts, and asks the hub to write them; every write is the hub’s, with its allowance and slug checks.',\n },\n owner,\n )\n registerPluginZone(\n {\n zone: PRODUCTS_CREATE_ZONE,\n label: 'Start a product',\n surface: 'console',\n layout: 'bare',\n description:\n 'Beside Add product on the Products page, and in the empty catalog: another way to start a product. A widget here is handed the site and its org and writes nothing through the page.',\n },\n owner,\n )\n registerPluginZone(\n {\n zone: PRODUCT_IMPORT_ZONE,\n label: 'Product import',\n surface: 'console',\n layout: 'bare',\n description:\n 'In the product import wizard’s After import step. A widget here sets options for what happens to the imported products once they land; the hub hands them to `productsHub` with the ids the import created.',\n },\n owner,\n )\n}\n"],"names":["definePluginZone","registerPluginZone","BUNDLE_ID","PRODUCT_EDITOR_ZONE","PRODUCTS_HUB_ZONE","PRODUCT_IMPORT_ZONE","PRODUCTS_CREATE_ZONE","registerCommerceZones","owner","pluginId","zone","label","surface","layout","description"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,gBAAgB,EAChBC,kBAAkB,QACb,2CAA0C;AACjD,SAASC,SAAS,QAAQ,mCAA+B;AAyNzD,OAAO,MAAMC,sBACXH,iBAAgD,iBAAgB;AAElE,OAAO,MAAMI,oBACXJ,iBAA8C,eAAc;AAE9D,OAAO,MAAMK,sBACXL,iBAAgD,iBAAgB;AAclE,OAAO,MAAMM,uBACXN,iBAAiD,kBAAiB;AAEpE;;;;;;;CAOC,GACD,OAAO,SAASO;IACd,MAAMC,QAAQ;QAAEC,UAAUP;IAAU;IACpCD,mBACE;QACES,MAAMP;QACNQ,OAAO;QACPC,SAAS;QACTC,QAAQ;QACRC,aACE;IACJ,GACAN;IAEFP,mBACE;QACES,MAAMN;QACNO,OAAO;QACPC,SAAS;QACTC,QAAQ;QACRC,aACE;IACJ,GACAN;IAEFP,mBACE;QACES,MAAMJ;QACNK,OAAO;QACPC,SAAS;QACTC,QAAQ;QACRC,aACE;IACJ,GACAN;IAEFP,mBACE;QACES,MAAML;QACNM,OAAO;QACPC,SAAS;QACTC,QAAQ;QACRC,aACE;IACJ,GACAN;AAEJ"}
|
|
@@ -12,8 +12,9 @@ import * as CommerceModel from '../../model';
|
|
|
12
12
|
* loaded is kept.
|
|
13
13
|
* - **A proposed product** is created through the quota-enforcing resources
|
|
14
14
|
* API, as Add product and Import create one, after the hub's own allowance
|
|
15
|
-
* check for the batch. It is a draft at
|
|
16
|
-
* (`defaultPricedProductDraft`,
|
|
15
|
+
* check for the batch. It is a draft at its suggested price, else the
|
|
16
|
+
* default price (`proposedProductPriceUsd`, `defaultPricedProductDraft`,
|
|
17
|
+
* AGL-3676), for the owner to change.
|
|
17
18
|
* - **A category** is created as the catalog card creates one.
|
|
18
19
|
* - **A discount** is created switched off (`switchedOffDiscount`), for a
|
|
19
20
|
* person to switch on from the discounts card.
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
18
19
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
19
20
|
import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
|
|
20
21
|
import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
|
|
@@ -125,7 +126,14 @@ export function ProductsHubZone(props) {
|
|
|
125
126
|
const slugs = productSlugLedger(firestore, hostId);
|
|
126
127
|
const created = [];
|
|
127
128
|
try {
|
|
128
|
-
|
|
129
|
+
// At its suggested price, held to range here, else the default
|
|
130
|
+
// (Zach, 2026-10-12). Only the proposal's own fields reach the draft.
|
|
131
|
+
const drafts = fresh.map((_0)=>{
|
|
132
|
+
let { priceUsd } = _0, proposal = _object_without_properties_loose(_0, [
|
|
133
|
+
"priceUsd"
|
|
134
|
+
]);
|
|
135
|
+
return CommerceModel.defaultPricedProductDraft(proposal, NO_SLUGS, Date.now(), CommerceModel.proposedProductPriceUsd(priceUsd));
|
|
136
|
+
});
|
|
129
137
|
await slugs.ask(drafts.map((draft)=>draft.slug));
|
|
130
138
|
const smartCollections = await readSmartCollections(firestore, hostId);
|
|
131
139
|
for (const draft of drafts){
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/products-hub-zone.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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n PRODUCTS_HUB_ZONE,\n type ConsoleProductCopyValues,\n type ConsoleProductSummary,\n type ConsoleProductsHubZoneProps,\n type ConsoleProposedDiscount,\n type ConsoleProposedProduct,\n} from './product-zones'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n useFirestore,\n useHostResourceApi,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n commitWithSiteWideEntry,\n settleSiteWideChange,\n} from '@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change'\nimport {\n collection,\n doc,\n getDocs,\n limit,\n query,\n runTransaction,\n setDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo } from 'react'\nimport * as CommerceModel from '../../model'\nimport { productSlugLedger } from './product-slugs'\nimport { readSmartCollections } from './smart-collections'\n\n/**\n * The products hub's zone (AGL-2916): the page of catalog rows the hub's table\n * shows, and the writes a widget there may ask the hub to make. A widget proposes; these are the\n * hub's writes, each through the door the hub's own controls use.\n *\n * - **Copy onto a saved product** is a transaction over the product as the\n * store holds it now, never a row the table was seeded with: it changes\n * only the fields the copy names, and renames options through\n * `renameProductOptions`, so a sale or a colleague's edit since the table\n * loaded is kept.\n * - **A proposed product** is created through the quota-enforcing resources\n * API, as Add product and Import create one, after the hub's own allowance\n * check for the batch. It is a draft at the default price\n * (`defaultPricedProductDraft`, AGL-3676), for the owner to change.\n * - **A category** is created as the catalog card creates one.\n * - **A discount** is created switched off (`switchedOffDiscount`), for a\n * person to switch on from the discounts card.\n *\n * Each create passes over what the site already has — a product of the same\n * name, a category of the same name, a discount with the same code or, with no\n * code, the same name — so a proposal applied twice, by anyone, creates\n * nothing twice. Each check is a read made when a person applies, never while\n * the hub is open, and reaches past the page the hub shows: a product is\n * looked up by its search key, and its slug by the slugs it could take\n * (`productSlugLedger`, AGL-3321).\n */\n\ntype ProductRow = CommerceModel.HostProduct & { $id: string }\n\n/** The hub's allowance check for `count` more products: `null` while the plan loads. */\nexport type ProductsRoomCheck = (count: number) => { allowed: boolean; limit: number } | null\n\nexport interface ProductsHubZoneProps {\n hostId: string\n /**\n * The page of catalog rows the hub's table shows, in its order — the\n * products its filters and search matched, one page at a time (AGL-3321).\n * Never the catalog: nothing here decides what exists from these rows.\n */\n products: readonly ProductRow[]\n roomFor: ProductsRoomCheck\n lastImport: ConsoleProductsHubZoneProps['lastImport']\n /** Called once a create has moved the catalog's count. */\n onCreated: () => void\n}\n\nexport const PRODUCT_GONE_COPY = 'This product is no longer in the catalog.'\nexport const PLAN_LOADING_COPY = 'Your plan is still loading. Try again in a moment.'\n\n/** How many of the site's categories the duplicate check reads. */\nconst CATEGORY_NAMES_READ = 250\n\n/** How a name is compared for a duplicate: case and spacing aside. */\nconst nameKey = (name: string) => name.replace(/\\s+/g, ' ').trim().toLowerCase()\n\n/** The most values one Firestore `in` clause takes. */\nconst IN_CLAUSE_MAX = 10\n\n/** No slug taken: a draft's own base slug, before the store is asked. */\nconst NO_SLUGS: ReadonlySet<string> = new Set()\n\nexport function ProductsHubZone(props: ProductsHubZoneProps) {\n const { hostId, products, roomFor, lastImport, onCreated } = props\n const Slot = useConsoleWidgetSlot()\n const firestore = useFirestore()\n const { data: user } = useUser()\n const createHostResource = useHostResourceApi()\n\n const summaries = useMemo<ConsoleProductSummary[]>(\n () =>\n products.map((product) => ({\n id: product.$id,\n name: product.name,\n status: product.status,\n priceMissing: CommerceModel.productPriceMissing(product),\n hasPhoto: Boolean(product.mediaUrls?.[0] ?? product.imageUrl),\n })),\n [products],\n )\n\n const applyProductCopy = useCallback(\n async (productId: string, values: ConsoleProductCopyValues) => {\n const ref = doc(firestore, 'hosts', hostId, 'products', productId)\n const smartCollections = await readSmartCollections(firestore, hostId)\n // The product page renders the copy with no publish step, so the\n // transaction carries the site's cache drop with it (AGL-3386).\n const entry = await commitWithSiteWideEntry(hostId, (stage) =>\n runTransaction(firestore, async (transaction) => {\n const snapshot = await transaction.get(ref)\n const stored = snapshot.exists() ? (snapshot.data() as CommerceModel.HostProduct) : null\n if (!stored || stored.deletedAt) throw new Error(PRODUCT_GONE_COPY)\n const lifted = CommerceModel.liftLegacyProduct(stored)\n const patch = CommerceModel.productCopyPatch(lifted, values)\n if (!Object.keys(patch).length) return\n // Tags and categories may move, and with them its smart collections\n // (AGL-3321).\n // Written only when it moved, so a copy that changes no rule input\n // writes only the copy.\n const collectionIds = CommerceModel.productCollectionIds(\n { ...lifted, ...patch },\n smartCollections,\n )\n const held = stored.collectionIds ?? []\n const moved =\n held.length !== collectionIds.length ||\n collectionIds.some((id, at) => held[at] !== id)\n transaction.update(ref, {\n ...patch,\n ...(moved ? { collectionIds } : {}),\n updatedAtMs: Date.now(),\n updatedAt: Timestamp.now(),\n })\n stage?.(transaction, firestore)\n }),\n )\n settleSiteWideChange({ user, hostId, entry })\n },\n [firestore, hostId, user],\n )\n\n const createProductDrafts = useCallback(\n async (proposals: readonly ConsoleProposedProduct[]) => {\n // What exists is asked of the store, never read off the page on screen.\n const held = new Set<string>()\n const searchKeys = [\n ...new Set(proposals.map((proposal) => CommerceModel.productSearchFields({ name: proposal.name.trim() }).nameLower)),\n ].filter(Boolean)\n const productsRef = collection(firestore, 'hosts', hostId, 'products')\n const stored = await Promise.all(\n Array.from({ length: Math.ceil(searchKeys.length / IN_CLAUSE_MAX) }, (_, index) =>\n getDocs(query(productsRef, where('nameLower', 'in', searchKeys.slice(index * IN_CLAUSE_MAX, (index + 1) * IN_CLAUSE_MAX)))),\n ),\n )\n for (const read of stored) {\n for (const existing of read.docs) {\n if (!existing.get('deletedAt')) held.add(nameKey(String(existing.get('name') ?? '')))\n }\n }\n const fresh = proposals.filter((proposal) => {\n const key = nameKey(proposal.name)\n if (!key || held.has(key)) return false\n held.add(key)\n return true\n })\n if (!fresh.length) return []\n const room = roomFor(fresh.length)\n if (!room) throw new Error(PLAN_LOADING_COPY)\n if (!room.allowed) {\n throw new Error(\n `These products need ${fresh.length} product slots — your plan allows ` +\n `${room.limit}. See Billing to upgrade.`,\n )\n }\n // Each draft's slug is asked of the store: the base slug its name makes,\n // then `-2`, `-3`, … until one no product holds, live or deleted.\n const slugs = productSlugLedger(firestore, hostId)\n const created: string[] = []\n try {\n const drafts = fresh.map((proposal) =>\n CommerceModel.defaultPricedProductDraft(proposal, NO_SLUGS, Date.now()),\n )\n await slugs.ask(drafts.map((draft) => draft.slug))\n const smartCollections = await readSmartCollections(firestore, hostId)\n for (const draft of drafts) {\n const slug = await slugs.claim(draft.slug)\n const collectionIds = CommerceModel.productCollectionIds(draft, smartCollections)\n const { id } = await createHostResource({\n hostId,\n resource: 'product',\n data: { ...draft, slug, collectionIds },\n })\n created.push(id)\n }\n } finally {\n if (created.length) onCreated()\n }\n return created\n },\n [roomFor, createHostResource, firestore, hostId, onCreated],\n )\n\n const createCategories = useCallback(\n async (names: readonly string[]) => {\n const existing = await getDocs(\n query(collection(firestore, 'hosts', hostId, 'productCategories'), limit(CATEGORY_NAMES_READ)),\n )\n const held = new Set(existing.docs.map((category) => nameKey(String(category.get('name') ?? ''))))\n let created = 0\n for (const raw of names) {\n const name = raw.trim().slice(0, 80)\n if (!name || held.has(nameKey(name))) continue\n held.add(nameKey(name))\n await setDoc(doc(firestore, 'hosts', hostId, 'productCategories', createResourceUid()), {\n name,\n slug: CommerceModel.commerceSlug(name),\n parentId: null,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n })\n created += 1\n }\n return created\n },\n [firestore, hostId],\n )\n\n const createDiscountDrafts = useCallback(\n async (discounts: readonly ConsoleProposedDiscount[]) => {\n const drafts = discounts\n .map((proposal) => CommerceModel.switchedOffDiscount(proposal))\n .filter((discount): discount is CommerceModel.StoredDiscount => discount !== null)\n if (!drafts.length) return 0\n const codes = [...new Set(drafts.map((discount) => discount.code).filter((code): code is string => Boolean(code)))]\n // A stored name is compared as typed, and the query matches it exactly.\n const names = [...new Set(drafts.filter((discount) => !discount.code).map((discount) => discount.name ?? ''))]\n const discountsRef = collection(firestore, 'hosts', hostId, 'discounts')\n const [byCode, byName] = await Promise.all([\n codes.length ? getDocs(query(discountsRef, where('code', 'in', codes.slice(0, 10)))) : null,\n names.length ? getDocs(query(discountsRef, where('name', 'in', names.slice(0, 10)))) : null,\n ])\n const heldCodes = new Set((byCode?.docs ?? []).map((existing) => String(existing.get('code'))))\n const heldNames = new Set((byName?.docs ?? []).map((existing) => nameKey(String(existing.get('name') ?? ''))))\n let created = 0\n for (const discount of drafts) {\n const name = nameKey(discount.name ?? '')\n if (discount.code ? heldCodes.has(discount.code) : heldNames.has(name)) continue\n if (discount.code) heldCodes.add(discount.code)\n else heldNames.add(name)\n await setDoc(doc(firestore, 'hosts', hostId, 'discounts', createResourceUid()), discount)\n created += 1\n }\n return created\n },\n [firestore, hostId],\n )\n\n if (!Slot) return null\n return (\n <Slot\n slot={PRODUCTS_HUB_ZONE.id}\n hostId={hostId}\n orgId={undefined}\n products={summaries}\n lastImport={lastImport}\n applyProductCopy={applyProductCopy}\n createProductDrafts={createProductDrafts}\n createCategories={createCategories}\n createDiscountDrafts={createDiscountDrafts}\n />\n )\n}\nProductsHubZone.displayName = 'ProductsHubZone'\n\nexport default ProductsHubZone\n"],"names":["createResourceUid","useConsoleWidgetSlot","PRODUCTS_HUB_ZONE","Timestamp","useFirestore","useHostResourceApi","useUser","commitWithSiteWideEntry","settleSiteWideChange","collection","doc","getDocs","limit","query","runTransaction","setDoc","where","useCallback","useMemo","CommerceModel","productSlugLedger","readSmartCollections","PRODUCT_GONE_COPY","PLAN_LOADING_COPY","CATEGORY_NAMES_READ","nameKey","name","replace","trim","toLowerCase","IN_CLAUSE_MAX","NO_SLUGS","Set","ProductsHubZone","props","hostId","products","roomFor","lastImport","onCreated","Slot","firestore","data","user","createHostResource","summaries","map","product","id","$id","status","priceMissing","productPriceMissing","hasPhoto","Boolean","mediaUrls","imageUrl","applyProductCopy","productId","values","ref","smartCollections","entry","stage","transaction","stored","snapshot","get","exists","deletedAt","Error","lifted","liftLegacyProduct","patch","productCopyPatch","Object","keys","length","collectionIds","productCollectionIds","held","moved","some","at","update","updatedAtMs","Date","now","updatedAt","createProductDrafts","proposals","searchKeys","proposal","productSearchFields","nameLower","filter","productsRef","Promise","all","Array","from","Math","ceil","_","index","slice","read","existing","docs","add","String","fresh","key","has","room","allowed","slugs","created","drafts","defaultPricedProductDraft","ask","draft","slug","claim","resource","push","createCategories","names","category","raw","commerceSlug","parentId","createdAt","createDiscountDrafts","discounts","switchedOffDiscount","discount","codes","code","discountsRef","byCode","byName","heldCodes","heldNames","slot","orgId","undefined","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/products-hub-zone.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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n PRODUCTS_HUB_ZONE,\n type ConsoleProductCopyValues,\n type ConsoleProductSummary,\n type ConsoleProductsHubZoneProps,\n type ConsoleProposedDiscount,\n type ConsoleProposedProduct,\n} from './product-zones'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n useFirestore,\n useHostResourceApi,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n commitWithSiteWideEntry,\n settleSiteWideChange,\n} from '@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change'\nimport {\n collection,\n doc,\n getDocs,\n limit,\n query,\n runTransaction,\n setDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo } from 'react'\nimport * as CommerceModel from '../../model'\nimport { productSlugLedger } from './product-slugs'\nimport { readSmartCollections } from './smart-collections'\n\n/**\n * The products hub's zone (AGL-2916): the page of catalog rows the hub's table\n * shows, and the writes a widget there may ask the hub to make. A widget proposes; these are the\n * hub's writes, each through the door the hub's own controls use.\n *\n * - **Copy onto a saved product** is a transaction over the product as the\n * store holds it now, never a row the table was seeded with: it changes\n * only the fields the copy names, and renames options through\n * `renameProductOptions`, so a sale or a colleague's edit since the table\n * loaded is kept.\n * - **A proposed product** is created through the quota-enforcing resources\n * API, as Add product and Import create one, after the hub's own allowance\n * check for the batch. It is a draft at its suggested price, else the\n * default price (`proposedProductPriceUsd`, `defaultPricedProductDraft`,\n * AGL-3676), for the owner to change.\n * - **A category** is created as the catalog card creates one.\n * - **A discount** is created switched off (`switchedOffDiscount`), for a\n * person to switch on from the discounts card.\n *\n * Each create passes over what the site already has — a product of the same\n * name, a category of the same name, a discount with the same code or, with no\n * code, the same name — so a proposal applied twice, by anyone, creates\n * nothing twice. Each check is a read made when a person applies, never while\n * the hub is open, and reaches past the page the hub shows: a product is\n * looked up by its search key, and its slug by the slugs it could take\n * (`productSlugLedger`, AGL-3321).\n */\n\ntype ProductRow = CommerceModel.HostProduct & { $id: string }\n\n/** The hub's allowance check for `count` more products: `null` while the plan loads. */\nexport type ProductsRoomCheck = (count: number) => { allowed: boolean; limit: number } | null\n\nexport interface ProductsHubZoneProps {\n hostId: string\n /**\n * The page of catalog rows the hub's table shows, in its order — the\n * products its filters and search matched, one page at a time (AGL-3321).\n * Never the catalog: nothing here decides what exists from these rows.\n */\n products: readonly ProductRow[]\n roomFor: ProductsRoomCheck\n lastImport: ConsoleProductsHubZoneProps['lastImport']\n /** Called once a create has moved the catalog's count. */\n onCreated: () => void\n}\n\nexport const PRODUCT_GONE_COPY = 'This product is no longer in the catalog.'\nexport const PLAN_LOADING_COPY = 'Your plan is still loading. Try again in a moment.'\n\n/** How many of the site's categories the duplicate check reads. */\nconst CATEGORY_NAMES_READ = 250\n\n/** How a name is compared for a duplicate: case and spacing aside. */\nconst nameKey = (name: string) => name.replace(/\\s+/g, ' ').trim().toLowerCase()\n\n/** The most values one Firestore `in` clause takes. */\nconst IN_CLAUSE_MAX = 10\n\n/** No slug taken: a draft's own base slug, before the store is asked. */\nconst NO_SLUGS: ReadonlySet<string> = new Set()\n\nexport function ProductsHubZone(props: ProductsHubZoneProps) {\n const { hostId, products, roomFor, lastImport, onCreated } = props\n const Slot = useConsoleWidgetSlot()\n const firestore = useFirestore()\n const { data: user } = useUser()\n const createHostResource = useHostResourceApi()\n\n const summaries = useMemo<ConsoleProductSummary[]>(\n () =>\n products.map((product) => ({\n id: product.$id,\n name: product.name,\n status: product.status,\n priceMissing: CommerceModel.productPriceMissing(product),\n hasPhoto: Boolean(product.mediaUrls?.[0] ?? product.imageUrl),\n })),\n [products],\n )\n\n const applyProductCopy = useCallback(\n async (productId: string, values: ConsoleProductCopyValues) => {\n const ref = doc(firestore, 'hosts', hostId, 'products', productId)\n const smartCollections = await readSmartCollections(firestore, hostId)\n // The product page renders the copy with no publish step, so the\n // transaction carries the site's cache drop with it (AGL-3386).\n const entry = await commitWithSiteWideEntry(hostId, (stage) =>\n runTransaction(firestore, async (transaction) => {\n const snapshot = await transaction.get(ref)\n const stored = snapshot.exists() ? (snapshot.data() as CommerceModel.HostProduct) : null\n if (!stored || stored.deletedAt) throw new Error(PRODUCT_GONE_COPY)\n const lifted = CommerceModel.liftLegacyProduct(stored)\n const patch = CommerceModel.productCopyPatch(lifted, values)\n if (!Object.keys(patch).length) return\n // Tags and categories may move, and with them its smart collections\n // (AGL-3321).\n // Written only when it moved, so a copy that changes no rule input\n // writes only the copy.\n const collectionIds = CommerceModel.productCollectionIds(\n { ...lifted, ...patch },\n smartCollections,\n )\n const held = stored.collectionIds ?? []\n const moved =\n held.length !== collectionIds.length ||\n collectionIds.some((id, at) => held[at] !== id)\n transaction.update(ref, {\n ...patch,\n ...(moved ? { collectionIds } : {}),\n updatedAtMs: Date.now(),\n updatedAt: Timestamp.now(),\n })\n stage?.(transaction, firestore)\n }),\n )\n settleSiteWideChange({ user, hostId, entry })\n },\n [firestore, hostId, user],\n )\n\n const createProductDrafts = useCallback(\n async (proposals: readonly ConsoleProposedProduct[]) => {\n // What exists is asked of the store, never read off the page on screen.\n const held = new Set<string>()\n const searchKeys = [\n ...new Set(proposals.map((proposal) => CommerceModel.productSearchFields({ name: proposal.name.trim() }).nameLower)),\n ].filter(Boolean)\n const productsRef = collection(firestore, 'hosts', hostId, 'products')\n const stored = await Promise.all(\n Array.from({ length: Math.ceil(searchKeys.length / IN_CLAUSE_MAX) }, (_, index) =>\n getDocs(query(productsRef, where('nameLower', 'in', searchKeys.slice(index * IN_CLAUSE_MAX, (index + 1) * IN_CLAUSE_MAX)))),\n ),\n )\n for (const read of stored) {\n for (const existing of read.docs) {\n if (!existing.get('deletedAt')) held.add(nameKey(String(existing.get('name') ?? '')))\n }\n }\n const fresh = proposals.filter((proposal) => {\n const key = nameKey(proposal.name)\n if (!key || held.has(key)) return false\n held.add(key)\n return true\n })\n if (!fresh.length) return []\n const room = roomFor(fresh.length)\n if (!room) throw new Error(PLAN_LOADING_COPY)\n if (!room.allowed) {\n throw new Error(\n `These products need ${fresh.length} product slots — your plan allows ` +\n `${room.limit}. See Billing to upgrade.`,\n )\n }\n // Each draft's slug is asked of the store: the base slug its name makes,\n // then `-2`, `-3`, … until one no product holds, live or deleted.\n const slugs = productSlugLedger(firestore, hostId)\n const created: string[] = []\n try {\n // At its suggested price, held to range here, else the default\n // (Zach, 2026-10-12). Only the proposal's own fields reach the draft.\n const drafts = fresh.map(({ priceUsd, ...proposal }) =>\n CommerceModel.defaultPricedProductDraft(\n proposal,\n NO_SLUGS,\n Date.now(),\n CommerceModel.proposedProductPriceUsd(priceUsd),\n ),\n )\n await slugs.ask(drafts.map((draft) => draft.slug))\n const smartCollections = await readSmartCollections(firestore, hostId)\n for (const draft of drafts) {\n const slug = await slugs.claim(draft.slug)\n const collectionIds = CommerceModel.productCollectionIds(draft, smartCollections)\n const { id } = await createHostResource({\n hostId,\n resource: 'product',\n data: { ...draft, slug, collectionIds },\n })\n created.push(id)\n }\n } finally {\n if (created.length) onCreated()\n }\n return created\n },\n [roomFor, createHostResource, firestore, hostId, onCreated],\n )\n\n const createCategories = useCallback(\n async (names: readonly string[]) => {\n const existing = await getDocs(\n query(collection(firestore, 'hosts', hostId, 'productCategories'), limit(CATEGORY_NAMES_READ)),\n )\n const held = new Set(existing.docs.map((category) => nameKey(String(category.get('name') ?? ''))))\n let created = 0\n for (const raw of names) {\n const name = raw.trim().slice(0, 80)\n if (!name || held.has(nameKey(name))) continue\n held.add(nameKey(name))\n await setDoc(doc(firestore, 'hosts', hostId, 'productCategories', createResourceUid()), {\n name,\n slug: CommerceModel.commerceSlug(name),\n parentId: null,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n })\n created += 1\n }\n return created\n },\n [firestore, hostId],\n )\n\n const createDiscountDrafts = useCallback(\n async (discounts: readonly ConsoleProposedDiscount[]) => {\n const drafts = discounts\n .map((proposal) => CommerceModel.switchedOffDiscount(proposal))\n .filter((discount): discount is CommerceModel.StoredDiscount => discount !== null)\n if (!drafts.length) return 0\n const codes = [...new Set(drafts.map((discount) => discount.code).filter((code): code is string => Boolean(code)))]\n // A stored name is compared as typed, and the query matches it exactly.\n const names = [...new Set(drafts.filter((discount) => !discount.code).map((discount) => discount.name ?? ''))]\n const discountsRef = collection(firestore, 'hosts', hostId, 'discounts')\n const [byCode, byName] = await Promise.all([\n codes.length ? getDocs(query(discountsRef, where('code', 'in', codes.slice(0, 10)))) : null,\n names.length ? getDocs(query(discountsRef, where('name', 'in', names.slice(0, 10)))) : null,\n ])\n const heldCodes = new Set((byCode?.docs ?? []).map((existing) => String(existing.get('code'))))\n const heldNames = new Set((byName?.docs ?? []).map((existing) => nameKey(String(existing.get('name') ?? ''))))\n let created = 0\n for (const discount of drafts) {\n const name = nameKey(discount.name ?? '')\n if (discount.code ? heldCodes.has(discount.code) : heldNames.has(name)) continue\n if (discount.code) heldCodes.add(discount.code)\n else heldNames.add(name)\n await setDoc(doc(firestore, 'hosts', hostId, 'discounts', createResourceUid()), discount)\n created += 1\n }\n return created\n },\n [firestore, hostId],\n )\n\n if (!Slot) return null\n return (\n <Slot\n slot={PRODUCTS_HUB_ZONE.id}\n hostId={hostId}\n orgId={undefined}\n products={summaries}\n lastImport={lastImport}\n applyProductCopy={applyProductCopy}\n createProductDrafts={createProductDrafts}\n createCategories={createCategories}\n createDiscountDrafts={createDiscountDrafts}\n />\n )\n}\nProductsHubZone.displayName = 'ProductsHubZone'\n\nexport default ProductsHubZone\n"],"names":["createResourceUid","useConsoleWidgetSlot","PRODUCTS_HUB_ZONE","Timestamp","useFirestore","useHostResourceApi","useUser","commitWithSiteWideEntry","settleSiteWideChange","collection","doc","getDocs","limit","query","runTransaction","setDoc","where","useCallback","useMemo","CommerceModel","productSlugLedger","readSmartCollections","PRODUCT_GONE_COPY","PLAN_LOADING_COPY","CATEGORY_NAMES_READ","nameKey","name","replace","trim","toLowerCase","IN_CLAUSE_MAX","NO_SLUGS","Set","ProductsHubZone","props","hostId","products","roomFor","lastImport","onCreated","Slot","firestore","data","user","createHostResource","summaries","map","product","id","$id","status","priceMissing","productPriceMissing","hasPhoto","Boolean","mediaUrls","imageUrl","applyProductCopy","productId","values","ref","smartCollections","entry","stage","transaction","stored","snapshot","get","exists","deletedAt","Error","lifted","liftLegacyProduct","patch","productCopyPatch","Object","keys","length","collectionIds","productCollectionIds","held","moved","some","at","update","updatedAtMs","Date","now","updatedAt","createProductDrafts","proposals","searchKeys","proposal","productSearchFields","nameLower","filter","productsRef","Promise","all","Array","from","Math","ceil","_","index","slice","read","existing","docs","add","String","fresh","key","has","room","allowed","slugs","created","drafts","priceUsd","defaultPricedProductDraft","proposedProductPriceUsd","ask","draft","slug","claim","resource","push","createCategories","names","category","raw","commerceSlug","parentId","createdAt","createDiscountDrafts","discounts","switchedOffDiscount","discount","codes","code","discountsRef","byCode","byName","heldCodes","heldNames","slot","orgId","undefined","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ReviewSettingsCardProps {
|
|
2
|
+
hostId: string;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Who may review the store's products, and the review-request email
|
|
6
|
+
* (AGL-3736), on `hosts/{hostId}/settings/store.reviews`. Each control writes
|
|
7
|
+
* only its own key, so this card never overwrites the store settings card's
|
|
8
|
+
* fields beside it. Defaults: verified buyers only, the email on, 7 days.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ReviewSettingsCard(props: ReviewSettingsCardProps): import("react").JSX.Element;
|
|
11
|
+
export declare namespace ReviewSettingsCard {
|
|
12
|
+
var displayName: string;
|
|
13
|
+
}
|
|
14
|
+
export default ReviewSettingsCard;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { pluginDocsHelp } from "@aglyn/aglyn";
|
|
19
|
+
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
20
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
21
|
+
import { useFirestore, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
|
|
22
|
+
import { writeSiteWideChange } from "@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change";
|
|
23
|
+
import { FormControlLabel, MenuItem, Stack, Switch, TextField, Typography } from "@mui/material";
|
|
24
|
+
import { doc } from "firebase/firestore";
|
|
25
|
+
import { useCallback, useEffect, useState } from "react";
|
|
26
|
+
import * as CommerceModel from "../../model/index.js";
|
|
27
|
+
/**
|
|
28
|
+
* Who may review the store's products, and the review-request email
|
|
29
|
+
* (AGL-3736), on `hosts/{hostId}/settings/store.reviews`. Each control writes
|
|
30
|
+
* only its own key, so this card never overwrites the store settings card's
|
|
31
|
+
* fields beside it. Defaults: verified buyers only, the email on, 7 days.
|
|
32
|
+
*/ export function ReviewSettingsCard(props) {
|
|
33
|
+
const { hostId } = props;
|
|
34
|
+
const firestore = useFirestore();
|
|
35
|
+
const { data: user } = useUser();
|
|
36
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
37
|
+
const { data: store } = useFirestoreDoc(()=>doc(firestore, 'hosts', hostId, 'settings', 'store'), [
|
|
38
|
+
firestore,
|
|
39
|
+
hostId
|
|
40
|
+
]);
|
|
41
|
+
const settings = CommerceModel.reviewSettingsFrom(store);
|
|
42
|
+
const [days, setDays] = useState(String(settings.requestDelayDays));
|
|
43
|
+
useEffect(()=>setDays(String(settings.requestDelayDays)), [
|
|
44
|
+
settings.requestDelayDays
|
|
45
|
+
]);
|
|
46
|
+
const [saving, setSaving] = useState(null);
|
|
47
|
+
const save = useCallback(async (key, value)=>{
|
|
48
|
+
setSaving(key);
|
|
49
|
+
try {
|
|
50
|
+
// The policy decides what the product page's review form draws, so
|
|
51
|
+
// the write carries the site's cache drop (AGL-3386).
|
|
52
|
+
await writeSiteWideChange({
|
|
53
|
+
firestore,
|
|
54
|
+
user,
|
|
55
|
+
hostId,
|
|
56
|
+
write: (batch)=>batch.set(doc(firestore, 'hosts', hostId, 'settings', 'store'), {
|
|
57
|
+
reviews: {
|
|
58
|
+
[key]: value
|
|
59
|
+
}
|
|
60
|
+
}, {
|
|
61
|
+
merge: true
|
|
62
|
+
})
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(error);
|
|
66
|
+
enqueueSnackbar('That setting could not be saved. Try again.', {
|
|
67
|
+
variant: 'error'
|
|
68
|
+
});
|
|
69
|
+
} finally{
|
|
70
|
+
setSaving(null);
|
|
71
|
+
}
|
|
72
|
+
}, [
|
|
73
|
+
enqueueSnackbar,
|
|
74
|
+
firestore,
|
|
75
|
+
hostId,
|
|
76
|
+
user
|
|
77
|
+
]);
|
|
78
|
+
const commitDays = ()=>{
|
|
79
|
+
const parsed = Math.round(Number(days));
|
|
80
|
+
const bounded = Number.isFinite(parsed) ? Math.min(CommerceModel.REVIEW_REQUEST_DELAY_MAX_DAYS, Math.max(CommerceModel.REVIEW_REQUEST_DELAY_MIN_DAYS, parsed)) : CommerceModel.REVIEW_REQUEST_DELAY_DEFAULT_DAYS;
|
|
81
|
+
setDays(String(bounded));
|
|
82
|
+
if (bounded !== settings.requestDelayDays) void save('requestDelayDays', bounded);
|
|
83
|
+
};
|
|
84
|
+
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
85
|
+
header: 'Reviews',
|
|
86
|
+
help: pluginDocsHelp('catalog', {
|
|
87
|
+
anchor: '#review-settings'
|
|
88
|
+
}),
|
|
89
|
+
contentGutterX: true,
|
|
90
|
+
contentGutterY: true,
|
|
91
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
92
|
+
spacing: 1.5,
|
|
93
|
+
children: [
|
|
94
|
+
/*#__PURE__*/ _jsxs(TextField, {
|
|
95
|
+
label: "Who can review",
|
|
96
|
+
value: settings.policy,
|
|
97
|
+
onChange: (event)=>void save('policy', event.target.value),
|
|
98
|
+
disabled: saving === 'policy',
|
|
99
|
+
size: "small",
|
|
100
|
+
select: true,
|
|
101
|
+
sx: {
|
|
102
|
+
maxWidth: 360
|
|
103
|
+
},
|
|
104
|
+
helperText: settings.policy === 'verified' ? 'Customers who bought the product: signed in to their account, or ' + 'through the link in their review-request email. Their reviews ' + 'publish right away with a "Verified buyer" badge.' : 'Anyone may review. Customers who bought the product still get the ' + '"Verified buyer" badge and publish right away; everyone else gives a ' + 'name and email (never shown) and waits for your approval.',
|
|
105
|
+
children: [
|
|
106
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
107
|
+
value: "verified",
|
|
108
|
+
children: 'Verified buyers only (recommended)'
|
|
109
|
+
}),
|
|
110
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
111
|
+
value: "anyone",
|
|
112
|
+
children: 'Anyone'
|
|
113
|
+
})
|
|
114
|
+
]
|
|
115
|
+
}),
|
|
116
|
+
/*#__PURE__*/ _jsx(FormControlLabel, {
|
|
117
|
+
control: /*#__PURE__*/ _jsx(Switch, {
|
|
118
|
+
size: "small",
|
|
119
|
+
checked: settings.requestEmail,
|
|
120
|
+
disabled: saving === 'requestEmail',
|
|
121
|
+
onChange: (change)=>void save('requestEmail', change.target.checked)
|
|
122
|
+
}),
|
|
123
|
+
label: /*#__PURE__*/ _jsxs(Stack, {
|
|
124
|
+
children: [
|
|
125
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
126
|
+
variant: "body2",
|
|
127
|
+
children: 'Ask customers for a review'
|
|
128
|
+
}),
|
|
129
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
130
|
+
variant: "caption",
|
|
131
|
+
color: "text.secondary",
|
|
132
|
+
children: 'One email per order, after it is fulfilled, with a link for each ' + 'product. Each link works once, for 60 days. Change its wording ' + 'under Emails → Review request.'
|
|
133
|
+
})
|
|
134
|
+
]
|
|
135
|
+
})
|
|
136
|
+
}),
|
|
137
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
138
|
+
label: "Send the request (days after fulfillment)",
|
|
139
|
+
value: days,
|
|
140
|
+
onChange: (event)=>setDays(event.target.value),
|
|
141
|
+
onBlur: commitDays,
|
|
142
|
+
disabled: !settings.requestEmail || saving === 'requestDelayDays',
|
|
143
|
+
size: "small",
|
|
144
|
+
type: "number",
|
|
145
|
+
slotProps: {
|
|
146
|
+
htmlInput: {
|
|
147
|
+
min: CommerceModel.REVIEW_REQUEST_DELAY_MIN_DAYS,
|
|
148
|
+
max: CommerceModel.REVIEW_REQUEST_DELAY_MAX_DAYS
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
sx: {
|
|
152
|
+
maxWidth: 280
|
|
153
|
+
},
|
|
154
|
+
helperText: `Between ${CommerceModel.REVIEW_REQUEST_DELAY_MIN_DAYS} and ${CommerceModel.REVIEW_REQUEST_DELAY_MAX_DAYS} days. Give the order time to arrive.`
|
|
155
|
+
}),
|
|
156
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
157
|
+
variant: "caption",
|
|
158
|
+
color: "text.secondary",
|
|
159
|
+
children: 'Verified reviews are published as written, good or bad. You can hide ' + 'or reject one only for spam, abuse, being off-topic or sharing ' + 'private information.'
|
|
160
|
+
})
|
|
161
|
+
]
|
|
162
|
+
})
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
ReviewSettingsCard.displayName = 'ReviewSettingsCard';
|
|
166
|
+
export default ReviewSettingsCard;
|
|
167
|
+
|
|
168
|
+
//# sourceMappingURL=review-settings-card.component.js.map
|