@aglyn/plugins-commerce 1.0.0-beta.217 → 1.0.0-beta.219

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +1 -0
  2. package/package.json +15 -15
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js +1 -19
  5. package/src/index.js.map +1 -1
  6. package/src/lib/components/cart.js +2 -1
  7. package/src/lib/components/cart.js.map +1 -1
  8. package/src/lib/components/console/catalog-organization-card.component.js +44 -18
  9. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  10. package/src/lib/components/console/host-orders-card.component.js +2 -1
  11. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  12. package/src/lib/components/console/member-posts-card.component.js +1 -1
  13. package/src/lib/components/console/member-posts-card.component.js.map +1 -1
  14. package/src/lib/components/console/order-detail-dialog.component.js +8 -7
  15. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  16. package/src/lib/components/console/payments-settings-card.component.js +1 -1
  17. package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
  18. package/src/lib/components/console/product-editor-dialog.component.js +114 -63
  19. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  20. package/src/lib/components/console/product-zones.d.ts +184 -0
  21. package/src/lib/components/console/product-zones.js +54 -0
  22. package/src/lib/components/console/product-zones.js.map +1 -0
  23. package/src/lib/components/console/products-hub-card.component.js +3 -2
  24. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  25. package/src/lib/components/console/products-hub-zone.component.d.ts +1 -1
  26. package/src/lib/components/console/products-hub-zone.component.js +2 -1
  27. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  28. package/src/lib/components/console/store-settings-card.component.js +2 -2
  29. package/src/lib/components/console/store-settings-card.component.js.map +1 -1
  30. package/src/lib/components/console/storefront-tax-summary-card.component.js +1 -1
  31. package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -1
  32. package/src/lib/components/product-detail.js +2 -1
  33. package/src/lib/components/product-detail.js.map +1 -1
  34. package/src/lib/components/product-grid.js +1 -1
  35. package/src/lib/components/product-grid.js.map +1 -1
  36. package/src/lib/components/storefront-payment-element.js +36 -5
  37. package/src/lib/components/storefront-payment-element.js.map +1 -1
  38. package/src/lib/constants/storefront-payments.d.ts +33 -0
  39. package/src/lib/constants/storefront-payments.js +35 -0
  40. package/src/lib/constants/storefront-payments.js.map +1 -0
  41. package/src/lib/declarations.server.d.ts +3 -1
  42. package/src/lib/declarations.server.js +19 -1
  43. package/src/lib/declarations.server.js.map +1 -1
  44. package/src/lib/model/commerce-record-routes.d.ts +27 -0
  45. package/src/lib/model/commerce-record-routes.js +72 -0
  46. package/src/lib/model/commerce-record-routes.js.map +1 -0
  47. package/src/lib/model/commerce-search-sources.d.ts +21 -0
  48. package/src/lib/model/commerce-search-sources.js +40 -0
  49. package/src/lib/model/commerce-search-sources.js.map +1 -0
  50. package/src/lib/model/storefront-tax-summary.d.ts +121 -0
  51. package/src/lib/model/storefront-tax-summary.js +148 -0
  52. package/src/lib/model/storefront-tax-summary.js.map +1 -0
  53. package/src/lib/plan-entitlements.d.ts +35 -0
  54. package/src/lib/plan-entitlements.js +54 -0
  55. package/src/lib/plan-entitlements.js.map +1 -0
  56. package/src/lib/plugin.js +9 -0
  57. package/src/lib/plugin.js.map +1 -1
  58. package/src/lib/server/billing-webhook.js +135 -168
  59. package/src/lib/server/billing-webhook.js.map +1 -1
  60. package/src/lib/server/cart-checkout.js +2 -2
  61. package/src/lib/server/cart-checkout.js.map +1 -1
  62. package/src/lib/server/checkout.js +2 -2
  63. package/src/lib/server/checkout.js.map +1 -1
  64. package/src/lib/server/connect.js +5 -5
  65. package/src/lib/server/connect.js.map +1 -1
  66. package/src/lib/server/draft-order.js +2 -2
  67. package/src/lib/server/draft-order.js.map +1 -1
  68. package/src/lib/server/membership-content.js +1 -1
  69. package/src/lib/server/membership-content.js.map +1 -1
  70. package/src/lib/server/native-checkout.d.ts +20 -5
  71. package/src/lib/server/native-checkout.js +27 -6
  72. package/src/lib/server/native-checkout.js.map +1 -1
  73. package/src/lib/server/pos-order.js +2 -2
  74. package/src/lib/server/pos-order.js.map +1 -1
  75. package/src/lib/server/product-record-index.d.ts +19 -0
  76. package/src/lib/server/product-record-index.js +115 -0
  77. package/src/lib/server/product-record-index.js.map +1 -0
  78. package/src/lib/server/reserve.js +2 -2
  79. package/src/lib/server/reserve.js.map +1 -1
  80. package/src/lib/server/revenue-source.d.ts +130 -0
  81. package/src/lib/server/revenue-source.js +286 -0
  82. package/src/lib/server/revenue-source.js.map +1 -0
  83. package/src/lib/server/tax-profile.d.ts +8 -5
  84. package/src/lib/server/tax-profile.js +22 -5
  85. package/src/lib/server/tax-profile.js.map +1 -1
  86. package/src/lib/server/tax-return-source.d.ts +125 -0
  87. package/src/lib/server/tax-return-source.js +524 -0
  88. package/src/lib/server/tax-return-source.js.map +1 -0
  89. package/src/lib/server/tax-summary.d.ts +92 -0
  90. package/src/lib/server/tax-summary.js +258 -0
  91. package/src/lib/server/tax-summary.js.map +1 -0
  92. package/src/lib/server.d.ts +1 -0
  93. package/src/lib/server.js +23 -0
  94. package/src/lib/server.js.map +1 -1
  95. package/src/lib/tenant-emails.d.ts +33 -0
  96. package/src/lib/tenant-emails.js +511 -0
  97. package/src/lib/tenant-emails.js.map +1 -0
  98. package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
  99. package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
  100. package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/products-hub-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport * as CommerceModel from '../../model'\nimport {\n PRODUCT_LIST_BASE,\n PRODUCT_LIST_FIELDS,\n PRODUCT_LIST_HEADERS,\n PRODUCT_LIST_OPTIONS,\n PRODUCT_LIST_QUERY,\n PRODUCT_LIST_SELECT_FIELDS,\n productListRequestClauses,\n} from '../../constants/product-list-query'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListQueryNotices,\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Box,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n doc,\n documentId,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n type QueryDocumentSnapshot,\n startAfter,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { writeSiteWideChange } from '@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change'\nimport {\n useFirestoreCollection,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n listQueryConstraints,\n useListQuery,\n} from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useHostResourceApi } from '@aglyn/tenant-feature-instance'\nimport { useOrgPlan } from '@aglyn/tenant-feature-instance'\nimport ProductEditorDialog from './product-editor-dialog.component'\nimport { productSlugLedger } from './product-slugs'\nimport { productCollectionFields, readSmartCollections } from './smart-collections'\nimport ProductsHubZone from './products-hub-zone.component'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport type { ConsoleProductsHubZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins'\n\n/** Products read per request of the CSV export's walk. */\nconst EXPORT_PAGE = 500\n\n/**\n * The most products one CSV export writes (AGL-3321).\n *\n * The export walks the table's own query page by page, so it writes what the\n * filters and search match across the whole catalog rather than the rows on\n * screen. It stops here because the file is built in the browser, and says\n * so when it does.\n */\nconst EXPORT_CEILING = 10_000\n\n/**\n * How many of ONE product's license keys the dialog reads.\n *\n * Per product, not per store: the query carries the `productId` equality, so\n * this bounds a single pool rather than the site's whole key collection.\n */\nconst KEY_POOL_CEILING = 500\n\nexport interface ProductsHubCardProps {\n hostId: string\n}\n\ntype ProductRow = CommerceModel.HostProduct & { $id: string }\n\nconst STATUS_COLOR: Record<string, 'default' | 'success' | 'warning'> = {\n active: 'success',\n draft: 'warning',\n archived: 'default',\n}\n\n/** The grid's own columns; every other declared field is a hidden filter column. */\nconst PRODUCT_VISIBLE_COLUMNS = ['name', 'status', 'type', 'priceUsd', 'stock', 'variants']\n\n/**\n * Products hub v1 (AGL-279): the catalog manager replacing the Commerce\n * Starter card — a table over `hosts/{hostId}/products` whose search and\n * filters are its Firestore query (AGL-3321), full editor dialog, duplicate, archive/activate, soft delete (past\n * order rows keep resolving). Product cap (`productsPerHost`, AGL-278)\n * gated here on create/duplicate/import (AGL-471); server-side\n * enforcement of the client-write path rides AGL-473.\n */\nexport function ProductsHubCard(props: ProductsHubCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n // Product cap (AGL-471): per-plan `productsPerHost`, same pattern as\n // locations. Console-side gate; server enforcement rides AGL-473.\n const createHostResource = useHostResourceApi()\n const { org, ready: planReady } = useOrgPlan(hostId)\n const { confirm } = useConfirmationContext()\n /*\n * The grid's Filters panel and quick search, bound to the clauses the\n * query below serves (AGL-3321). Every clause and the search word go on\n * that query; see `constants/product-list-query.ts` for what is offered.\n */\n const gridFilter = useListGridFilter({\n selectFields: PRODUCT_LIST_SELECT_FIELDS,\n })\n const filtering =\n gridFilter.clauses.length > 0 ||\n gridFilter.searchWords.some((word) => word.trim())\n const [editing, setEditing] = useState<ProductRow | null>(null)\n const [creating, setCreating] = useState(false)\n const [adjusting, setAdjusting] = useState<{\n product: ProductRow\n variantId: string\n delta: string\n reason: CommerceModel.InventoryAdjustmentReason\n locationId: string\n } | null>(null)\n const [importing, setImporting] = useState<{\n text: string\n parsed: CommerceModel.ProductCsvImport | null\n } | null>(null)\n const [keysFor, setKeysFor] = useState<ProductRow | null>(null)\n const [keysText, setKeysText] = useState('')\n /**\n * What the import zone set for the import in the dialog, and what the last\n * import created with those options (AGL-2916), which the hub's zone hands\n * to its widgets.\n */\n const [importOptions, setImportOptions] = useState<Record<string, boolean>>({})\n const [lastImport, setLastImport] =\n useState<ConsoleProductsHubZoneProps['lastImport']>(null)\n const WidgetSlot = useConsoleWidgetSlot()\n\n const {\n rows: productDocs,\n data: productWindow,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n status: productsStatus,\n /**\n * The product rows the stock dialog is seeded from are unconfirmed by\n * the server (AGL-1358). A stock adjustment does not write the delta —\n * it recomputes the WHOLE `variants` array from the seeded product and\n * replaces it, so a cached seed silently reverts every sale, return and\n * adjustment the server has recorded since that snapshot, on every\n * variant and every location.\n */\n fromCache: productsFromCache,\n } = useListQuery<any>({\n /*\n * PAGED BY THE QUERY, and every filter ON it (AGL-3321).\n *\n * The table read a five-hundred-product window and put ONE predicate on\n * it — the search or the Status clause — then matched Status beside a\n * search, dropped soft-deleted products and sorted by name over the rows\n * it held, and paged that. Past five hundred products, a match past the\n * window answered \"no products match\", and a page came back short by\n * however many deleted products it had held.\n *\n * Now the plan puts the search word and every clause on one query under\n * the `deletedAt == null` scope, ordered by `nameLower`, and each page is\n * a page of that query — one page plus a probe row, widened as the reader\n * pages forward. Nothing is matched over the rows afterwards; a clause\n * the query cannot hold is refused by name (`ListQueryNotices` below).\n */\n collection: collection(firestore, 'hosts', hostId, 'products'),\n declaration: PRODUCT_LIST_QUERY,\n request: {\n clauses: productListRequestClauses(gridFilter.clauses),\n search: gridFilter.searchWords,\n base: PRODUCT_LIST_BASE,\n },\n deps: [firestore, hostId],\n idField: '$id',\n /*\n * The scope and the filters read MUTABLE fields — a delete sets\n * `deletedAt`, Archive moves `status`, a rename moves the tokens — so a\n * product can leave this query mid-session, and the SDK can cache that\n * departure as a tombstone at the product's own path (AGL-1196). A\n * disappearance is confirmed against the server before it is believed.\n */\n confirmDisappearances: true,\n })\n /*\n * License key pool (AGL-308) for the open dialog's product — asked FOR that\n * product, and ordered (AGL-2501).\n *\n * This read `limit(500)` over the site's whole `licenseKeys` collection with\n * no `orderBy`, then filtered by `productId` in the browser. Firestore\n * answers an unordered limit in DOCUMENT-ID order, so on a store past five\n * hundred keys the window was an arbitrary five hundred taken across every\n * product — and the \"N available\" line below counted what happened to be in\n * it. A product whose keys all hashed high showed a pool of zero while the\n * storefront went on delivering them.\n *\n * The equality moves into the query, so the window is this product's keys\n * rather than the store's. `orderBy(documentId())` orders on the document\n * NAME, which cannot be absent — every candidate FIELD here (`createdAtMs`,\n * `assignedAtMs`, `revokedAtMs`) is either optional by design or absent on\n * exactly the keys the counts are about. An equality plus an ordering on\n * `__name__` is served by the automatic single-field index, so this adds no\n * composite for anyone to deploy.\n */\n const { data: keyDocs } = useFirestoreCollection<any>(\n () =>\n keysFor\n ? query(\n collection(firestore, 'hosts', hostId, 'licenseKeys'),\n where('productId', '==', keysFor.$id),\n orderBy(documentId()),\n // One past the ceiling, so a pool larger than the window is a\n // fact rather than a guess from `length === 500`.\n limit(KEY_POOL_CEILING + 1),\n )\n : null,\n [firestore, hostId, keysFor?.$id],\n { idField: '$id' },\n )\n /** The key pool is larger than the window, so the counts below are partial. */\n const keyPoolTruncated = (keyDocs?.length ?? 0) > KEY_POOL_CEILING\n // Locations (AGL-286): the stock dialog buckets deltas when they exist.\n const { data: locationDocs } = useFirestoreCollection<any>(\n () => query(collection(firestore, 'hosts', hostId, 'locations'), limit(25)),\n [firestore, hostId],\n { idField: '$id' },\n )\n /*\n * The PAGE on screen, lifted from legacy shapes. It is what the table\n * draws, the reserved-stock clock watches and the zone hands its widgets,\n * and nothing narrows it: the query already answered every filter, and\n * soft-deleted products are outside its scope.\n */\n const products = useMemo<ProductRow[]>(\n () =>\n (productDocs ?? []).map((product: any) => ({\n ...CommerceModel.liftLegacyProduct(product),\n $id: product.$id,\n })),\n [productDocs],\n )\n\n /**\n * A CLOCK, because a hold lapses without anybody writing anything\n * (AGL-2356).\n *\n * The Firestore listener re-renders on document changes, and a reservation\n * expiring is not one — `expiresAtMs` simply passes. Without a tick the\n * \"reserved\" caption below would keep naming a hold that lapsed twenty\n * minutes ago, which is worse than not showing it at all: the merchant would\n * be told stock is spoken for while the storefront happily sells it.\n *\n * Only runs while something is actually held, so an ordinary catalog costs\n * no timer. A NEW hold arrives as a document change, which re-renders, which\n * re-arms this. It watches the page on screen, which is every row whose\n * caption it keeps true.\n */\n const [nowMs, setNowMs] = useState(() => Date.now())\n const anyHeld = useMemo(\n () =>\n products.some(\n (product) => CommerceModel.heldProductUnits(product, nowMs) > 0,\n ),\n [products, nowMs],\n )\n useEffect(() => {\n if (!anyHeld) return undefined\n const timer = setInterval(() => setNowMs(Date.now()), 60_000)\n return () => clearInterval(timer)\n }, [anyHeld])\n\n /**\n * The catalog HEAD-COUNT is a server aggregate, not the length of the\n * capped listener (AGL-1716, the AGL-1706 shape).\n *\n * The listener is `limit(500)` — correctly; a 25,000-product catalog does\n * not belong in a table. What it must not do is answer \"how many products\n * does this site have\", and it did: the length saturated at 500 and was\n * handed to `checkQuota(org, 'productsPerHost', …)` and to the batch check\n * the CSV importer runs. The bands are 2,500 / 10,000 / 25,000 above the\n * window, so on Pro and up the check compared 500 against thousands and\n * could never refuse — the card offered headroom and `api/hosts/resources`\n * then refused the create, which is the AGL-1716 shape exactly.\n *\n * The aggregate is deliberately UNFILTERED, which also closes a second,\n * quieter disagreement: `api/hosts/resources` enforces this quota with a\n * plain `collection('products').count()`, and `softDeletes` there governs\n * only the flat per-host cap on webhooks — so the server has always\n * counted soft-deleted products toward `productsPerHost` while this card\n * excluded them. The card now asks the enforcing route's question, in the\n * enforcing route's terms.\n *\n * The table pages its own query (AGL-3321) and answers a different\n * question — which products match — so it never stands in for this. A\n * one-shot goes stale where a listener refreshed for free, so the count is\n * re-read after any mutation that moves it.\n *\n * No counting RULE moves: `checkQuota` is untouched and `report-usage`\n * meters contacts, storage and API requests — never the catalog.\n */\n const [productCountEpoch, setProductCountEpoch] = useState(0)\n const [serverProductCount, setServerProductCount] = useState<number | null>(\n null,\n )\n useEffect(() => {\n let active = true\n void getCountFromServer(collection(firestore, 'hosts', hostId, 'products'))\n .then((snapshot) => {\n if (active) setServerProductCount(snapshot.data().count)\n })\n .catch(() => {\n // Falls back to the live-row count below — a LOWER bound, and this\n // card's prior behaviour. Deliberately not 0: `checkQuota` answers\n // from whatever it is handed, and 0 used is a confident wrong\n // number in the flattering direction.\n })\n return () => {\n active = false\n }\n }, [firestore, hostId, productCountEpoch])\n // The fallback, pending or denied: the live products the table's query has\n // read, probe row included. Each is a product the site has, so it can only\n // UNDERSTATE, never overstate, and nothing it gates fires on a count larger\n // than the truth (AGL-471).\n const loadedProductCount = productWindow?.length ?? 0\n const productCount = serverProductCount ?? loadedProductCount\n // Gate only once the org doc has loaded: an unresolved org reads as the\n // free tier's 0-product cap, which swallowed every Add/Duplicate click.\n // The resources API (AGL-473) stays the authoritative cap on create.\n //\n // `planReady` rather than `org` truthiness (AGL-1064): a host with no\n // owning org never produces one, and waiting on the value alone would\n // hold this open forever. The old fallback allowed UNLIMITED during the\n // window — safe only because the API re-checks; the controls now disable\n // instead, which does not lean on that backstop.\n // NULL means \"not known yet\", which a boolean cannot say — the old\n // fallback object claimed UNLIMITED and every consumer believed it.\n const productQuota = useMemo(\n () =>\n planReady\n ? Aglyn.checkQuota(org, 'productsPerHost', productCount)\n : null,\n [org, planReady, productCount],\n )\n\n const handleDuplicate = useCallback(\n (product: ProductRow) => async () => {\n // Plan unknown — the control is disabled; this guards the race.\n if (!productQuota) return\n if (!productQuota.allowed) {\n return void enqueueSnackbar(\n `Your plan includes ${productQuota.limit} products — upgrade for more`,\n { variant: 'info', persist: false },\n )\n }\n // The source's SKU and barcode keys are not copied: the copy's own are\n // derived below, and a copy of a product with none must have none.\n const { $id: _sourceId, skus: _skus, barcodes: _barcodes, ...copy } =\n product as ProductRow & { skus?: string[]; barcodes?: string[] }\n try {\n /*\n * The copy's slug is asked of the store, so a second Duplicate of\n * the same product does not mint a second `-copy` (AGL-3321), and\n * its search keys are the COPY's: spreading the source's would sort\n * and find it by the source's name.\n */\n const slug = await productSlugLedger(firestore, hostId).claim(\n CommerceModel.commerceSlug(`${product.slug}-copy`),\n )\n // Duplicate is a create — rides the quota-enforcing API (AGL-473).\n await createHostResource({\n hostId,\n resource: 'product',\n data: {\n ...copy,\n ...CommerceModel.productSearchFields({\n name: `${product.name} (copy)`,\n variants: product.variants,\n }),\n ...CommerceModel.productStockFields(product),\n // The COPY's smart collections: its name differs from the source's.\n ...(await productCollectionFields(firestore, hostId, {\n ...product,\n name: `${product.name} (copy)`,\n })),\n slug,\n status: 'draft',\n createdAtMs: Date.now(),\n updatedAtMs: Date.now(),\n },\n })\n // A create moves the count and the aggregate is a one-shot — the\n // listener refreshes the ROWS for free, the count has to be asked\n // again or the cap drifts stale for the rest of the session.\n setProductCountEpoch((epoch) => epoch + 1)\n enqueueSnackbar('Product duplicated as draft', {\n variant: 'success',\n persist: false,\n })\n } catch (error: any) {\n enqueueSnackbar(error?.message ?? 'Could not duplicate product', {\n variant: 'warning',\n persist: false,\n })\n }\n },\n [firestore, hostId, createHostResource, enqueueSnackbar, productQuota],\n )\n\n const handleStatus = useCallback(\n (product: ProductRow, status: CommerceModel.ProductStatus) => async () => {\n // A product with a variant nobody has priced (AGL-2916) stays off the\n // storefront until the editor has a price for each.\n if (status === 'active' && CommerceModel.productPriceMissing(product)) {\n return void enqueueSnackbar(\n `Set a price for every variant of ${product.name} before activating it.`,\n { variant: 'info', persist: false },\n )\n }\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(doc(firestore, 'hosts', hostId, 'products', product.$id), {\n status,\n updatedAtMs: Date.now(),\n updatedAt: Timestamp.now(),\n }),\n })\n },\n [firestore, hostId, user, enqueueSnackbar],\n )\n\n const handleDelete = useCallback(\n (product: ProductRow) => async () => {\n const confirmed = await confirm({\n title: 'Delete this product?',\n description:\n `\"${product.name}\" stops being purchasable; blocks referencing ` +\n 'it show a checkout error until repointed.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(doc(firestore, 'hosts', hostId, 'products', product.$id), {\n deletedAt: Timestamp.now(),\n }),\n })\n enqueueSnackbar('Product deleted', { variant: 'success', persist: false })\n },\n [confirm, firestore, hostId, user, enqueueSnackbar],\n )\n\n /*\n * CSV import/export (AGL-282): Shopify-dialect columns, dry-run first.\n *\n * The export writes what the table's filters and search MATCH, across the\n * whole catalog (AGL-3321): it walks the table's own query — the same plan,\n * so the same scope, predicates and order — a page of `EXPORT_PAGE` at a\n * time from a cursor, rather than writing the rows on screen. A walk that\n * reaches `EXPORT_CEILING` stops, asks once whether anything is left, and\n * says so when something is.\n */\n const [exporting, setExporting] = useState(false)\n const handleExport = useCallback(async () => {\n setExporting(true)\n try {\n const productsRef = collection(firestore, 'hosts', hostId, 'products')\n const constraints = listQueryConstraints(plan)\n const exported: ProductRow[] = []\n let cursor: QueryDocumentSnapshot | null = null\n let more = false\n for (;;) {\n const room = EXPORT_CEILING - exported.length\n const snapshot = await getDocs(\n query(\n productsRef,\n ...constraints,\n ...(cursor ? [startAfter(cursor)] : []),\n limit(room > 0 ? Math.min(EXPORT_PAGE, room) : 1),\n ),\n )\n if (room <= 0) {\n more = !snapshot.empty\n break\n }\n for (const product of snapshot.docs) {\n exported.push({\n ...CommerceModel.liftLegacyProduct(product.data() as any),\n $id: product.id,\n })\n }\n if (snapshot.docs.length < Math.min(EXPORT_PAGE, room)) break\n cursor = snapshot.docs[snapshot.docs.length - 1]\n }\n const csv = CommerceModel.productsToCsv(exported)\n const blob = new Blob([csv], { type: 'text/csv' })\n const anchor = document.createElement('a')\n anchor.href = URL.createObjectURL(blob)\n anchor.download = `products-${hostId}.csv`\n anchor.click()\n URL.revokeObjectURL(anchor.href)\n if (more) {\n enqueueSnackbar(\n `Exported the first ${EXPORT_CEILING.toLocaleString()} matching ` +\n 'products — the most one file holds. Filter the list to export the rest.',\n { variant: 'info', persist: false },\n )\n }\n } catch (error: any) {\n enqueueSnackbar(error?.message ?? 'Could not export the products', {\n variant: 'warning',\n persist: false,\n })\n } finally {\n setExporting(false)\n }\n }, [firestore, hostId, plan, enqueueSnackbar])\n\n const handleImportApply = useCallback(async () => {\n const parsed = importing?.parsed\n if (!parsed || parsed.products.length === 0) return\n // Batch-aware cap (AGL-471): the whole import must fit the plan.\n // Not startable until the plan is known (AGL-1064) — the old `org ?`\n // guard let an import begin against an unknown cap and leaned on the\n // API rejecting it partway through, leaving a half-imported catalog.\n if (!planReady) return\n const batchQuota = Aglyn.checkQuota(\n org,\n 'productsPerHost',\n productCount + parsed.products.length - 1,\n )\n if (!batchQuota.allowed) {\n return void enqueueSnackbar(\n `This import needs ${parsed.products.length} product slots — your ` +\n `plan allows ${batchQuota.limit}. See Billing to upgrade.`,\n { variant: 'info', persist: false },\n )\n }\n /*\n * The duplicate-slug check asks the STORE (AGL-3321): every slug the file\n * names, by `slug in […]`, and any suffixed one only when it is needed.\n * It asked the table's rows, which held five hundred products and now\n * hold one page, so a slug past them was handed out a second time.\n */\n const slugs = productSlugLedger(firestore, hostId)\n const created: string[] = []\n try {\n await slugs.ask(parsed.products.map((product) => product.slug))\n // One read of the host's smart collections for the whole file.\n const smartCollections = await readSmartCollections(firestore, hostId)\n // Each create rides the quota-enforcing API (AGL-473); the batch cap\n // above short-circuits before we start, so this loop stays bounded.\n for (const product of parsed.products) {\n const slug = await slugs.claim(product.slug)\n const { id } = await createHostResource({\n hostId,\n resource: 'product',\n data: {\n ...product,\n // Search keys travel with the name on the IMPORT path too — a\n // catalog arrives here in bulk, and the table orders, pages and\n // searches it by these keys alone.\n ...CommerceModel.productSearchFields(product),\n ...CommerceModel.productStockFields(product),\n // Its smart collections (AGL-3321).\n collectionIds: CommerceModel.productCollectionIds(product, smartCollections),\n slug,\n priceUsd: product.variants[0]?.priceUsd ?? 0,\n imageUrl: product.mediaUrls?.[0] ?? null,\n createdAtMs: Date.now(),\n updatedAtMs: Date.now(),\n },\n })\n created.push(id)\n }\n } catch (error: any) {\n return void enqueueSnackbar(error?.message ?? 'Import failed', {\n variant: 'warning',\n persist: false,\n })\n }\n setLastImport({\n key: `${Date.now().toString(36)}-${created.length}`,\n productIds: created,\n options: importOptions,\n })\n setImportOptions({})\n setImporting(null)\n setProductCountEpoch((epoch) => epoch + 1)\n enqueueSnackbar(`Imported ${parsed.products.length} products`, {\n variant: 'success',\n persist: false,\n })\n }, [\n importing,\n firestore,\n hostId,\n createHostResource,\n enqueueSnackbar,\n org,\n planReady,\n productCount,\n importOptions,\n ])\n\n const handleAdjustSave = useCallback(async () => {\n if (!adjusting) return\n const delta = Math.round(Number(adjusting.delta))\n if (!delta) return\n const variants = CommerceModel.adjustVariantInventory(\n adjusting.product,\n adjusting.variantId,\n delta,\n adjusting.locationId || undefined,\n )\n /**\n * Refuse the adjustment when the seed is unconfirmed (AGL-1358).\n *\n * `adjustVariantInventory` reads counts off the seeded product and\n * returns a whole new `variants` array, which is then written over the\n * stored one — so this is a full replace of live stock, not a delta, and\n * `merge` could not help. The refusal also has to cover the ADJUSTMENT\n * LOG, which is why the guard wraps both writes: a logged adjustment\n * whose stock write never happened is worse than neither, because the\n * history then disagrees with the count it is supposed to explain.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'stock',\n unreadable: productsStatus === 'error',\n fromCache: productsFromCache,\n },\n async () => {\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(\n doc(firestore, 'hosts', hostId, 'products', adjusting.product.$id),\n {\n variants,\n // The total and the In stock verdict move with the count (AGL-3321).\n ...CommerceModel.productStockFields({ ...adjusting.product, variants }),\n updatedAtMs: Date.now(),\n },\n ),\n })\n // Adjustment history (AGL-281): the same log the sale webhook writes.\n await addDoc(\n collection(firestore, 'hosts', hostId, 'inventoryAdjustments'),\n {\n productId: adjusting.product.$id,\n variantId: adjusting.variantId,\n delta,\n reason: adjusting.reason,\n ...(adjusting.locationId ? { locationId: adjusting.locationId } : {}),\n atMs: Date.now(),\n } satisfies CommerceModel.InventoryAdjustment,\n )\n },\n )\n // Keep the dialog open with the typed delta rather than failing silently.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n setAdjusting(null)\n enqueueSnackbar('Stock adjusted', { variant: 'success', persist: false })\n }, [\n user,\n adjusting,\n firestore,\n hostId,\n enqueueSnackbar,\n productsFromCache,\n productsStatus,\n ])\n\n /** The hub's allowance check for a batch of `count` new products, for its zone. */\n const roomFor = useCallback(\n (count: number) =>\n planReady\n ? Aglyn.checkQuota(org, 'productsPerHost', productCount + count - 1)\n : null,\n [org, planReady, productCount],\n )\n const onCatalogCreated = useCallback(\n () => setProductCountEpoch((epoch) => epoch + 1),\n [],\n )\n const setImportOption = useCallback(\n (key: string, on: boolean) =>\n setImportOptions((current) => ({ ...current, [key]: on })),\n [],\n )\n\n const formatPrice = (product: ProductRow) => {\n const [min, max] = CommerceModel.productPriceRange(product)\n return min === max ? `$${min}` : `$${min}–$${max}`\n }\n const formatStock = (product: ProductRow) => {\n const total = CommerceModel.productInventory(product)\n if (total == null) return '—'\n return total > 0 ? String(total) : 'Sold out'\n }\n /**\n * RESERVED UNITS, NAMED (AGL-2356).\n *\n * Checkout now takes a hold on the units a live session is about to buy, and\n * that hold deliberately does NOT move `inventory` — the shelf count means\n * units on the shelf, and half the product reads it that way. The\n * consequence a merchant meets is that the storefront can refuse a sale of\n * the third unit while this column says `3`, and without this caption the\n * number is right and the behaviour looks broken.\n *\n * The house style of `stock-movements-card.component.tsx`: a trailing\n * `caption` in `text.secondary`, rendered ONLY when there is something to\n * say. Nothing is held on the overwhelming majority of products, so the\n * column reads exactly as it does today.\n */\n const formatHeld = (product: ProductRow) => {\n const held = CommerceModel.heldProductUnits(product, nowMs)\n return held > 0 ? ` (${held} reserved)` : ''\n }\n\n const productColumns = listFilterGridColumns(\n [\n {\n field: 'name',\n headerName: 'Product',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }: { row: ProductRow }) => (\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {row.name}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`/${row.slug} · id: ${row.$id}`}\n </Typography>\n </Stack>\n ),\n },\n {\n field: 'status',\n headerName: 'Status',\n width: 120,\n renderCell: ({ row }: { row: ProductRow }) => (\n <Chip\n label={row.status}\n size=\"small\"\n color={STATUS_COLOR[row.status] ?? 'default'}\n variant=\"outlined\"\n />\n ),\n },\n { field: 'type', headerName: 'Type', width: 110 },\n {\n field: 'priceUsd',\n headerName: 'Price',\n width: 130,\n sortable: false,\n renderCell: ({ row }: { row: ProductRow }) =>\n CommerceModel.productPriceMissing(row) ? (\n <Chip label=\"Set a price\" size=\"small\" color=\"warning\" variant=\"outlined\" />\n ) : (\n formatPrice(row)\n ),\n },\n {\n field: 'stock',\n headerName: 'Stock',\n width: 150,\n sortable: false,\n renderCell: ({ row }: { row: ProductRow }) => (\n <span>\n {formatStock(row)}\n {formatHeld(row) ? (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"span\">\n {formatHeld(row)}\n </Typography>\n ) : null}\n </span>\n ),\n },\n {\n field: 'variants',\n headerName: 'Variants',\n width: 100,\n sortable: false,\n valueGetter: (_value: unknown, row: ProductRow) => row.variants.length,\n },\n listActionsColumn(\n (product: ProductRow) => (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ justifyContent: 'flex-end', whiteSpace: 'nowrap' }}\n >\n <Button size=\"small\" onClick={() => setEditing(product)}>\n {'Edit'}\n </Button>\n <Button size=\"small\" onClick={handleDuplicate(product)}>\n {'Duplicate'}\n </Button>\n {product.type === 'digital' ? (\n <Button size=\"small\" onClick={() => setKeysFor(product)}>\n {'Keys'}\n </Button>\n ) : null}\n {CommerceModel.productInventory(product) != null ? (\n <Button\n size=\"small\"\n onClick={() =>\n setAdjusting({\n product,\n variantId:\n product.variants.find(\n (variant) => variant.inventory != null,\n )?.id ?? product.variants[0].id,\n delta: '',\n reason: 'restock',\n locationId:\n (locationDocs ?? []).find(\n (location: any) => location.isDefault,\n )?.$id ??\n (locationDocs ?? [])[0]?.$id ??\n '',\n })\n }\n >\n {'Stock'}\n </Button>\n ) : null}\n <Button\n size=\"small\"\n onClick={handleStatus(\n product,\n product.status === 'archived' ? 'active' : 'archived',\n )}\n >\n {product.status === 'archived' ? 'Activate' : 'Archive'}\n </Button>\n <Button size=\"small\" color=\"error\" onClick={handleDelete(product)}>\n {'Delete'}\n </Button>\n </Stack>\n ),\n { width: 470 },\n ),\n ] as GridColDef[],\n PRODUCT_LIST_FIELDS,\n PRODUCT_LIST_OPTIONS,\n PRODUCT_LIST_HEADERS,\n )\n\n return (\n <CardDisplay\n /*\n * The header count is the SITE'S catalog, not the rows in hand.\n *\n * It was `products.length`, which is the filtered, ceilinged, now-paged\n * view — so it read 500 on a 25,000-product catalog and would read 10\n * once the table paged. `productCount` is the same server aggregate the\n * quota gate uses. Under a filter there is no honest number to put here:\n * the aggregate counts the whole catalog and the view counts matches, so\n * neither describes what the reader is looking at, and the footer's own\n * count line answers it instead.\n */\n header={\n filtering\n ? 'Products'\n : `Products${productCount ? ` (${productCount})` : ''}`\n }\n help={pluginDocsHelp('commerce', { anchor: '#products-hub' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n <Box sx={{ flex: 1 }} />\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={!productQuota}\n onClick={() => {\n if (!productQuota) return\n if (!productQuota.allowed) {\n return void enqueueSnackbar(\n `Your plan includes ${productQuota.limit} products — ` +\n 'upgrade for more',\n { variant: 'info', persist: false },\n )\n }\n setCreating(true)\n }}\n >\n {'Add product'}\n </Button>\n <Button\n size=\"small\"\n disabled={!planReady}\n onClick={() => {\n setImportOptions({})\n setImporting({ text: '', parsed: null })\n }}\n >\n {'Import'}\n </Button>\n <Button\n size=\"small\"\n // Nothing matches: there is nothing for the walk to write.\n disabled={exporting || (products.length === 0 && page === 0)}\n onClick={handleExport}\n >\n {exporting ? 'Exporting…' : 'Export'}\n </Button>\n </Stack>\n {/* The cap, standing rather than only on refusal (AGL-2113). The\n count is `productCount` — the site's products, the same number\n the gate above counts — not `products.length`, which is one page\n of the table's query (AGL-1716). */}\n <QuotaReadoutComponent\n ready={productQuota !== null}\n used={productCount}\n limit={productQuota?.limit ?? 0}\n noun=\"product\"\n />\n {/* The zone's widgets are handed the PAGE on screen — the products\n the reader has found and is looking at. Every write the zone makes\n asks the store for what it must not duplicate, never these rows. */}\n <ProductsHubZone\n hostId={hostId}\n products={products}\n roomFor={roomFor}\n lastImport={lastImport}\n onCreated={onCatalogCreated}\n />\n <ListFilterChips\n fields={PRODUCT_LIST_FIELDS}\n headers={PRODUCT_LIST_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={PRODUCT_LIST_OPTIONS}\n />\n <ListQueryNotices\n refused={listQueryRefusals(plan.refused, {\n fields: PRODUCT_LIST_FIELDS,\n headers: PRODUCT_LIST_HEADERS,\n options: PRODUCT_LIST_OPTIONS,\n })}\n notices={plan.notices}\n />\n <ListTable\n aria-label=\"Products\"\n rows={products}\n columns={productColumns}\n loading={productsStatus === 'loading'}\n // One page of the query, turned by the footer below: the grid\n // neither slices, filters nor sorts it.\n hideFooter\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // The one order the list offers, by name: the query's.\n sortingMode=\"server\"\n disableColumnSorting\n initialState={{\n columns: {\n columnVisibilityModel: hiddenFilterVisibility(\n PRODUCT_LIST_FIELDS,\n PRODUCT_VISIBLE_COLUMNS,\n ),\n },\n }}\n noRowsLabel={\n filtering ? 'No products match these filters' : 'No products yet'\n }\n noRowsDescription={\n filtering\n ? undefined\n : 'Build your catalog: add a product, then drop commerce ' +\n 'blocks on any screen in Besigner.'\n }\n />\n {products.length === 0 && page === 0 && productsStatus !== 'loading' ? null : (\n /* The count line is the PAGE's. `hasMore` is a fact from the probe\n row; the site's total is `productCount` above, which counts the\n whole catalog rather than the matches, so it is not this. */\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={products.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n </Stack>\n <Dialog\n open={Boolean(keysFor)}\n onClose={() => setKeysFor(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{`License keys — ${keysFor?.name ?? ''}`}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n {(() => {\n // The query already asked for this product's keys, so no second\n // filter here: narrowing a window that was already narrowed is how\n // a reader comes to believe the count covers the pool.\n const productKeys = (keyDocs ?? []).slice(0, KEY_POOL_CEILING)\n const available = productKeys.filter(\n (key: any) => !key.assignedAtMs,\n )\n // RETIRED KEYS ARE COUNTED SEPARATELY (AGL-2454). A refund retires\n // the key rather than returning it to the pool — the buyer already\n // holds the string, so reissuing it would give two people one\n // working key — and without this line the merchant simply watches\n // \"available\" fall with nothing to explain where the key went.\n const retired = productKeys.filter((key: any) => key.revokedAtMs)\n const assigned =\n productKeys.length - available.length - retired.length\n return (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ mt: 1 }}>\n {`${available.length} available · ${assigned} assigned` +\n (retired.length\n ? ` · ${retired.length} retired (refunded or revoked — not reissued)`\n : '') +\n (keyPoolTruncated\n ? `, of the ${KEY_POOL_CEILING} keys read`\n : '') +\n '. Keys deliver automatically on purchase (receipt + account).'}\n </Typography>\n {available.slice(0, 8).map((key: any) => (\n <Stack\n key={key.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\" sx={{ flex: 1, fontFamily: 'monospace' }} noWrap>\n {key.key}\n </Typography>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() =>\n updateDoc(\n doc(firestore, 'hosts', hostId, 'licenseKeys', key.$id),\n { revokedAtMs: Date.now(), assignedAtMs: Date.now() },\n )\n }\n >\n {'Revoke'}\n </Button>\n </Stack>\n ))}\n <TextField\n label=\"Add keys (one per line)\"\n value={keysText}\n onChange={(event) => setKeysText(event.target.value)}\n size=\"small\"\n multiline\n minRows={3}\n />\n </>\n )\n })()}\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setKeysFor(null)}>{'Close'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!keysText.trim()}\n onClick={async () => {\n const keys = keysText\n .split('\\n')\n .map((key) => key.trim())\n .filter(Boolean)\n .slice(0, 200)\n for (const key of keys) {\n await addDoc(\n collection(firestore, 'hosts', hostId, 'licenseKeys'),\n {\n productId: keysFor!.$id,\n key,\n assignedAtMs: null,\n createdAtMs: Date.now(),\n },\n )\n }\n setKeysText('')\n enqueueSnackbar(`Added ${keys.length} keys`, {\n variant: 'success',\n persist: false,\n })\n }}\n >\n {'Add keys'}\n </Button>\n </DialogActions>\n </Dialog>\n <Dialog\n open={Boolean(importing)}\n onClose={() => setImporting(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{'Import products (CSV)'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ mt: 1 }}>\n {'Shopify-compatible columns (Handle, Title, Option/Variant ' +\n 'columns, Image Src). Paste the file contents or choose a file.'}\n </Typography>\n <Button component=\"label\" size=\"small\" sx={{ alignSelf: 'flex-start' }}>\n {'Choose file'}\n <input\n type=\"file\"\n accept=\".csv,text/csv\"\n hidden\n onChange={async (event) => {\n const file = event.target.files?.[0]\n if (!file) return\n const text = await file.text()\n setImporting({ text, parsed: CommerceModel.parseProductsCsv(text) })\n }}\n />\n </Button>\n <TextField\n label=\"CSV\"\n value={importing?.text ?? ''}\n onChange={(event) =>\n setImporting({\n text: event.target.value,\n parsed: event.target.value.trim()\n ? CommerceModel.parseProductsCsv(event.target.value)\n : null,\n })\n }\n size=\"small\"\n multiline\n minRows={5}\n maxRows={10}\n />\n {importing?.parsed ? (\n <>\n <Typography variant=\"body2\">\n {`Ready to import ${importing.parsed.products.length} products` +\n (importing.parsed.errors.length\n ? ` — ${importing.parsed.errors.length} rows skipped:`\n : '')}\n </Typography>\n {importing.parsed.errors.slice(0, 5).map((error) => (\n <Typography\n key={error}\n variant=\"caption\"\n color=\"warning.main\"\n >\n {error}\n </Typography>\n ))}\n </>\n ) : null}\n {WidgetSlot && importing?.parsed?.products.length ? (\n <WidgetSlot\n slot=\"productImport\"\n hostId={hostId}\n orgId={undefined}\n count={importing.parsed.products.length}\n options={importOptions}\n setOption={setImportOption}\n />\n ) : null}\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setImporting(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!importing?.parsed?.products.length}\n onClick={handleImportApply}\n >\n {`Import${importing?.parsed?.products.length ? ` ${importing.parsed.products.length}` : ''}`}\n </Button>\n </DialogActions>\n </Dialog>\n <Dialog\n open={Boolean(adjusting)}\n onClose={() => setAdjusting(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{`Adjust stock — ${adjusting?.product.name ?? ''}`}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Variant\"\n value={adjusting?.variantId ?? ''}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev ? { ...prev, variantId: event.target.value } : prev,\n )\n }\n size=\"small\"\n select\n sx={{ mt: 1 }}\n >\n {(adjusting?.product.variants ?? [])\n .filter((variant) => variant.inventory != null)\n .map((variant) => (\n <MenuItem key={variant.id} value={variant.id}>\n {`${Object.values(variant.options ?? {}).join(' / ') || 'Default'} — ${variant.inventory} in stock${\n CommerceModel.heldVariantUnits(\n adjusting?.product,\n variant.id,\n nowMs,\n ) > 0\n ? `, ${CommerceModel.heldVariantUnits(\n adjusting?.product,\n variant.id,\n nowMs,\n )} reserved in checkout`\n : ''\n }`}\n </MenuItem>\n ))}\n </TextField>\n {(locationDocs?.length ?? 0) > 1 ? (\n <TextField\n label=\"Location\"\n value={adjusting?.locationId ?? ''}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev ? { ...prev, locationId: event.target.value } : prev,\n )\n }\n size=\"small\"\n select\n >\n {(locationDocs ?? []).map((location: any) => (\n <MenuItem key={location.$id} value={location.$id}>\n {location.name}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <TextField\n label=\"Change\"\n placeholder=\"+10 or -3\"\n value={adjusting?.delta ?? ''}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev\n ? {\n ...prev,\n delta: event.target.value.replace(/[^0-9+-]/g, ''),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Reason\"\n value={adjusting?.reason ?? 'restock'}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev\n ? {\n ...prev,\n reason: event.target\n .value as CommerceModel.InventoryAdjustmentReason,\n }\n : prev,\n )\n }\n size=\"small\"\n select\n >\n <MenuItem value=\"restock\">{'Restock'}</MenuItem>\n <MenuItem value=\"correction\">{'Correction'}</MenuItem>\n <MenuItem value=\"damage\">{'Damaged'}</MenuItem>\n <MenuItem value=\"refund\">{'Refund return'}</MenuItem>\n </TextField>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setAdjusting(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!Math.round(Number(adjusting?.delta))}\n onClick={handleAdjustSave}\n >\n {'Apply'}\n </Button>\n </DialogActions>\n </Dialog>\n <ProductEditorDialog\n key={editing?.$id ?? (creating ? 'new' : 'closed')}\n hostId={hostId}\n product={editing}\n // The editor is seeded from a row of THIS listener and replaces the\n // whole document, so the freshness verdict belongs here (AGL-1358) —\n // the dialog has no listener of its own to ask.\n seedFromCache={productsFromCache}\n seedUnreadable={productsStatus === 'error'}\n open={creating || editing !== null}\n onClose={() => {\n setEditing(null)\n setCreating(false)\n // The dialog reports no verdict, so a create and a cancel look\n // the same from here. Re-reading on both is one aggregate and\n // keeps the cap off the dialog's shoulders (AGL-1716).\n setProductCountEpoch((epoch) => epoch + 1)\n }}\n />\n </CardDisplay>\n )\n}\nProductsHubCard.displayName = 'ProductsHubCard'\n\nexport default ProductsHubCard\n"],"names":["Aglyn","CommerceModel","PRODUCT_LIST_BASE","PRODUCT_LIST_FIELDS","PRODUCT_LIST_HEADERS","PRODUCT_LIST_OPTIONS","PRODUCT_LIST_QUERY","PRODUCT_LIST_SELECT_FIELDS","productListRequestClauses","CardDisplay","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","listActionsColumn","QuotaReadoutComponent","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","useSnackbar","Timestamp","Box","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","TextField","Typography","addDoc","collection","doc","documentId","getCountFromServer","getDocs","limit","orderBy","query","startAfter","updateDoc","where","useCallback","useEffect","useMemo","useState","useFirestore","useUser","writeSiteWideChange","useFirestoreCollection","writeGuardedBySeed","listQueryConstraints","useListQuery","useHostResourceApi","useOrgPlan","ProductEditorDialog","productSlugLedger","productCollectionFields","readSmartCollections","ProductsHubZone","pluginDocsHelp","useConsoleWidgetSlot","EXPORT_PAGE","EXPORT_CEILING","KEY_POOL_CEILING","STATUS_COLOR","active","draft","archived","PRODUCT_VISIBLE_COLUMNS","ProductsHubCard","props","importing","hostId","firestore","data","user","enqueueSnackbar","createHostResource","org","ready","planReady","confirm","gridFilter","selectFields","filtering","clauses","length","searchWords","some","word","trim","editing","setEditing","creating","setCreating","adjusting","setAdjusting","setImporting","keysFor","setKeysFor","keysText","setKeysText","importOptions","setImportOptions","lastImport","setLastImport","WidgetSlot","rows","productDocs","productWindow","hasMore","page","setPage","pageSize","setPageSize","plan","status","productsStatus","fromCache","productsFromCache","declaration","request","search","base","deps","idField","confirmDisappearances","keyDocs","$id","keyPoolTruncated","locationDocs","products","map","product","liftLegacyProduct","nowMs","setNowMs","Date","now","anyHeld","heldProductUnits","undefined","timer","setInterval","clearInterval","productCountEpoch","setProductCountEpoch","serverProductCount","setServerProductCount","then","snapshot","count","catch","loadedProductCount","productCount","productQuota","checkQuota","handleDuplicate","allowed","variant","persist","_sourceId","skus","_skus","barcodes","_barcodes","copy","slug","claim","commerceSlug","resource","productSearchFields","name","variants","productStockFields","createdAtMs","updatedAtMs","epoch","error","message","handleStatus","productPriceMissing","write","batch","update","updatedAt","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","deletedAt","exporting","setExporting","handleExport","productsRef","constraints","exported","cursor","more","room","Math","min","empty","docs","push","id","csv","productsToCsv","blob","Blob","type","anchor","document","createElement","href","URL","createObjectURL","download","click","revokeObjectURL","toLocaleString","handleImportApply","parsed","batchQuota","slugs","created","ask","smartCollections","collectionIds","productCollectionIds","priceUsd","imageUrl","mediaUrls","key","toString","productIds","options","handleAdjustSave","delta","round","Number","adjustVariantInventory","variantId","locationId","verdict","subject","unreadable","productId","reason","atMs","ok","roomFor","onCatalogCreated","setImportOption","on","current","formatPrice","max","productPriceRange","formatStock","total","productInventory","String","formatHeld","held","productColumns","field","headerName","flex","minWidth","renderCell","row","sx","noWrap","width","label","size","sortable","span","component","valueGetter","_value","direction","spacing","justifyContent","whiteSpace","onClick","find","inventory","location","isDefault","header","help","contentGutterX","contentGutterY","xs","sm","disabled","text","used","noun","onCreated","fields","headers","onChange","setClauses","refused","notices","aria-label","columns","loading","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","sortingMode","disableColumnSorting","initialState","columnVisibilityModel","noRowsLabel","noRowsDescription","rowCount","onPageChange","onPageSizeChange","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","productKeys","slice","available","filter","assignedAtMs","retired","revokedAtMs","assigned","mt","alignItems","fontFamily","value","event","target","multiline","minRows","keys","split","alignSelf","input","accept","hidden","file","files","parseProductsCsv","maxRows","errors","slot","orgId","setOption","prev","select","Object","values","join","heldVariantUnits","placeholder","replace","seedFromCache","seedUnreadable","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;;AAEA,YAAYA,WAAW,eAAc;AACrC,YAAYC,mBAAmB,uBAAa;AAC5C,SACEC,iBAAiB,EACjBC,mBAAmB,EACnBC,oBAAoB,EACpBC,oBAAoB,EACpBC,kBAAkB,EAClBC,0BAA0B,EAC1BC,yBAAyB,QACpB,wCAAoC;AAC3C,SAASC,WAAW,EAAEC,sBAAsB,QAAQ,uBAAsB;AAC1E,OAAOC,qBAAqB,8DAA6D;AACzF,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,SACEC,gBAAgB,EAChBC,iBAAiB,QACZ,+DAA8D;AACrE,SACEC,SAAS,EACTC,iBAAiB,QACZ,uDAAsD;AAC7D,OAAOC,2BAA2B,0DAAyD;AAC3F,SAASC,sBAAsB,QAAQ,yCAAwC;AAC/E,SAASC,qBAAqB,QAAQ,8CAA6C;AACnF,SAASC,iBAAiB,QAAQ,kDAAiD;AACnF,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAASC,SAAS,QAAQ,+BAA8B;AACxD,SACEC,GAAG,EACHC,MAAM,EACNC,IAAI,EACJC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,WAAW,EACXC,QAAQ,EACRC,KAAK,EACLC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SACEC,MAAM,EACNC,UAAU,EACVC,GAAG,EACHC,UAAU,EACVC,kBAAkB,EAClBC,OAAO,EACPC,KAAK,EACLC,OAAO,EACPC,KAAK,EAELC,UAAU,EACVC,SAAS,EACTC,KAAK,QACA,qBAAoB;AAE3B,SAASC,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACjE,SAASC,YAAY,EAAEC,OAAO,QAAQ,iCAAgC;AACtE,SAASC,mBAAmB,QAAQ,gEAA+D;AACnG,SACEC,sBAAsB,EACtBC,kBAAkB,QACb,iCAAgC;AACvC,SACEC,oBAAoB,EACpBC,YAAY,QACP,sDAAqD;AAC5D,SAASC,kBAAkB,QAAQ,iCAAgC;AACnE,SAASC,UAAU,QAAQ,iCAAgC;AAC3D,OAAOC,yBAAyB,uCAAmC;AACnE,SAASC,iBAAiB,QAAQ,qBAAiB;AACnD,SAASC,uBAAuB,EAAEC,oBAAoB,QAAQ,yBAAqB;AACnF,OAAOC,qBAAqB,mCAA+B;AAC3D,SAASC,cAAc,QAAQ,eAAc;AAC7C,SAASC,oBAAoB,QAAQ,qDAAoD;AAGzF,wDAAwD,GACxD,MAAMC,cAAc;AAEpB;;;;;;;CAOC,GACD,MAAMC,iBAAiB;AAEvB;;;;;CAKC,GACD,MAAMC,mBAAmB;AAQzB,MAAMC,eAAkE;IACtEC,QAAQ;IACRC,OAAO;IACPC,UAAU;AACZ;AAEA,kFAAkF,GAClF,MAAMC,0BAA0B;IAAC;IAAQ;IAAU;IAAQ;IAAY;IAAS;CAAW;AAE3F;;;;;;;CAOC,GACD,OAAO,SAASC,gBAAgBC,KAA2B;;QAwkClCC,mBAgBFA,oBAGDA;IA1lCpB,MAAM,EAAEC,MAAM,EAAE,GAAGF;IACnB,MAAMG,YAAY5B;IAClB,MAAM,EAAE6B,MAAMC,IAAI,EAAE,GAAG7B;IACvB,MAAM,EAAE8B,eAAe,EAAE,GAAG5D;IAC5B,qEAAqE;IACrE,kEAAkE;IAClE,MAAM6D,qBAAqBzB;IAC3B,MAAM,EAAE0B,GAAG,EAAEC,OAAOC,SAAS,EAAE,GAAG3B,WAAWmB;IAC7C,MAAM,EAAES,OAAO,EAAE,GAAG5E;IACpB;;;;GAIC,GACD,MAAM6E,aAAanE,kBAAkB;QACnCoE,cAAcjF;IAChB;IACA,MAAMkF,YACJF,WAAWG,OAAO,CAACC,MAAM,GAAG,KAC5BJ,WAAWK,WAAW,CAACC,IAAI,CAAC,CAACC,OAASA,KAAKC,IAAI;IACjD,MAAM,CAACC,SAASC,WAAW,GAAGhD,SAA4B;IAC1D,MAAM,CAACiD,UAAUC,YAAY,GAAGlD,SAAS;IACzC,MAAM,CAACmD,WAAWC,aAAa,GAAGpD,SAMxB;IACV,MAAM,CAAC2B,WAAW0B,aAAa,GAAGrD,SAGxB;IACV,MAAM,CAACsD,SAASC,WAAW,GAAGvD,SAA4B;IAC1D,MAAM,CAACwD,UAAUC,YAAY,GAAGzD,SAAS;IACzC;;;;GAIC,GACD,MAAM,CAAC0D,eAAeC,iBAAiB,GAAG3D,SAAkC,CAAC;IAC7E,MAAM,CAAC4D,YAAYC,cAAc,GAC/B7D,SAAoD;IACtD,MAAM8D,aAAa9C;IAEnB,MAAM,EACJ+C,MAAMC,WAAW,EACjBlC,MAAMmC,aAAa,EACnBC,OAAO,EACPC,IAAI,EACJC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACXC,IAAI,EACJC,QAAQC,cAAc,EACtB;;;;;;;KAOC,GACDC,WAAWC,iBAAiB,EAC7B,GAAGpE,aAAkB;QACpB;;;;;;;;;;;;;;;KAeC,GACDrB,YAAYA,WAAW2C,WAAW,SAASD,QAAQ;QACnDgD,aAAavH;QACbwH,SAAS;YACPpC,SAASlF,0BAA0B+E,WAAWG,OAAO;YACrDqC,QAAQxC,WAAWK,WAAW;YAC9BoC,MAAM9H;QACR;QACA+H,MAAM;YAACnD;YAAWD;SAAO;QACzBqD,SAAS;QACT;;;;;;KAMC,GACDC,uBAAuB;IACzB;IACA;;;;;;;;;;;;;;;;;;;GAmBC,GACD,MAAM,EAAEpD,MAAMqD,OAAO,EAAE,GAAG/E,uBACxB,IACEkD,UACI7D,MACEP,WAAW2C,WAAW,SAASD,QAAQ,gBACvChC,MAAM,aAAa,MAAM0D,QAAQ8B,GAAG,GACpC5F,QAAQJ,eACR,8DAA8D;QAC9D,kDAAkD;QAClDG,MAAM4B,mBAAmB,MAE3B,MACN;QAACU;QAAWD;QAAQ0B,2BAAAA,QAAS8B,GAAG;KAAC,EACjC;QAAEH,SAAS;IAAM;IAEnB,6EAA6E,GAC7E,MAAMI,mBAAmB,SAACF,2BAAAA,QAASzC,MAAM,mBAAI,KAAKvB;IAClD,wEAAwE;IACxE,MAAM,EAAEW,MAAMwD,YAAY,EAAE,GAAGlF,uBAC7B,IAAMX,MAAMP,WAAW2C,WAAW,SAASD,QAAQ,cAAcrC,MAAM,MACvE;QAACsC;QAAWD;KAAO,EACnB;QAAEqD,SAAS;IAAM;IAEnB;;;;;GAKC,GACD,MAAMM,WAAWxF,QACf,IACE,CAACiE,sBAAAA,cAAe,EAAE,EAAEwB,GAAG,CAAC,CAACC,UAAkB,aACtCzI,cAAc0I,iBAAiB,CAACD;gBACnCL,KAAKK,QAAQL,GAAG;iBAEpB;QAACpB;KAAY;IAGf;;;;;;;;;;;;;;GAcC,GACD,MAAM,CAAC2B,OAAOC,SAAS,GAAG5F,SAAS,IAAM6F,KAAKC,GAAG;IACjD,MAAMC,UAAUhG,QACd,IACEwF,SAAS3C,IAAI,CACX,CAAC6C,UAAYzI,cAAcgJ,gBAAgB,CAACP,SAASE,SAAS,IAElE;QAACJ;QAAUI;KAAM;IAEnB7F,UAAU;QACR,IAAI,CAACiG,SAAS,OAAOE;QACrB,MAAMC,QAAQC,YAAY,IAAMP,SAASC,KAAKC,GAAG,KAAK;QACtD,OAAO,IAAMM,cAAcF;IAC7B,GAAG;QAACH;KAAQ;IAEZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BC,GACD,MAAM,CAACM,mBAAmBC,qBAAqB,GAAGtG,SAAS;IAC3D,MAAM,CAACuG,oBAAoBC,sBAAsB,GAAGxG,SAClD;IAEFF,UAAU;QACR,IAAIuB,SAAS;QACb,KAAKhC,mBAAmBH,WAAW2C,WAAW,SAASD,QAAQ,aAC5D6E,IAAI,CAAC,CAACC;YACL,IAAIrF,QAAQmF,sBAAsBE,SAAS5E,IAAI,GAAG6E,KAAK;QACzD,GACCC,KAAK,CAAC;QACL,mEAAmE;QACnE,mEAAmE;QACnE,8DAA8D;QAC9D,sCAAsC;QACxC;QACF,OAAO;YACLvF,SAAS;QACX;IACF,GAAG;QAACQ;QAAWD;QAAQyE;KAAkB;IACzC,2EAA2E;IAC3E,2EAA2E;IAC3E,4EAA4E;IAC5E,4BAA4B;IAC5B,MAAMQ,8BAAqB5C,iCAAAA,cAAevB,MAAM,oBAAI;IACpD,MAAMoE,eAAeP,6BAAAA,qBAAsBM;IAC3C,wEAAwE;IACxE,wEAAwE;IACxE,qEAAqE;IACrE,EAAE;IACF,sEAAsE;IACtE,sEAAsE;IACtE,wEAAwE;IACxE,yEAAyE;IACzE,iDAAiD;IACjD,mEAAmE;IACnE,oEAAoE;IACpE,MAAME,eAAehH,QACnB,IACEqC,YACIrF,MAAMiK,UAAU,CAAC9E,KAAK,mBAAmB4E,gBACzC,MACN;QAAC5E;QAAKE;QAAW0E;KAAa;IAGhC,MAAMG,kBAAkBpH,YACtB,CAAC4F,UAAwB;YACvB,gEAAgE;YAChE,IAAI,CAACsB,cAAc;YACnB,IAAI,CAACA,aAAaG,OAAO,EAAE;gBACzB,OAAO,KAAKlF,gBACV,CAAC,mBAAmB,EAAE+E,aAAaxH,KAAK,CAAC,4BAA4B,CAAC,EACtE;oBAAE4H,SAAS;oBAAQC,SAAS;gBAAM;YAEtC;YACA,uEAAuE;YACvE,mEAAmE;YACnE,MAAM,EAAEhC,KAAKiC,SAAS,EAAEC,MAAMC,KAAK,EAAEC,UAAUC,SAAS,EAAW,GACjEhC,SAD2DiC,wCAC3DjC;;;;;YACF,IAAI;gBACF;;;;;SAKC,GACD,MAAMkC,OAAO,MAAMhH,kBAAkBkB,WAAWD,QAAQgG,KAAK,CAC3D5K,cAAc6K,YAAY,CAAC,GAAGpC,QAAQkC,IAAI,CAAC,KAAK,CAAC;gBAEnD,mEAAmE;gBACnE,MAAM1F,mBAAmB;oBACvBL;oBACAkG,UAAU;oBACVhG,MAAM,aACD4F,MACA1K,cAAc+K,mBAAmB,CAAC;wBACnCC,MAAM,GAAGvC,QAAQuC,IAAI,CAAC,OAAO,CAAC;wBAC9BC,UAAUxC,QAAQwC,QAAQ;oBAC5B,IACGjL,cAAckL,kBAAkB,CAACzC,UAEhC,MAAM7E,wBAAwBiB,WAAWD,QAAQ,aAChD6D;wBACHuC,MAAM,GAAGvC,QAAQuC,IAAI,CAAC,OAAO,CAAC;;wBAEhCL;wBACAnD,QAAQ;wBACR2D,aAAatC,KAAKC,GAAG;wBACrBsC,aAAavC,KAAKC,GAAG;;gBAEzB;gBACA,iEAAiE;gBACjE,kEAAkE;gBAClE,6DAA6D;gBAC7DQ,qBAAqB,CAAC+B,QAAUA,QAAQ;gBACxCrG,gBAAgB,+BAA+B;oBAC7CmF,SAAS;oBACTC,SAAS;gBACX;YACF,EAAE,OAAOkB,OAAY;;gBACnBtG,wBAAgBsG,yBAAAA,MAAOC,OAAO,mBAAI,+BAA+B;oBAC/DpB,SAAS;oBACTC,SAAS;gBACX;YACF;QACF,GACA;QAACvF;QAAWD;QAAQK;QAAoBD;QAAiB+E;KAAa;IAGxE,MAAMyB,eAAe3I,YACnB,CAAC4F,SAAqBjB,SAAwC;YAC5D,sEAAsE;YACtE,oDAAoD;YACpD,IAAIA,WAAW,YAAYxH,cAAcyL,mBAAmB,CAAChD,UAAU;gBACrE,OAAO,KAAKzD,gBACV,CAAC,iCAAiC,EAAEyD,QAAQuC,IAAI,CAAC,sBAAsB,CAAC,EACxE;oBAAEb,SAAS;oBAAQC,SAAS;gBAAM;YAEtC;YACA,MAAMjH,oBAAoB;gBACxB0B;gBACAE;gBACAH;gBACA8G,OAAO,CAACC,QACNA,MAAMC,MAAM,CAACzJ,IAAI0C,WAAW,SAASD,QAAQ,YAAY6D,QAAQL,GAAG,GAAG;wBACrEZ;wBACA4D,aAAavC,KAAKC,GAAG;wBACrB+C,WAAWxK,UAAUyH,GAAG;oBAC1B;YACJ;QACF,GACA;QAACjE;QAAWD;QAAQG;QAAMC;KAAgB;IAG5C,MAAM8G,eAAejJ,YACnB,CAAC4F,UAAwB;YACvB,MAAMsD,YAAY,MAAM1G,QAAQ;gBAC9B2G,OAAO;gBACPC,aACE,CAAC,CAAC,EAAExD,QAAQuC,IAAI,CAAC,8CAA8C,CAAC,GAChE;gBACFkB,kBAAkB;gBAClBC,yBAAyB;oBAAEC,OAAO;gBAAQ;YAC5C,GACG3C,IAAI,CAAC,IAAM,MACXG,KAAK,CAAC,IAAM;YACf,IAAI,CAACmC,WAAW;YAChB,MAAM5I,oBAAoB;gBACxB0B;gBACAE;gBACAH;gBACA8G,OAAO,CAACC,QACNA,MAAMC,MAAM,CAACzJ,IAAI0C,WAAW,SAASD,QAAQ,YAAY6D,QAAQL,GAAG,GAAG;wBACrEiE,WAAWhL,UAAUyH,GAAG;oBAC1B;YACJ;YACA9D,gBAAgB,mBAAmB;gBAAEmF,SAAS;gBAAWC,SAAS;YAAM;QAC1E,GACA;QAAC/E;QAASR;QAAWD;QAAQG;QAAMC;KAAgB;IAGrD;;;;;;;;;GASC,GACD,MAAM,CAACsH,WAAWC,aAAa,GAAGvJ,SAAS;IAC3C,MAAMwJ,eAAe3J,YAAY;QAC/B0J,aAAa;QACb,IAAI;YACF,MAAME,cAAcvK,WAAW2C,WAAW,SAASD,QAAQ;YAC3D,MAAM8H,cAAcpJ,qBAAqBiE;YACzC,MAAMoF,WAAyB,EAAE;YACjC,IAAIC,SAAuC;YAC3C,IAAIC,OAAO;YACX,OAAS;gBACP,MAAMC,OAAO5I,iBAAiByI,SAASjH,MAAM;gBAC7C,MAAMgE,WAAW,MAAMpH,QACrBG,MACEgK,gBACGC,gBACCE,SAAS;oBAAClK,WAAWkK;iBAAQ,GAAG,EAAE,EACtCrK,MAAMuK,OAAO,IAAIC,KAAKC,GAAG,CAAC/I,aAAa6I,QAAQ;gBAGnD,IAAIA,QAAQ,GAAG;oBACbD,OAAO,CAACnD,SAASuD,KAAK;oBACtB;gBACF;gBACA,KAAK,MAAMxE,WAAWiB,SAASwD,IAAI,CAAE;oBACnCP,SAASQ,IAAI,CAAC,aACTnN,cAAc0I,iBAAiB,CAACD,QAAQ3D,IAAI;wBAC/CsD,KAAKK,QAAQ2E,EAAE;;gBAEnB;gBACA,IAAI1D,SAASwD,IAAI,CAACxH,MAAM,GAAGqH,KAAKC,GAAG,CAAC/I,aAAa6I,OAAO;gBACxDF,SAASlD,SAASwD,IAAI,CAACxD,SAASwD,IAAI,CAACxH,MAAM,GAAG,EAAE;YAClD;YACA,MAAM2H,MAAMrN,cAAcsN,aAAa,CAACX;YACxC,MAAMY,OAAO,IAAIC,KAAK;gBAACH;aAAI,EAAE;gBAAEI,MAAM;YAAW;YAChD,MAAMC,SAASC,SAASC,aAAa,CAAC;YACtCF,OAAOG,IAAI,GAAGC,IAAIC,eAAe,CAACR;YAClCG,OAAOM,QAAQ,GAAG,CAAC,SAAS,EAAEpJ,OAAO,IAAI,CAAC;YAC1C8I,OAAOO,KAAK;YACZH,IAAII,eAAe,CAACR,OAAOG,IAAI;YAC/B,IAAIhB,MAAM;gBACR7H,gBACE,CAAC,mBAAmB,EAAEd,eAAeiK,cAAc,GAAG,UAAU,CAAC,GAC/D,2EACF;oBAAEhE,SAAS;oBAAQC,SAAS;gBAAM;YAEtC;QACF,EAAE,OAAOkB,OAAY;;YACnBtG,wBAAgBsG,yBAAAA,MAAOC,OAAO,mBAAI,iCAAiC;gBACjEpB,SAAS;gBACTC,SAAS;YACX;QACF,SAAU;YACRmC,aAAa;QACf;IACF,GAAG;QAAC1H;QAAWD;QAAQ2C;QAAMvC;KAAgB;IAE7C,MAAMoJ,oBAAoBvL,YAAY;QACpC,MAAMwL,SAAS1J,6BAAAA,UAAW0J,MAAM;QAChC,IAAI,CAACA,UAAUA,OAAO9F,QAAQ,CAAC7C,MAAM,KAAK,GAAG;QAC7C,iEAAiE;QACjE,qEAAqE;QACrE,qEAAqE;QACrE,qEAAqE;QACrE,IAAI,CAACN,WAAW;QAChB,MAAMkJ,aAAavO,MAAMiK,UAAU,CACjC9E,KACA,mBACA4E,eAAeuE,OAAO9F,QAAQ,CAAC7C,MAAM,GAAG;QAE1C,IAAI,CAAC4I,WAAWpE,OAAO,EAAE;YACvB,OAAO,KAAKlF,gBACV,CAAC,kBAAkB,EAAEqJ,OAAO9F,QAAQ,CAAC7C,MAAM,CAAC,sBAAsB,CAAC,GACjE,CAAC,YAAY,EAAE4I,WAAW/L,KAAK,CAAC,yBAAyB,CAAC,EAC5D;gBAAE4H,SAAS;gBAAQC,SAAS;YAAM;QAEtC;QACA;;;;;KAKC,GACD,MAAMmE,QAAQ5K,kBAAkBkB,WAAWD;QAC3C,MAAM4J,UAAoB,EAAE;QAC5B,IAAI;YACF,MAAMD,MAAME,GAAG,CAACJ,OAAO9F,QAAQ,CAACC,GAAG,CAAC,CAACC,UAAYA,QAAQkC,IAAI;YAC7D,+DAA+D;YAC/D,MAAM+D,mBAAmB,MAAM7K,qBAAqBgB,WAAWD;YAC/D,qEAAqE;YACrE,oEAAoE;YACpE,KAAK,MAAM6D,WAAW4F,OAAO9F,QAAQ,CAAE;;oBAevBE,oBACAA;gBAfd,MAAMkC,OAAO,MAAM4D,MAAM3D,KAAK,CAACnC,QAAQkC,IAAI;gBAC3C,MAAM,EAAEyC,EAAE,EAAE,GAAG,MAAMnI,mBAAmB;oBACtCL;oBACAkG,UAAU;oBACVhG,MAAM,aACD2D,SAIAzI,cAAc+K,mBAAmB,CAACtC,UAClCzI,cAAckL,kBAAkB,CAACzC;wBACpC,oCAAoC;wBACpCkG,eAAe3O,cAAc4O,oBAAoB,CAACnG,SAASiG;wBAC3D/D;wBACAkE,QAAQ,WAAEpG,qBAAAA,QAAQwC,QAAQ,CAAC,EAAE,qBAAnBxC,mBAAqBoG,QAAQ,mBAAI;wBAC3CC,QAAQ,YAAErG,qBAAAA,QAAQsG,SAAS,qBAAjBtG,kBAAmB,CAAC,EAAE,oBAAI;wBACpC0C,aAAatC,KAAKC,GAAG;wBACrBsC,aAAavC,KAAKC,GAAG;;gBAEzB;gBACA0F,QAAQrB,IAAI,CAACC;YACf;QACF,EAAE,OAAO9B,OAAY;;YACnB,OAAO,KAAKtG,yBAAgBsG,yBAAAA,MAAOC,OAAO,oBAAI,iBAAiB;gBAC7DpB,SAAS;gBACTC,SAAS;YACX;QACF;QACAvD,cAAc;YACZmI,KAAK,GAAGnG,KAAKC,GAAG,GAAGmG,QAAQ,CAAC,IAAI,CAAC,EAAET,QAAQ9I,MAAM,EAAE;YACnDwJ,YAAYV;YACZW,SAASzI;QACX;QACAC,iBAAiB,CAAC;QAClBN,aAAa;QACbiD,qBAAqB,CAAC+B,QAAUA,QAAQ;QACxCrG,gBAAgB,CAAC,SAAS,EAAEqJ,OAAO9F,QAAQ,CAAC7C,MAAM,CAAC,SAAS,CAAC,EAAE;YAC7DyE,SAAS;YACTC,SAAS;QACX;IACF,GAAG;QACDzF;QACAE;QACAD;QACAK;QACAD;QACAE;QACAE;QACA0E;QACApD;KACD;IAED,MAAM0I,mBAAmBvM,YAAY;QACnC,IAAI,CAACsD,WAAW;QAChB,MAAMkJ,QAAQtC,KAAKuC,KAAK,CAACC,OAAOpJ,UAAUkJ,KAAK;QAC/C,IAAI,CAACA,OAAO;QACZ,MAAMpE,WAAWjL,cAAcwP,sBAAsB,CACnDrJ,UAAUsC,OAAO,EACjBtC,UAAUsJ,SAAS,EACnBJ,OACAlJ,UAAUuJ,UAAU,IAAIzG;QAE1B;;;;;;;;;;KAUC,GACD,MAAM0G,UAAU,MAAMtM,mBACpB;YACEuM,SAAS;YACTC,YAAYpI,mBAAmB;YAC/BC,WAAWC;QACb,GACA;YACE,MAAMxE,oBAAoB;gBACxB0B;gBACAE;gBACAH;gBACA8G,OAAO,CAACC,QACNA,MAAMC,MAAM,CACVzJ,IAAI0C,WAAW,SAASD,QAAQ,YAAYuB,UAAUsC,OAAO,CAACL,GAAG,GACjE;wBACE6C;uBAEGjL,cAAckL,kBAAkB,CAAC,aAAK/E,UAAUsC,OAAO;wBAAEwC;;wBAC5DG,aAAavC,KAAKC,GAAG;;YAG7B;YACA,sEAAsE;YACtE,MAAM7G,OACJC,WAAW2C,WAAW,SAASD,QAAQ,yBACvC;gBACEkL,WAAW3J,UAAUsC,OAAO,CAACL,GAAG;gBAChCqH,WAAWtJ,UAAUsJ,SAAS;gBAC9BJ;gBACAU,QAAQ5J,UAAU4J,MAAM;eACpB5J,UAAUuJ,UAAU,GAAG;gBAAEA,YAAYvJ,UAAUuJ,UAAU;YAAC,IAAI,CAAC;gBACnEM,MAAMnH,KAAKC,GAAG;;QAGpB;QAEF,0EAA0E;QAC1E,IAAI,CAAC6G,QAAQM,EAAE,EAAE;YACf,OAAO,KAAKjL,gBAAgB2K,QAAQpE,OAAO,EAAE;gBAC3CpB,SAAS;gBACTC,SAAS;YACX;QACF;QACAhE,aAAa;QACbpB,gBAAgB,kBAAkB;YAAEmF,SAAS;YAAWC,SAAS;QAAM;IACzE,GAAG;QACDrF;QACAoB;QACAtB;QACAD;QACAI;QACA2C;QACAF;KACD;IAED,iFAAiF,GACjF,MAAMyI,UAAUrN,YACd,CAAC8G,QACCvE,YACIrF,MAAMiK,UAAU,CAAC9E,KAAK,mBAAmB4E,eAAeH,QAAQ,KAChE,MACN;QAACzE;QAAKE;QAAW0E;KAAa;IAEhC,MAAMqG,mBAAmBtN,YACvB,IAAMyG,qBAAqB,CAAC+B,QAAUA,QAAQ,IAC9C,EAAE;IAEJ,MAAM+E,kBAAkBvN,YACtB,CAACmM,KAAaqB,KACZ1J,iBAAiB,CAAC2J,UAAa,aAAKA;gBAAS,CAACtB,IAAI,EAAEqB;iBACtD,EAAE;IAGJ,MAAME,cAAc,CAAC9H;QACnB,MAAM,CAACuE,KAAKwD,IAAI,GAAGxQ,cAAcyQ,iBAAiB,CAAChI;QACnD,OAAOuE,QAAQwD,MAAM,CAAC,CAAC,EAAExD,KAAK,GAAG,CAAC,CAAC,EAAEA,IAAI,EAAE,EAAEwD,KAAK;IACpD;IACA,MAAME,cAAc,CAACjI;QACnB,MAAMkI,QAAQ3Q,cAAc4Q,gBAAgB,CAACnI;QAC7C,IAAIkI,SAAS,MAAM,OAAO;QAC1B,OAAOA,QAAQ,IAAIE,OAAOF,SAAS;IACrC;IACA;;;;;;;;;;;;;;GAcC,GACD,MAAMG,aAAa,CAACrI;QAClB,MAAMsI,OAAO/Q,cAAcgJ,gBAAgB,CAACP,SAASE;QACrD,OAAOoI,OAAO,IAAI,CAAC,EAAE,EAAEA,KAAK,UAAU,CAAC,GAAG;IAC5C;IAEA,MAAMC,iBAAiB9P,sBACrB;QACE;YACE+P,OAAO;YACPC,YAAY;YACZC,MAAM;YACNC,UAAU;YACVC,YAAY,CAAC,EAAEC,GAAG,EAAuB,iBACvC,MAACxP;oBAAMyP,IAAI;wBAAEH,UAAU;oBAAE;;sCACvB,KAACpP;4BAAWmI,SAAQ;4BAAQqH,MAAM;sCAC/BF,IAAItG,IAAI;;sCAEX,KAAChJ;4BAAWmI,SAAQ;4BAAUiC,OAAM;4BAAiBoF,MAAM;sCACxD,CAAC,CAAC,EAAEF,IAAI3G,IAAI,CAAC,OAAO,EAAE2G,IAAIlJ,GAAG,EAAE;;;;QAIxC;QACA;YACE6I,OAAO;YACPC,YAAY;YACZO,OAAO;YACPJ,YAAY,CAAC,EAAEC,GAAG,EAAuB;oBAI9BlN;qCAHT,KAAC5C;oBACCkQ,OAAOJ,IAAI9J,MAAM;oBACjBmK,MAAK;oBACLvF,KAAK,GAAEhI,2BAAAA,YAAY,CAACkN,IAAI9J,MAAM,CAAC,YAAxBpD,2BAA4B;oBACnC+F,SAAQ;;;QAGd;QACA;YAAE8G,OAAO;YAAQC,YAAY;YAAQO,OAAO;QAAI;QAChD;YACER,OAAO;YACPC,YAAY;YACZO,OAAO;YACPG,UAAU;YACVP,YAAY,CAAC,EAAEC,GAAG,EAAuB,GACvCtR,cAAcyL,mBAAmB,CAAC6F,qBAChC,KAAC9P;oBAAKkQ,OAAM;oBAAcC,MAAK;oBAAQvF,OAAM;oBAAUjC,SAAQ;qBAE/DoG,YAAYe;QAElB;QACA;YACEL,OAAO;YACPC,YAAY;YACZO,OAAO;YACPG,UAAU;YACVP,YAAY,CAAC,EAAEC,GAAG,EAAuB,iBACvC,MAACO;;wBACEnB,YAAYY;wBACZR,WAAWQ,qBACV,KAACtP;4BAAWmI,SAAQ;4BAAUiC,OAAM;4BAAiB0F,WAAU;sCAC5DhB,WAAWQ;6BAEZ;;;QAGV;QACA;YACEL,OAAO;YACPC,YAAY;YACZO,OAAO;YACPG,UAAU;YACVG,aAAa,CAACC,QAAiBV,MAAoBA,IAAIrG,QAAQ,CAACvF,MAAM;QACxE;QACA3E,kBACE,CAAC0H,wBACC,MAAC3G;gBACCmQ,WAAU;gBACVC,SAAS;gBACTX,IAAI;oBAAEY,gBAAgB;oBAAYC,YAAY;gBAAS;;kCAEvD,KAAC7Q;wBAAOoQ,MAAK;wBAAQU,SAAS,IAAMrM,WAAWyC;kCAC5C;;kCAEH,KAAClH;wBAAOoQ,MAAK;wBAAQU,SAASpI,gBAAgBxB;kCAC3C;;oBAEFA,QAAQgF,IAAI,KAAK,0BAChB,KAAClM;wBAAOoQ,MAAK;wBAAQU,SAAS,IAAM9L,WAAWkC;kCAC5C;yBAED;oBACHzI,cAAc4Q,gBAAgB,CAACnI,YAAY,qBAC1C,KAAClH;wBACCoQ,MAAK;wBACLU,SAAS;sCAUH;gCANA5J,wBAMA,OAGA;mCAZJrC,aAAa;gCACXqC;gCACAgH,SAAS,WACPhH,yBAAAA,QAAQwC,QAAQ,CAACqH,IAAI,CACnB,CAACnI,UAAYA,QAAQoI,SAAS,IAAI,0BADpC9J,uBAEG2E,EAAE,mBAAI3E,QAAQwC,QAAQ,CAAC,EAAE,CAACmC,EAAE;gCACjCiC,OAAO;gCACPU,QAAQ;gCACRL,UAAU,GACR,kBAAA,QAAA,CAACpH,uBAAAA,eAAgB,EAAE,EAAEgK,IAAI,CACvB,CAACE,WAAkBA,SAASC,SAAS,sBADvC,MAEGrK,GAAG,qBACN,IAAA,CAACE,uBAAAA,eAAgB,EAAE,CAAC,CAAC,EAAE,qBAAvB,EAAyBF,GAAG,YAH5B,QAIA;4BACJ;;kCAGD;yBAED;kCACJ,KAAC7G;wBACCoQ,MAAK;wBACLU,SAAS7G,aACP/C,SACAA,QAAQjB,MAAM,KAAK,aAAa,WAAW;kCAG5CiB,QAAQjB,MAAM,KAAK,aAAa,aAAa;;kCAEhD,KAACjG;wBAAOoQ,MAAK;wBAAQvF,OAAM;wBAAQiG,SAASvG,aAAarD;kCACtD;;;gBAIP;YAAEgJ,OAAO;QAAI;KAEhB,EACDvR,qBACAE,sBACAD;IAGF,qBACE,MAACK;QACC;;;;;;;;;;OAUC,GACDkS,QACElN,YACI,aACA,CAAC,QAAQ,EAAEsE,eAAe,CAAC,EAAE,EAAEA,aAAa,CAAC,CAAC,GAAG,IAAI;QAE3D6I,MAAM5O,eAAe,YAAY;YAAE2J,QAAQ;QAAgB;QAC3DkF,cAAc;QACdC,cAAc;;0BAEd,MAAC/Q;gBAAMoQ,SAAS;;kCACd,MAACpQ;wBAAMmQ,WAAW;4BAAEa,IAAI;4BAAUC,IAAI;wBAAM;wBAAGb,SAAS;;0CACtD,KAAC5Q;gCAAIiQ,IAAI;oCAAEJ,MAAM;gCAAE;;0CACnB,KAAC5P;gCACC4I,SAAQ;gCACRiC,OAAM;gCACNuF,MAAK;gCACLqB,UAAU,CAACjJ;gCACXsI,SAAS;oCACP,IAAI,CAACtI,cAAc;oCACnB,IAAI,CAACA,aAAaG,OAAO,EAAE;wCACzB,OAAO,KAAKlF,gBACV,CAAC,mBAAmB,EAAE+E,aAAaxH,KAAK,CAAC,YAAY,CAAC,GACpD,oBACF;4CAAE4H,SAAS;4CAAQC,SAAS;wCAAM;oCAEtC;oCACAlE,YAAY;gCACd;0CAEC;;0CAEH,KAAC3E;gCACCoQ,MAAK;gCACLqB,UAAU,CAAC5N;gCACXiN,SAAS;oCACP1L,iBAAiB,CAAC;oCAClBN,aAAa;wCAAE4M,MAAM;wCAAI5E,QAAQ;oCAAK;gCACxC;0CAEC;;0CAEH,KAAC9M;gCACCoQ,MAAK;gCACL,2DAA2D;gCAC3DqB,UAAU1G,aAAc/D,SAAS7C,MAAM,KAAK,KAAKyB,SAAS;gCAC1DkL,SAAS7F;0CAERF,YAAY,eAAe;;;;kCAOhC,KAACtL;wBACCmE,OAAO4E,iBAAiB;wBACxBmJ,MAAMpJ;wBACNvH,KAAK,WAAEwH,gCAAAA,aAAcxH,KAAK,oBAAI;wBAC9B4Q,MAAK;;kCAKP,KAACrP;wBACCc,QAAQA;wBACR2D,UAAUA;wBACV2H,SAASA;wBACTtJ,YAAYA;wBACZwM,WAAWjD;;kCAEb,KAACzP;wBACC2S,QAAQnT;wBACRoT,SAASnT;wBACTsF,SAASH,WAAWG,OAAO;wBAC3B8N,UAAUjO,WAAWkO,UAAU;wBAC/BrE,SAAS/O;;kCAEX,KAACQ;wBACC6S,SAAS5S,kBAAkB0G,KAAKkM,OAAO,EAAE;4BACvCJ,QAAQnT;4BACRoT,SAASnT;4BACTgP,SAAS/O;wBACX;wBACAsT,SAASnM,KAAKmM,OAAO;;kCAEvB,KAAC5S;wBACC6S,cAAW;wBACX5M,MAAMwB;wBACNqL,SAAS5C;wBACT6C,SAASpM,mBAAmB;wBAC5B,8DAA8D;wBAC9D,wCAAwC;wBACxCqM,UAAU;wBACVC,YAAW;wBACXC,aAAa1O,WAAW0O,WAAW;wBACnCC,qBAAqB3O,WAAW2O,mBAAmB;wBACnDC,WAAW;wBACX,uDAAuD;wBACvDC,aAAY;wBACZC,oBAAoB;wBACpBC,cAAc;4BACZT,SAAS;gCACPU,uBAAuBrT,uBACrBf,qBACAsE;4BAEJ;wBACF;wBACA+P,aACE/O,YAAY,oCAAoC;wBAElDgP,mBACEhP,YACIyD,YACA,2DACA;;oBAGPV,SAAS7C,MAAM,KAAK,KAAKyB,SAAS,KAAKM,mBAAmB,YAAY,OACrE;;uEAE6D,iBAC7D,KAAC9G;wBACCwG,MAAMA;wBACNE,UAAUA;wBACVoN,UAAUlM,SAAS7C,MAAM;wBACzBwB,SAASA;wBACTwN,cAActN;wBACduN,kBAAkBrN;;;;0BAIxB,MAAC7F;gBACCmT,MAAMC,QAAQvO;gBACdwO,SAAS,IAAMvO,WAAW;gBAC1BwO,UAAS;gBACTC,SAAS;;kCAET,KAACpT;kCAAa,CAAC,eAAe,WAAE0E,2BAAAA,QAAS0E,IAAI,oBAAI,IAAI;;kCACrD,KAACrJ;wBAAc4P,IAAI;4BAAE0D,SAAS;4BAAQC,eAAe;4BAAUC,KAAK;wBAAE;kCACnE,AAAC,CAAA;4BACA,gEAAgE;4BAChE,mEAAmE;4BACnE,uDAAuD;4BACvD,MAAMC,cAAc,CAACjN,kBAAAA,UAAW,EAAE,EAAEkN,KAAK,CAAC,GAAGlR;4BAC7C,MAAMmR,YAAYF,YAAYG,MAAM,CAClC,CAACvG,MAAa,CAACA,IAAIwG,YAAY;4BAEjC,mEAAmE;4BACnE,mEAAmE;4BACnE,8DAA8D;4BAC9D,kEAAkE;4BAClE,+DAA+D;4BAC/D,MAAMC,UAAUL,YAAYG,MAAM,CAAC,CAACvG,MAAaA,IAAI0G,WAAW;4BAChE,MAAMC,WACJP,YAAY1P,MAAM,GAAG4P,UAAU5P,MAAM,GAAG+P,QAAQ/P,MAAM;4BACxD,qBACE;;kDACE,KAAC1D;wCAAWmI,SAAQ;wCAAQiC,OAAM;wCAAiBmF,IAAI;4CAAEqE,IAAI;wCAAE;kDAC5D,GAAGN,UAAU5P,MAAM,CAAC,aAAa,EAAEiQ,SAAS,SAAS,CAAC,GACpDF,CAAAA,QAAQ/P,MAAM,GACX,CAAC,GAAG,EAAE+P,QAAQ/P,MAAM,CAAC,6CAA6C,CAAC,GACnE,EAAC,IACJ2C,CAAAA,mBACG,CAAC,SAAS,EAAElE,iBAAiB,UAAU,CAAC,GACxC,EAAC,IACL;;oCAEHmR,UAAUD,KAAK,CAAC,GAAG,GAAG7M,GAAG,CAAC,CAACwG,oBAC1B,MAAClN;4CAECmQ,WAAU;4CACVC,SAAS;4CACTX,IAAI;gDAAEsE,YAAY;4CAAS;;8DAE3B,KAAC7T;oDAAWmI,SAAQ;oDAAUoH,IAAI;wDAAEJ,MAAM;wDAAG2E,YAAY;oDAAY;oDAAGtE,MAAM;8DAC3ExC,IAAIA,GAAG;;8DAEV,KAACzN;oDACCoQ,MAAK;oDACLvF,OAAM;oDACNiG,SAAS,IACP1P,UACER,IAAI0C,WAAW,SAASD,QAAQ,eAAeoK,IAAI5G,GAAG,GACtD;4DAAEsN,aAAa7M,KAAKC,GAAG;4DAAI0M,cAAc3M,KAAKC,GAAG;wDAAG;8DAIvD;;;2CAlBEkG,IAAI5G,GAAG;kDAsBhB,KAACrG;wCACC2P,OAAM;wCACNqE,OAAOvP;wCACP+M,UAAU,CAACyC,QAAUvP,YAAYuP,MAAMC,MAAM,CAACF,KAAK;wCACnDpE,MAAK;wCACLuE,SAAS;wCACTC,SAAS;;;;wBAIjB,CAAA;;kCAEF,MAACzU;;0CACC,KAACH;gCAAO8Q,SAAS,IAAM9L,WAAW;0CAAQ;;0CAC1C,KAAChF;gCACC4I,SAAQ;gCACRiC,OAAM;gCACN4G,UAAU,CAACxM,SAASV,IAAI;gCACxBuM,SAAS;oCACP,MAAM+D,OAAO5P,SACV6P,KAAK,CAAC,MACN7N,GAAG,CAAC,CAACwG,MAAQA,IAAIlJ,IAAI,IACrByP,MAAM,CAACV,SACPQ,KAAK,CAAC,GAAG;oCACZ,KAAK,MAAMrG,OAAOoH,KAAM;wCACtB,MAAMnU,OACJC,WAAW2C,WAAW,SAASD,QAAQ,gBACvC;4CACEkL,WAAWxJ,QAAS8B,GAAG;4CACvB4G;4CACAwG,cAAc;4CACdrK,aAAatC,KAAKC,GAAG;wCACvB;oCAEJ;oCACArC,YAAY;oCACZzB,gBAAgB,CAAC,MAAM,EAAEoR,KAAK1Q,MAAM,CAAC,KAAK,CAAC,EAAE;wCAC3CyE,SAAS;wCACTC,SAAS;oCACX;gCACF;0CAEC;;;;;;0BAIP,MAAC3I;gBACCmT,MAAMC,QAAQlQ;gBACdmQ,SAAS,IAAMzO,aAAa;gBAC5B0O,UAAS;gBACTC,SAAS;;kCAET,KAACpT;kCAAa;;kCACd,MAACD;wBAAc4P,IAAI;4BAAE0D,SAAS;4BAAQC,eAAe;4BAAUC,KAAK;wBAAE;;0CACpE,KAACnT;gCAAWmI,SAAQ;gCAAQiC,OAAM;gCAAiBmF,IAAI;oCAAEqE,IAAI;gCAAE;0CAC5D,+DACC;;0CAEJ,MAACrU;gCAAOuQ,WAAU;gCAAQH,MAAK;gCAAQJ,IAAI;oCAAE+E,WAAW;gCAAa;;oCAClE;kDACD,KAACC;wCACC9I,MAAK;wCACL+I,QAAO;wCACPC,MAAM;wCACNlD,UAAU,OAAOyC;gDACFA;4CAAb,MAAMU,QAAOV,sBAAAA,MAAMC,MAAM,CAACU,KAAK,qBAAlBX,mBAAoB,CAAC,EAAE;4CACpC,IAAI,CAACU,MAAM;4CACX,MAAMzD,OAAO,MAAMyD,KAAKzD,IAAI;4CAC5B5M,aAAa;gDAAE4M;gDAAM5E,QAAQrO,cAAc4W,gBAAgB,CAAC3D;4CAAM;wCACpE;;;;0CAGJ,KAAClR;gCACC2P,OAAM;gCACNqE,KAAK,WAAEpR,6BAAAA,UAAWsO,IAAI,oBAAI;gCAC1BM,UAAU,CAACyC,QACT3P,aAAa;wCACX4M,MAAM+C,MAAMC,MAAM,CAACF,KAAK;wCACxB1H,QAAQ2H,MAAMC,MAAM,CAACF,KAAK,CAACjQ,IAAI,KAC3B9F,cAAc4W,gBAAgB,CAACZ,MAAMC,MAAM,CAACF,KAAK,IACjD;oCACN;gCAEFpE,MAAK;gCACLuE,SAAS;gCACTC,SAAS;gCACTU,SAAS;;4BAEVlS,CAAAA,6BAAAA,UAAW0J,MAAM,kBAChB;;kDACE,KAACrM;wCAAWmI,SAAQ;kDACjB,CAAC,gBAAgB,EAAExF,UAAU0J,MAAM,CAAC9F,QAAQ,CAAC7C,MAAM,CAAC,SAAS,CAAC,GAC5Df,CAAAA,UAAU0J,MAAM,CAACyI,MAAM,CAACpR,MAAM,GAC3B,CAAC,GAAG,EAAEf,UAAU0J,MAAM,CAACyI,MAAM,CAACpR,MAAM,CAAC,cAAc,CAAC,GACpD,EAAC;;oCAERf,UAAU0J,MAAM,CAACyI,MAAM,CAACzB,KAAK,CAAC,GAAG,GAAG7M,GAAG,CAAC,CAAC8C,sBACxC,KAACtJ;4CAECmI,SAAQ;4CACRiC,OAAM;sDAELd;2CAJIA;;iCAQT;4BACHxE,eAAcnC,8BAAAA,oBAAAA,UAAW0J,MAAM,qBAAjB1J,kBAAmB4D,QAAQ,CAAC7C,MAAM,kBAC/C,KAACoB;gCACCiQ,MAAK;gCACLnS,QAAQA;gCACRoS,OAAO/N;gCACPU,OAAOhF,UAAU0J,MAAM,CAAC9F,QAAQ,CAAC7C,MAAM;gCACvCyJ,SAASzI;gCACTuQ,WAAW7G;iCAEX;;;kCAEN,MAAC1O;;0CACC,KAACH;gCAAO8Q,SAAS,IAAMhM,aAAa;0CAAQ;;0CAC5C,KAAC9E;gCACC4I,SAAQ;gCACRiC,OAAM;gCACN4G,UAAU,EAACrO,8BAAAA,qBAAAA,UAAW0J,MAAM,qBAAjB1J,mBAAmB4D,QAAQ,CAAC7C,MAAM;gCAC7C2M,SAASjE;0CAER,CAAC,MAAM,EAAEzJ,CAAAA,8BAAAA,qBAAAA,UAAW0J,MAAM,qBAAjB1J,mBAAmB4D,QAAQ,CAAC7C,MAAM,IAAG,CAAC,CAAC,EAAEf,UAAU0J,MAAM,CAAC9F,QAAQ,CAAC7C,MAAM,EAAE,GAAG,IAAI;;;;;;0BAIlG,MAACjE;gBACCmT,MAAMC,QAAQ1O;gBACd2O,SAAS,IAAM1O,aAAa;gBAC5B2O,UAAS;gBACTC,SAAS;;kCAET,KAACpT;kCAAa,CAAC,eAAe,WAAEuE,6BAAAA,UAAWsC,OAAO,CAACuC,IAAI,oBAAI,IAAI;;kCAC/D,MAACrJ;wBAAc4P,IAAI;4BAAE0D,SAAS;4BAAQC,eAAe;4BAAUC,KAAK;wBAAE;;0CACpE,KAACpT;gCACC2P,OAAM;gCACNqE,KAAK,WAAE5P,6BAAAA,UAAWsJ,SAAS,oBAAI;gCAC/B8D,UAAU,CAACyC,QACT5P,aAAa,CAAC8Q,OACZA,OAAO,aAAKA;4CAAMzH,WAAWuG,MAAMC,MAAM,CAACF,KAAK;6CAAKmB;gCAGxDvF,MAAK;gCACLwF,MAAM;gCACN5F,IAAI;oCAAEqE,IAAI;gCAAE;0CAEX,UAACzP,6BAAAA,UAAWsC,OAAO,CAACwC,QAAQ,oBAAI,EAAE,EAChCsK,MAAM,CAAC,CAACpL,UAAYA,QAAQoI,SAAS,IAAI,MACzC/J,GAAG,CAAC,CAAC2B;wCAEgBA;yDADpB,KAACtI;wCAA0BkU,OAAO5L,QAAQiD,EAAE;kDACzC,GAAGgK,OAAOC,MAAM,EAAClN,mBAAAA,QAAQgF,OAAO,YAAfhF,mBAAmB,CAAC,GAAGmN,IAAI,CAAC,UAAU,UAAU,GAAG,EAAEnN,QAAQoI,SAAS,CAAC,SAAS,EAChGvS,cAAcuX,gBAAgB,CAC5BpR,6BAAAA,UAAWsC,OAAO,EAClB0B,QAAQiD,EAAE,EACVzE,SACE,IACA,CAAC,EAAE,EAAE3I,cAAcuX,gBAAgB,CACjCpR,6BAAAA,UAAWsC,OAAO,EAClB0B,QAAQiD,EAAE,EACVzE,OACA,qBAAqB,CAAC,GACxB,IACJ;uCAbWwB,QAAQiD,EAAE;;;4BAiB9B,UAAC9E,gCAAAA,aAAc5C,MAAM,oBAAI,KAAK,kBAC7B,KAAC3D;gCACC2P,OAAM;gCACNqE,KAAK,WAAE5P,6BAAAA,UAAWuJ,UAAU,oBAAI;gCAChC6D,UAAU,CAACyC,QACT5P,aAAa,CAAC8Q,OACZA,OAAO,aAAKA;4CAAMxH,YAAYsG,MAAMC,MAAM,CAACF,KAAK;6CAAKmB;gCAGzDvF,MAAK;gCACLwF,MAAM;0CAEL,CAAC7O,uBAAAA,eAAgB,EAAE,EAAEE,GAAG,CAAC,CAACgK,yBACzB,KAAC3Q;wCAA4BkU,OAAOvD,SAASpK,GAAG;kDAC7CoK,SAASxH,IAAI;uCADDwH,SAASpK,GAAG;iCAK7B;0CACJ,KAACrG;gCACC2P,OAAM;gCACN8F,aAAY;gCACZzB,KAAK,YAAE5P,6BAAAA,UAAWkJ,KAAK,qBAAI;gCAC3BkE,UAAU,CAACyC,QACT5P,aAAa,CAAC8Q,OACZA,OACI,aACKA;4CACH7H,OAAO2G,MAAMC,MAAM,CAACF,KAAK,CAAC0B,OAAO,CAAC,aAAa;6CAEjDP;gCAGRvF,MAAK;;0CAEP,MAAC5P;gCACC2P,OAAM;gCACNqE,KAAK,YAAE5P,6BAAAA,UAAW4J,MAAM,qBAAI;gCAC5BwD,UAAU,CAACyC,QACT5P,aAAa,CAAC8Q,OACZA,OACI,aACKA;4CACHnH,QAAQiG,MAAMC,MAAM,CACjBF,KAAK;6CAEVmB;gCAGRvF,MAAK;gCACLwF,MAAM;;kDAEN,KAACtV;wCAASkU,OAAM;kDAAW;;kDAC3B,KAAClU;wCAASkU,OAAM;kDAAc;;kDAC9B,KAAClU;wCAASkU,OAAM;kDAAU;;kDAC1B,KAAClU;wCAASkU,OAAM;kDAAU;;;;;;kCAG9B,MAACrU;;0CACC,KAACH;gCAAO8Q,SAAS,IAAMjM,aAAa;0CAAQ;;0CAC5C,KAAC7E;gCACC4I,SAAQ;gCACRiC,OAAM;gCACN4G,UAAU,CAACjG,KAAKuC,KAAK,CAACC,OAAOpJ,6BAAAA,UAAWkJ,KAAK;gCAC7CgD,SAASjD;0CAER;;;;;;0BAIP,KAAC1L;gBAECkB,QAAQA;gBACR6D,SAAS1C;gBACT,oEAAoE;gBACpE,qEAAqE;gBACrE,gDAAgD;gBAChD2R,eAAe/P;gBACfgQ,gBAAgBlQ,mBAAmB;gBACnCmN,MAAM3O,YAAYF,YAAY;gBAC9B+O,SAAS;oBACP9O,WAAW;oBACXE,YAAY;oBACZ,+DAA+D;oBAC/D,8DAA8D;oBAC9D,uDAAuD;oBACvDoD,qBAAqB,CAAC+B,QAAUA,QAAQ;gBAC1C;yBAhBKtF,2BAAAA,QAASqC,GAAG,qBAAKnC,WAAW,QAAQ;;;AAoBjD;AACAxB,gBAAgBmT,WAAW,GAAG;AAE9B,eAAenT,gBAAe"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/products-hub-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport * as CommerceModel from '../../model'\nimport {\n PRODUCT_LIST_BASE,\n PRODUCT_LIST_FIELDS,\n PRODUCT_LIST_HEADERS,\n PRODUCT_LIST_OPTIONS,\n PRODUCT_LIST_QUERY,\n PRODUCT_LIST_SELECT_FIELDS,\n productListRequestClauses,\n} from '../../constants/product-list-query'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListQueryNotices,\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Box,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n doc,\n documentId,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n type QueryDocumentSnapshot,\n startAfter,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { writeSiteWideChange } from '@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change'\nimport {\n useFirestoreCollection,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n listQueryConstraints,\n useListQuery,\n} from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useHostResourceApi } from '@aglyn/tenant-feature-instance'\nimport { useOrgPlan } from '@aglyn/tenant-feature-instance'\nimport ProductEditorDialog from './product-editor-dialog.component'\nimport { productSlugLedger } from './product-slugs'\nimport { productCollectionFields, readSmartCollections } from './smart-collections'\nimport ProductsHubZone from './products-hub-zone.component'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n PRODUCT_IMPORT_ZONE,\n type ConsoleProductsHubZoneProps,\n} from './product-zones'\n\n/** Products read per request of the CSV export's walk. */\nconst EXPORT_PAGE = 500\n\n/**\n * The most products one CSV export writes (AGL-3321).\n *\n * The export walks the table's own query page by page, so it writes what the\n * filters and search match across the whole catalog rather than the rows on\n * screen. It stops here because the file is built in the browser, and says\n * so when it does.\n */\nconst EXPORT_CEILING = 10_000\n\n/**\n * How many of ONE product's license keys the dialog reads.\n *\n * Per product, not per store: the query carries the `productId` equality, so\n * this bounds a single pool rather than the site's whole key collection.\n */\nconst KEY_POOL_CEILING = 500\n\nexport interface ProductsHubCardProps {\n hostId: string\n}\n\ntype ProductRow = CommerceModel.HostProduct & { $id: string }\n\nconst STATUS_COLOR: Record<string, 'default' | 'success' | 'warning'> = {\n active: 'success',\n draft: 'warning',\n archived: 'default',\n}\n\n/** The grid's own columns; every other declared field is a hidden filter column. */\nconst PRODUCT_VISIBLE_COLUMNS = ['name', 'status', 'type', 'priceUsd', 'stock', 'variants']\n\n/**\n * Products hub v1 (AGL-279): the catalog manager replacing the Commerce\n * Starter card — a table over `hosts/{hostId}/products` whose search and\n * filters are its Firestore query (AGL-3321), full editor dialog, duplicate, archive/activate, soft delete (past\n * order rows keep resolving). Product cap (`productsPerHost`, AGL-278)\n * gated here on create/duplicate/import (AGL-471); server-side\n * enforcement of the client-write path rides AGL-473.\n */\nexport function ProductsHubCard(props: ProductsHubCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n // Product cap (AGL-471): per-plan `productsPerHost`, same pattern as\n // locations. Console-side gate; server enforcement rides AGL-473.\n const createHostResource = useHostResourceApi()\n const { org, ready: planReady } = useOrgPlan(hostId)\n const { confirm } = useConfirmationContext()\n /*\n * The grid's Filters panel and quick search, bound to the clauses the\n * query below serves (AGL-3321). Every clause and the search word go on\n * that query; see `constants/product-list-query.ts` for what is offered.\n */\n const gridFilter = useListGridFilter({\n selectFields: PRODUCT_LIST_SELECT_FIELDS,\n })\n const filtering =\n gridFilter.clauses.length > 0 ||\n gridFilter.searchWords.some((word) => word.trim())\n const [editing, setEditing] = useState<ProductRow | null>(null)\n const [creating, setCreating] = useState(false)\n const [adjusting, setAdjusting] = useState<{\n product: ProductRow\n variantId: string\n delta: string\n reason: CommerceModel.InventoryAdjustmentReason\n locationId: string\n } | null>(null)\n const [importing, setImporting] = useState<{\n text: string\n parsed: CommerceModel.ProductCsvImport | null\n } | null>(null)\n const [keysFor, setKeysFor] = useState<ProductRow | null>(null)\n const [keysText, setKeysText] = useState('')\n /**\n * What the import zone set for the import in the dialog, and what the last\n * import created with those options (AGL-2916), which the hub's zone hands\n * to its widgets.\n */\n const [importOptions, setImportOptions] = useState<Record<string, boolean>>({})\n const [lastImport, setLastImport] =\n useState<ConsoleProductsHubZoneProps['lastImport']>(null)\n const WidgetSlot = useConsoleWidgetSlot()\n\n const {\n rows: productDocs,\n data: productWindow,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n status: productsStatus,\n /**\n * The product rows the stock dialog is seeded from are unconfirmed by\n * the server (AGL-1358). A stock adjustment does not write the delta —\n * it recomputes the WHOLE `variants` array from the seeded product and\n * replaces it, so a cached seed silently reverts every sale, return and\n * adjustment the server has recorded since that snapshot, on every\n * variant and every location.\n */\n fromCache: productsFromCache,\n } = useListQuery<any>({\n /*\n * PAGED BY THE QUERY, and every filter ON it (AGL-3321).\n *\n * The table read a five-hundred-product window and put ONE predicate on\n * it — the search or the Status clause — then matched Status beside a\n * search, dropped soft-deleted products and sorted by name over the rows\n * it held, and paged that. Past five hundred products, a match past the\n * window answered \"no products match\", and a page came back short by\n * however many deleted products it had held.\n *\n * Now the plan puts the search word and every clause on one query under\n * the `deletedAt == null` scope, ordered by `nameLower`, and each page is\n * a page of that query — one page plus a probe row, widened as the reader\n * pages forward. Nothing is matched over the rows afterwards; a clause\n * the query cannot hold is refused by name (`ListQueryNotices` below).\n */\n collection: collection(firestore, 'hosts', hostId, 'products'),\n declaration: PRODUCT_LIST_QUERY,\n request: {\n clauses: productListRequestClauses(gridFilter.clauses),\n search: gridFilter.searchWords,\n base: PRODUCT_LIST_BASE,\n },\n deps: [firestore, hostId],\n idField: '$id',\n /*\n * The scope and the filters read MUTABLE fields — a delete sets\n * `deletedAt`, Archive moves `status`, a rename moves the tokens — so a\n * product can leave this query mid-session, and the SDK can cache that\n * departure as a tombstone at the product's own path (AGL-1196). A\n * disappearance is confirmed against the server before it is believed.\n */\n confirmDisappearances: true,\n })\n /*\n * License key pool (AGL-308) for the open dialog's product — asked FOR that\n * product, and ordered (AGL-2501).\n *\n * This read `limit(500)` over the site's whole `licenseKeys` collection with\n * no `orderBy`, then filtered by `productId` in the browser. Firestore\n * answers an unordered limit in DOCUMENT-ID order, so on a store past five\n * hundred keys the window was an arbitrary five hundred taken across every\n * product — and the \"N available\" line below counted what happened to be in\n * it. A product whose keys all hashed high showed a pool of zero while the\n * storefront went on delivering them.\n *\n * The equality moves into the query, so the window is this product's keys\n * rather than the store's. `orderBy(documentId())` orders on the document\n * NAME, which cannot be absent — every candidate FIELD here (`createdAtMs`,\n * `assignedAtMs`, `revokedAtMs`) is either optional by design or absent on\n * exactly the keys the counts are about. An equality plus an ordering on\n * `__name__` is served by the automatic single-field index, so this adds no\n * composite for anyone to deploy.\n */\n const { data: keyDocs } = useFirestoreCollection<any>(\n () =>\n keysFor\n ? query(\n collection(firestore, 'hosts', hostId, 'licenseKeys'),\n where('productId', '==', keysFor.$id),\n orderBy(documentId()),\n // One past the ceiling, so a pool larger than the window is a\n // fact rather than a guess from `length === 500`.\n limit(KEY_POOL_CEILING + 1),\n )\n : null,\n [firestore, hostId, keysFor?.$id],\n { idField: '$id' },\n )\n /** The key pool is larger than the window, so the counts below are partial. */\n const keyPoolTruncated = (keyDocs?.length ?? 0) > KEY_POOL_CEILING\n // Locations (AGL-286): the stock dialog buckets deltas when they exist.\n const { data: locationDocs } = useFirestoreCollection<any>(\n () => query(collection(firestore, 'hosts', hostId, 'locations'), limit(25)),\n [firestore, hostId],\n { idField: '$id' },\n )\n /*\n * The PAGE on screen, lifted from legacy shapes. It is what the table\n * draws, the reserved-stock clock watches and the zone hands its widgets,\n * and nothing narrows it: the query already answered every filter, and\n * soft-deleted products are outside its scope.\n */\n const products = useMemo<ProductRow[]>(\n () =>\n (productDocs ?? []).map((product: any) => ({\n ...CommerceModel.liftLegacyProduct(product),\n $id: product.$id,\n })),\n [productDocs],\n )\n\n /**\n * A CLOCK, because a hold lapses without anybody writing anything\n * (AGL-2356).\n *\n * The Firestore listener re-renders on document changes, and a reservation\n * expiring is not one — `expiresAtMs` simply passes. Without a tick the\n * \"reserved\" caption below would keep naming a hold that lapsed twenty\n * minutes ago, which is worse than not showing it at all: the merchant would\n * be told stock is spoken for while the storefront happily sells it.\n *\n * Only runs while something is actually held, so an ordinary catalog costs\n * no timer. A NEW hold arrives as a document change, which re-renders, which\n * re-arms this. It watches the page on screen, which is every row whose\n * caption it keeps true.\n */\n const [nowMs, setNowMs] = useState(() => Date.now())\n const anyHeld = useMemo(\n () =>\n products.some(\n (product) => CommerceModel.heldProductUnits(product, nowMs) > 0,\n ),\n [products, nowMs],\n )\n useEffect(() => {\n if (!anyHeld) return undefined\n const timer = setInterval(() => setNowMs(Date.now()), 60_000)\n return () => clearInterval(timer)\n }, [anyHeld])\n\n /**\n * The catalog HEAD-COUNT is a server aggregate, not the length of the\n * capped listener (AGL-1716, the AGL-1706 shape).\n *\n * The listener is `limit(500)` — correctly; a 25,000-product catalog does\n * not belong in a table. What it must not do is answer \"how many products\n * does this site have\", and it did: the length saturated at 500 and was\n * handed to `checkQuota(org, 'productsPerHost', …)` and to the batch check\n * the CSV importer runs. The bands are 2,500 / 10,000 / 25,000 above the\n * window, so on Pro and up the check compared 500 against thousands and\n * could never refuse — the card offered headroom and `api/hosts/resources`\n * then refused the create, which is the AGL-1716 shape exactly.\n *\n * The aggregate is deliberately UNFILTERED, which also closes a second,\n * quieter disagreement: `api/hosts/resources` enforces this quota with a\n * plain `collection('products').count()`, and `softDeletes` there governs\n * only the flat per-host cap on webhooks — so the server has always\n * counted soft-deleted products toward `productsPerHost` while this card\n * excluded them. The card now asks the enforcing route's question, in the\n * enforcing route's terms.\n *\n * The table pages its own query (AGL-3321) and answers a different\n * question — which products match — so it never stands in for this. A\n * one-shot goes stale where a listener refreshed for free, so the count is\n * re-read after any mutation that moves it.\n *\n * No counting RULE moves: `checkQuota` is untouched and `report-usage`\n * meters contacts, storage and API requests — never the catalog.\n */\n const [productCountEpoch, setProductCountEpoch] = useState(0)\n const [serverProductCount, setServerProductCount] = useState<number | null>(\n null,\n )\n useEffect(() => {\n let active = true\n void getCountFromServer(collection(firestore, 'hosts', hostId, 'products'))\n .then((snapshot) => {\n if (active) setServerProductCount(snapshot.data().count)\n })\n .catch(() => {\n // Falls back to the live-row count below — a LOWER bound, and this\n // card's prior behaviour. Deliberately not 0: `checkQuota` answers\n // from whatever it is handed, and 0 used is a confident wrong\n // number in the flattering direction.\n })\n return () => {\n active = false\n }\n }, [firestore, hostId, productCountEpoch])\n // The fallback, pending or denied: the live products the table's query has\n // read, probe row included. Each is a product the site has, so it can only\n // UNDERSTATE, never overstate, and nothing it gates fires on a count larger\n // than the truth (AGL-471).\n const loadedProductCount = productWindow?.length ?? 0\n const productCount = serverProductCount ?? loadedProductCount\n // Gate only once the org doc has loaded: an unresolved org reads as the\n // free tier's 0-product cap, which swallowed every Add/Duplicate click.\n // The resources API (AGL-473) stays the authoritative cap on create.\n //\n // `planReady` rather than `org` truthiness (AGL-1064): a host with no\n // owning org never produces one, and waiting on the value alone would\n // hold this open forever. The old fallback allowed UNLIMITED during the\n // window — safe only because the API re-checks; the controls now disable\n // instead, which does not lean on that backstop.\n // NULL means \"not known yet\", which a boolean cannot say — the old\n // fallback object claimed UNLIMITED and every consumer believed it.\n const productQuota = useMemo(\n () =>\n planReady\n ? Aglyn.checkQuota(org, 'productsPerHost', productCount)\n : null,\n [org, planReady, productCount],\n )\n\n const handleDuplicate = useCallback(\n (product: ProductRow) => async () => {\n // Plan unknown — the control is disabled; this guards the race.\n if (!productQuota) return\n if (!productQuota.allowed) {\n return void enqueueSnackbar(\n `Your plan includes ${productQuota.limit} products — upgrade for more`,\n { variant: 'info', persist: false },\n )\n }\n // The source's SKU and barcode keys are not copied: the copy's own are\n // derived below, and a copy of a product with none must have none.\n const { $id: _sourceId, skus: _skus, barcodes: _barcodes, ...copy } =\n product as ProductRow & { skus?: string[]; barcodes?: string[] }\n try {\n /*\n * The copy's slug is asked of the store, so a second Duplicate of\n * the same product does not mint a second `-copy` (AGL-3321), and\n * its search keys are the COPY's: spreading the source's would sort\n * and find it by the source's name.\n */\n const slug = await productSlugLedger(firestore, hostId).claim(\n CommerceModel.commerceSlug(`${product.slug}-copy`),\n )\n // Duplicate is a create — rides the quota-enforcing API (AGL-473).\n await createHostResource({\n hostId,\n resource: 'product',\n data: {\n ...copy,\n ...CommerceModel.productSearchFields({\n name: `${product.name} (copy)`,\n variants: product.variants,\n }),\n ...CommerceModel.productStockFields(product),\n // The COPY's smart collections: its name differs from the source's.\n ...(await productCollectionFields(firestore, hostId, {\n ...product,\n name: `${product.name} (copy)`,\n })),\n slug,\n status: 'draft',\n createdAtMs: Date.now(),\n updatedAtMs: Date.now(),\n },\n })\n // A create moves the count and the aggregate is a one-shot — the\n // listener refreshes the ROWS for free, the count has to be asked\n // again or the cap drifts stale for the rest of the session.\n setProductCountEpoch((epoch) => epoch + 1)\n enqueueSnackbar('Product duplicated as draft', {\n variant: 'success',\n persist: false,\n })\n } catch (error: any) {\n enqueueSnackbar(error?.message ?? 'Could not duplicate product', {\n variant: 'warning',\n persist: false,\n })\n }\n },\n [firestore, hostId, createHostResource, enqueueSnackbar, productQuota],\n )\n\n const handleStatus = useCallback(\n (product: ProductRow, status: CommerceModel.ProductStatus) => async () => {\n // A product with a variant nobody has priced (AGL-2916) stays off the\n // storefront until the editor has a price for each.\n if (status === 'active' && CommerceModel.productPriceMissing(product)) {\n return void enqueueSnackbar(\n `Set a price for every variant of ${product.name} before activating it.`,\n { variant: 'info', persist: false },\n )\n }\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(doc(firestore, 'hosts', hostId, 'products', product.$id), {\n status,\n updatedAtMs: Date.now(),\n updatedAt: Timestamp.now(),\n }),\n })\n },\n [firestore, hostId, user, enqueueSnackbar],\n )\n\n const handleDelete = useCallback(\n (product: ProductRow) => async () => {\n const confirmed = await confirm({\n title: 'Delete this product?',\n description:\n `\"${product.name}\" stops being purchasable; blocks referencing ` +\n 'it show a checkout error until repointed.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(doc(firestore, 'hosts', hostId, 'products', product.$id), {\n deletedAt: Timestamp.now(),\n }),\n })\n enqueueSnackbar('Product deleted', { variant: 'success', persist: false })\n },\n [confirm, firestore, hostId, user, enqueueSnackbar],\n )\n\n /*\n * CSV import/export (AGL-282): Shopify-dialect columns, dry-run first.\n *\n * The export writes what the table's filters and search MATCH, across the\n * whole catalog (AGL-3321): it walks the table's own query — the same plan,\n * so the same scope, predicates and order — a page of `EXPORT_PAGE` at a\n * time from a cursor, rather than writing the rows on screen. A walk that\n * reaches `EXPORT_CEILING` stops, asks once whether anything is left, and\n * says so when something is.\n */\n const [exporting, setExporting] = useState(false)\n const handleExport = useCallback(async () => {\n setExporting(true)\n try {\n const productsRef = collection(firestore, 'hosts', hostId, 'products')\n const constraints = listQueryConstraints(plan)\n const exported: ProductRow[] = []\n let cursor: QueryDocumentSnapshot | null = null\n let more = false\n for (;;) {\n const room = EXPORT_CEILING - exported.length\n const snapshot = await getDocs(\n query(\n productsRef,\n ...constraints,\n ...(cursor ? [startAfter(cursor)] : []),\n limit(room > 0 ? Math.min(EXPORT_PAGE, room) : 1),\n ),\n )\n if (room <= 0) {\n more = !snapshot.empty\n break\n }\n for (const product of snapshot.docs) {\n exported.push({\n ...CommerceModel.liftLegacyProduct(product.data() as any),\n $id: product.id,\n })\n }\n if (snapshot.docs.length < Math.min(EXPORT_PAGE, room)) break\n cursor = snapshot.docs[snapshot.docs.length - 1]\n }\n const csv = CommerceModel.productsToCsv(exported)\n const blob = new Blob([csv], { type: 'text/csv' })\n const anchor = document.createElement('a')\n anchor.href = URL.createObjectURL(blob)\n anchor.download = `products-${hostId}.csv`\n anchor.click()\n URL.revokeObjectURL(anchor.href)\n if (more) {\n enqueueSnackbar(\n `Exported the first ${EXPORT_CEILING.toLocaleString()} matching ` +\n 'products — the most one file holds. Filter the list to export the rest.',\n { variant: 'info', persist: false },\n )\n }\n } catch (error: any) {\n enqueueSnackbar(error?.message ?? 'Could not export the products', {\n variant: 'warning',\n persist: false,\n })\n } finally {\n setExporting(false)\n }\n }, [firestore, hostId, plan, enqueueSnackbar])\n\n const handleImportApply = useCallback(async () => {\n const parsed = importing?.parsed\n if (!parsed || parsed.products.length === 0) return\n // Batch-aware cap (AGL-471): the whole import must fit the plan.\n // Not startable until the plan is known (AGL-1064) — the old `org ?`\n // guard let an import begin against an unknown cap and leaned on the\n // API rejecting it partway through, leaving a half-imported catalog.\n if (!planReady) return\n const batchQuota = Aglyn.checkQuota(\n org,\n 'productsPerHost',\n productCount + parsed.products.length - 1,\n )\n if (!batchQuota.allowed) {\n return void enqueueSnackbar(\n `This import needs ${parsed.products.length} product slots — your ` +\n `plan allows ${batchQuota.limit}. See Billing to upgrade.`,\n { variant: 'info', persist: false },\n )\n }\n /*\n * The duplicate-slug check asks the STORE (AGL-3321): every slug the file\n * names, by `slug in […]`, and any suffixed one only when it is needed.\n * It asked the table's rows, which held five hundred products and now\n * hold one page, so a slug past them was handed out a second time.\n */\n const slugs = productSlugLedger(firestore, hostId)\n const created: string[] = []\n try {\n await slugs.ask(parsed.products.map((product) => product.slug))\n // One read of the host's smart collections for the whole file.\n const smartCollections = await readSmartCollections(firestore, hostId)\n // Each create rides the quota-enforcing API (AGL-473); the batch cap\n // above short-circuits before we start, so this loop stays bounded.\n for (const product of parsed.products) {\n const slug = await slugs.claim(product.slug)\n const { id } = await createHostResource({\n hostId,\n resource: 'product',\n data: {\n ...product,\n // Search keys travel with the name on the IMPORT path too — a\n // catalog arrives here in bulk, and the table orders, pages and\n // searches it by these keys alone.\n ...CommerceModel.productSearchFields(product),\n ...CommerceModel.productStockFields(product),\n // Its smart collections (AGL-3321).\n collectionIds: CommerceModel.productCollectionIds(product, smartCollections),\n slug,\n priceUsd: product.variants[0]?.priceUsd ?? 0,\n imageUrl: product.mediaUrls?.[0] ?? null,\n createdAtMs: Date.now(),\n updatedAtMs: Date.now(),\n },\n })\n created.push(id)\n }\n } catch (error: any) {\n return void enqueueSnackbar(error?.message ?? 'Import failed', {\n variant: 'warning',\n persist: false,\n })\n }\n setLastImport({\n key: `${Date.now().toString(36)}-${created.length}`,\n productIds: created,\n options: importOptions,\n })\n setImportOptions({})\n setImporting(null)\n setProductCountEpoch((epoch) => epoch + 1)\n enqueueSnackbar(`Imported ${parsed.products.length} products`, {\n variant: 'success',\n persist: false,\n })\n }, [\n importing,\n firestore,\n hostId,\n createHostResource,\n enqueueSnackbar,\n org,\n planReady,\n productCount,\n importOptions,\n ])\n\n const handleAdjustSave = useCallback(async () => {\n if (!adjusting) return\n const delta = Math.round(Number(adjusting.delta))\n if (!delta) return\n const variants = CommerceModel.adjustVariantInventory(\n adjusting.product,\n adjusting.variantId,\n delta,\n adjusting.locationId || undefined,\n )\n /**\n * Refuse the adjustment when the seed is unconfirmed (AGL-1358).\n *\n * `adjustVariantInventory` reads counts off the seeded product and\n * returns a whole new `variants` array, which is then written over the\n * stored one — so this is a full replace of live stock, not a delta, and\n * `merge` could not help. The refusal also has to cover the ADJUSTMENT\n * LOG, which is why the guard wraps both writes: a logged adjustment\n * whose stock write never happened is worse than neither, because the\n * history then disagrees with the count it is supposed to explain.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'stock',\n unreadable: productsStatus === 'error',\n fromCache: productsFromCache,\n },\n async () => {\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(\n doc(firestore, 'hosts', hostId, 'products', adjusting.product.$id),\n {\n variants,\n // The total and the In stock verdict move with the count (AGL-3321).\n ...CommerceModel.productStockFields({ ...adjusting.product, variants }),\n updatedAtMs: Date.now(),\n },\n ),\n })\n // Adjustment history (AGL-281): the same log the sale webhook writes.\n await addDoc(\n collection(firestore, 'hosts', hostId, 'inventoryAdjustments'),\n {\n productId: adjusting.product.$id,\n variantId: adjusting.variantId,\n delta,\n reason: adjusting.reason,\n ...(adjusting.locationId ? { locationId: adjusting.locationId } : {}),\n atMs: Date.now(),\n } satisfies CommerceModel.InventoryAdjustment,\n )\n },\n )\n // Keep the dialog open with the typed delta rather than failing silently.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n setAdjusting(null)\n enqueueSnackbar('Stock adjusted', { variant: 'success', persist: false })\n }, [\n user,\n adjusting,\n firestore,\n hostId,\n enqueueSnackbar,\n productsFromCache,\n productsStatus,\n ])\n\n /** The hub's allowance check for a batch of `count` new products, for its zone. */\n const roomFor = useCallback(\n (count: number) =>\n planReady\n ? Aglyn.checkQuota(org, 'productsPerHost', productCount + count - 1)\n : null,\n [org, planReady, productCount],\n )\n const onCatalogCreated = useCallback(\n () => setProductCountEpoch((epoch) => epoch + 1),\n [],\n )\n const setImportOption = useCallback(\n (key: string, on: boolean) =>\n setImportOptions((current) => ({ ...current, [key]: on })),\n [],\n )\n\n const formatPrice = (product: ProductRow) => {\n const [min, max] = CommerceModel.productPriceRange(product)\n return min === max ? `$${min}` : `$${min}–$${max}`\n }\n const formatStock = (product: ProductRow) => {\n const total = CommerceModel.productInventory(product)\n if (total == null) return '—'\n return total > 0 ? String(total) : 'Sold out'\n }\n /**\n * RESERVED UNITS, NAMED (AGL-2356).\n *\n * Checkout now takes a hold on the units a live session is about to buy, and\n * that hold deliberately does NOT move `inventory` — the shelf count means\n * units on the shelf, and half the product reads it that way. The\n * consequence a merchant meets is that the storefront can refuse a sale of\n * the third unit while this column says `3`, and without this caption the\n * number is right and the behaviour looks broken.\n *\n * The house style of `stock-movements-card.component.tsx`: a trailing\n * `caption` in `text.secondary`, rendered ONLY when there is something to\n * say. Nothing is held on the overwhelming majority of products, so the\n * column reads exactly as it does today.\n */\n const formatHeld = (product: ProductRow) => {\n const held = CommerceModel.heldProductUnits(product, nowMs)\n return held > 0 ? ` (${held} reserved)` : ''\n }\n\n const productColumns = listFilterGridColumns(\n [\n {\n field: 'name',\n headerName: 'Product',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }: { row: ProductRow }) => (\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {row.name}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`/${row.slug} · id: ${row.$id}`}\n </Typography>\n </Stack>\n ),\n },\n {\n field: 'status',\n headerName: 'Status',\n width: 120,\n renderCell: ({ row }: { row: ProductRow }) => (\n <Chip\n label={row.status}\n size=\"small\"\n color={STATUS_COLOR[row.status] ?? 'default'}\n variant=\"outlined\"\n />\n ),\n },\n { field: 'type', headerName: 'Type', width: 110 },\n {\n field: 'priceUsd',\n headerName: 'Price',\n width: 130,\n sortable: false,\n renderCell: ({ row }: { row: ProductRow }) =>\n CommerceModel.productPriceMissing(row) ? (\n <Chip label=\"Set a price\" size=\"small\" color=\"warning\" variant=\"outlined\" />\n ) : (\n formatPrice(row)\n ),\n },\n {\n field: 'stock',\n headerName: 'Stock',\n width: 150,\n sortable: false,\n renderCell: ({ row }: { row: ProductRow }) => (\n <span>\n {formatStock(row)}\n {formatHeld(row) ? (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"span\">\n {formatHeld(row)}\n </Typography>\n ) : null}\n </span>\n ),\n },\n {\n field: 'variants',\n headerName: 'Variants',\n width: 100,\n sortable: false,\n valueGetter: (_value: unknown, row: ProductRow) => row.variants.length,\n },\n listActionsColumn(\n (product: ProductRow) => (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ justifyContent: 'flex-end', whiteSpace: 'nowrap' }}\n >\n <Button size=\"small\" onClick={() => setEditing(product)}>\n {'Edit'}\n </Button>\n <Button size=\"small\" onClick={handleDuplicate(product)}>\n {'Duplicate'}\n </Button>\n {product.type === 'digital' ? (\n <Button size=\"small\" onClick={() => setKeysFor(product)}>\n {'Keys'}\n </Button>\n ) : null}\n {CommerceModel.productInventory(product) != null ? (\n <Button\n size=\"small\"\n onClick={() =>\n setAdjusting({\n product,\n variantId:\n product.variants.find(\n (variant) => variant.inventory != null,\n )?.id ?? product.variants[0].id,\n delta: '',\n reason: 'restock',\n locationId:\n (locationDocs ?? []).find(\n (location: any) => location.isDefault,\n )?.$id ??\n (locationDocs ?? [])[0]?.$id ??\n '',\n })\n }\n >\n {'Stock'}\n </Button>\n ) : null}\n <Button\n size=\"small\"\n onClick={handleStatus(\n product,\n product.status === 'archived' ? 'active' : 'archived',\n )}\n >\n {product.status === 'archived' ? 'Activate' : 'Archive'}\n </Button>\n <Button size=\"small\" color=\"error\" onClick={handleDelete(product)}>\n {'Delete'}\n </Button>\n </Stack>\n ),\n { width: 470 },\n ),\n ] as GridColDef[],\n PRODUCT_LIST_FIELDS,\n PRODUCT_LIST_OPTIONS,\n PRODUCT_LIST_HEADERS,\n )\n\n return (\n <CardDisplay\n /*\n * The header count is the SITE'S catalog, not the rows in hand.\n *\n * It was `products.length`, which is the filtered, ceilinged, now-paged\n * view — so it read 500 on a 25,000-product catalog and would read 10\n * once the table paged. `productCount` is the same server aggregate the\n * quota gate uses. Under a filter there is no honest number to put here:\n * the aggregate counts the whole catalog and the view counts matches, so\n * neither describes what the reader is looking at, and the footer's own\n * count line answers it instead.\n */\n header={\n filtering\n ? 'Products'\n : `Products${productCount ? ` (${productCount})` : ''}`\n }\n help={pluginDocsHelp('commerce', { anchor: '#products-hub' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n <Box sx={{ flex: 1 }} />\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={!productQuota}\n onClick={() => {\n if (!productQuota) return\n if (!productQuota.allowed) {\n return void enqueueSnackbar(\n `Your plan includes ${productQuota.limit} products — ` +\n 'upgrade for more',\n { variant: 'info', persist: false },\n )\n }\n setCreating(true)\n }}\n >\n {'Add product'}\n </Button>\n <Button\n size=\"small\"\n disabled={!planReady}\n onClick={() => {\n setImportOptions({})\n setImporting({ text: '', parsed: null })\n }}\n >\n {'Import'}\n </Button>\n <Button\n size=\"small\"\n // Nothing matches: there is nothing for the walk to write.\n disabled={exporting || (products.length === 0 && page === 0)}\n onClick={handleExport}\n >\n {exporting ? 'Exporting…' : 'Export'}\n </Button>\n </Stack>\n {/* The cap, standing rather than only on refusal (AGL-2113). The\n count is `productCount` — the site's products, the same number\n the gate above counts — not `products.length`, which is one page\n of the table's query (AGL-1716). */}\n <QuotaReadoutComponent\n ready={productQuota !== null}\n used={productCount}\n limit={productQuota?.limit ?? 0}\n noun=\"product\"\n />\n {/* The zone's widgets are handed the PAGE on screen — the products\n the reader has found and is looking at. Every write the zone makes\n asks the store for what it must not duplicate, never these rows. */}\n <ProductsHubZone\n hostId={hostId}\n products={products}\n roomFor={roomFor}\n lastImport={lastImport}\n onCreated={onCatalogCreated}\n />\n <ListFilterChips\n fields={PRODUCT_LIST_FIELDS}\n headers={PRODUCT_LIST_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={PRODUCT_LIST_OPTIONS}\n />\n <ListQueryNotices\n refused={listQueryRefusals(plan.refused, {\n fields: PRODUCT_LIST_FIELDS,\n headers: PRODUCT_LIST_HEADERS,\n options: PRODUCT_LIST_OPTIONS,\n })}\n notices={plan.notices}\n />\n <ListTable\n aria-label=\"Products\"\n rows={products}\n columns={productColumns}\n loading={productsStatus === 'loading'}\n // One page of the query, turned by the footer below: the grid\n // neither slices, filters nor sorts it.\n hideFooter\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // The one order the list offers, by name: the query's.\n sortingMode=\"server\"\n disableColumnSorting\n initialState={{\n columns: {\n columnVisibilityModel: hiddenFilterVisibility(\n PRODUCT_LIST_FIELDS,\n PRODUCT_VISIBLE_COLUMNS,\n ),\n },\n }}\n noRowsLabel={\n filtering ? 'No products match these filters' : 'No products yet'\n }\n noRowsDescription={\n filtering\n ? undefined\n : 'Build your catalog: add a product, then drop commerce ' +\n 'blocks on any page in Besigner.'\n }\n />\n {products.length === 0 && page === 0 && productsStatus !== 'loading' ? null : (\n /* The count line is the PAGE's. `hasMore` is a fact from the probe\n row; the site's total is `productCount` above, which counts the\n whole catalog rather than the matches, so it is not this. */\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={products.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n </Stack>\n <Dialog\n open={Boolean(keysFor)}\n onClose={() => setKeysFor(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{`License keys — ${keysFor?.name ?? ''}`}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n {(() => {\n // The query already asked for this product's keys, so no second\n // filter here: narrowing a window that was already narrowed is how\n // a reader comes to believe the count covers the pool.\n const productKeys = (keyDocs ?? []).slice(0, KEY_POOL_CEILING)\n const available = productKeys.filter(\n (key: any) => !key.assignedAtMs,\n )\n // RETIRED KEYS ARE COUNTED SEPARATELY (AGL-2454). A refund retires\n // the key rather than returning it to the pool — the buyer already\n // holds the string, so reissuing it would give two people one\n // working key — and without this line the merchant simply watches\n // \"available\" fall with nothing to explain where the key went.\n const retired = productKeys.filter((key: any) => key.revokedAtMs)\n const assigned =\n productKeys.length - available.length - retired.length\n return (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ mt: 1 }}>\n {`${available.length} available · ${assigned} assigned` +\n (retired.length\n ? ` · ${retired.length} retired (refunded or revoked — not reissued)`\n : '') +\n (keyPoolTruncated\n ? `, of the ${KEY_POOL_CEILING} keys read`\n : '') +\n '. Keys deliver automatically on purchase (receipt + account).'}\n </Typography>\n {available.slice(0, 8).map((key: any) => (\n <Stack\n key={key.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\" sx={{ flex: 1, fontFamily: 'monospace' }} noWrap>\n {key.key}\n </Typography>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() =>\n updateDoc(\n doc(firestore, 'hosts', hostId, 'licenseKeys', key.$id),\n { revokedAtMs: Date.now(), assignedAtMs: Date.now() },\n )\n }\n >\n {'Revoke'}\n </Button>\n </Stack>\n ))}\n <TextField\n label=\"Add keys (one per line)\"\n value={keysText}\n onChange={(event) => setKeysText(event.target.value)}\n size=\"small\"\n multiline\n minRows={3}\n />\n </>\n )\n })()}\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setKeysFor(null)}>{'Close'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!keysText.trim()}\n onClick={async () => {\n const keys = keysText\n .split('\\n')\n .map((key) => key.trim())\n .filter(Boolean)\n .slice(0, 200)\n for (const key of keys) {\n await addDoc(\n collection(firestore, 'hosts', hostId, 'licenseKeys'),\n {\n productId: keysFor!.$id,\n key,\n assignedAtMs: null,\n createdAtMs: Date.now(),\n },\n )\n }\n setKeysText('')\n enqueueSnackbar(`Added ${keys.length} keys`, {\n variant: 'success',\n persist: false,\n })\n }}\n >\n {'Add keys'}\n </Button>\n </DialogActions>\n </Dialog>\n <Dialog\n open={Boolean(importing)}\n onClose={() => setImporting(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{'Import products (CSV)'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ mt: 1 }}>\n {'Shopify-compatible columns (Handle, Title, Option/Variant ' +\n 'columns, Image Src). Paste the file contents or choose a file.'}\n </Typography>\n <Button component=\"label\" size=\"small\" sx={{ alignSelf: 'flex-start' }}>\n {'Choose file'}\n <input\n type=\"file\"\n accept=\".csv,text/csv\"\n hidden\n onChange={async (event) => {\n const file = event.target.files?.[0]\n if (!file) return\n const text = await file.text()\n setImporting({ text, parsed: CommerceModel.parseProductsCsv(text) })\n }}\n />\n </Button>\n <TextField\n label=\"CSV\"\n value={importing?.text ?? ''}\n onChange={(event) =>\n setImporting({\n text: event.target.value,\n parsed: event.target.value.trim()\n ? CommerceModel.parseProductsCsv(event.target.value)\n : null,\n })\n }\n size=\"small\"\n multiline\n minRows={5}\n maxRows={10}\n />\n {importing?.parsed ? (\n <>\n <Typography variant=\"body2\">\n {`Ready to import ${importing.parsed.products.length} products` +\n (importing.parsed.errors.length\n ? ` — ${importing.parsed.errors.length} rows skipped:`\n : '')}\n </Typography>\n {importing.parsed.errors.slice(0, 5).map((error) => (\n <Typography\n key={error}\n variant=\"caption\"\n color=\"warning.main\"\n >\n {error}\n </Typography>\n ))}\n </>\n ) : null}\n {WidgetSlot && importing?.parsed?.products.length ? (\n <WidgetSlot\n slot={PRODUCT_IMPORT_ZONE.id}\n hostId={hostId}\n orgId={undefined}\n count={importing.parsed.products.length}\n options={importOptions}\n setOption={setImportOption}\n />\n ) : null}\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setImporting(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!importing?.parsed?.products.length}\n onClick={handleImportApply}\n >\n {`Import${importing?.parsed?.products.length ? ` ${importing.parsed.products.length}` : ''}`}\n </Button>\n </DialogActions>\n </Dialog>\n <Dialog\n open={Boolean(adjusting)}\n onClose={() => setAdjusting(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{`Adjust stock — ${adjusting?.product.name ?? ''}`}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Variant\"\n value={adjusting?.variantId ?? ''}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev ? { ...prev, variantId: event.target.value } : prev,\n )\n }\n size=\"small\"\n select\n sx={{ mt: 1 }}\n >\n {(adjusting?.product.variants ?? [])\n .filter((variant) => variant.inventory != null)\n .map((variant) => (\n <MenuItem key={variant.id} value={variant.id}>\n {`${Object.values(variant.options ?? {}).join(' / ') || 'Default'} — ${variant.inventory} in stock${\n CommerceModel.heldVariantUnits(\n adjusting?.product,\n variant.id,\n nowMs,\n ) > 0\n ? `, ${CommerceModel.heldVariantUnits(\n adjusting?.product,\n variant.id,\n nowMs,\n )} reserved in checkout`\n : ''\n }`}\n </MenuItem>\n ))}\n </TextField>\n {(locationDocs?.length ?? 0) > 1 ? (\n <TextField\n label=\"Location\"\n value={adjusting?.locationId ?? ''}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev ? { ...prev, locationId: event.target.value } : prev,\n )\n }\n size=\"small\"\n select\n >\n {(locationDocs ?? []).map((location: any) => (\n <MenuItem key={location.$id} value={location.$id}>\n {location.name}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <TextField\n label=\"Change\"\n placeholder=\"+10 or -3\"\n value={adjusting?.delta ?? ''}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev\n ? {\n ...prev,\n delta: event.target.value.replace(/[^0-9+-]/g, ''),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Reason\"\n value={adjusting?.reason ?? 'restock'}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev\n ? {\n ...prev,\n reason: event.target\n .value as CommerceModel.InventoryAdjustmentReason,\n }\n : prev,\n )\n }\n size=\"small\"\n select\n >\n <MenuItem value=\"restock\">{'Restock'}</MenuItem>\n <MenuItem value=\"correction\">{'Correction'}</MenuItem>\n <MenuItem value=\"damage\">{'Damaged'}</MenuItem>\n <MenuItem value=\"refund\">{'Refund return'}</MenuItem>\n </TextField>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setAdjusting(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!Math.round(Number(adjusting?.delta))}\n onClick={handleAdjustSave}\n >\n {'Apply'}\n </Button>\n </DialogActions>\n </Dialog>\n <ProductEditorDialog\n key={editing?.$id ?? (creating ? 'new' : 'closed')}\n hostId={hostId}\n product={editing}\n // The editor is seeded from a row of THIS listener and replaces the\n // whole document, so the freshness verdict belongs here (AGL-1358) —\n // the dialog has no listener of its own to ask.\n seedFromCache={productsFromCache}\n seedUnreadable={productsStatus === 'error'}\n open={creating || editing !== null}\n onClose={() => {\n setEditing(null)\n setCreating(false)\n // The dialog reports no verdict, so a create and a cancel look\n // the same from here. Re-reading on both is one aggregate and\n // keeps the cap off the dialog's shoulders (AGL-1716).\n setProductCountEpoch((epoch) => epoch + 1)\n }}\n />\n </CardDisplay>\n )\n}\nProductsHubCard.displayName = 'ProductsHubCard'\n\nexport default ProductsHubCard\n"],"names":["Aglyn","CommerceModel","PRODUCT_LIST_BASE","PRODUCT_LIST_FIELDS","PRODUCT_LIST_HEADERS","PRODUCT_LIST_OPTIONS","PRODUCT_LIST_QUERY","PRODUCT_LIST_SELECT_FIELDS","productListRequestClauses","CardDisplay","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","listActionsColumn","QuotaReadoutComponent","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","useSnackbar","Timestamp","Box","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","TextField","Typography","addDoc","collection","doc","documentId","getCountFromServer","getDocs","limit","orderBy","query","startAfter","updateDoc","where","useCallback","useEffect","useMemo","useState","useFirestore","useUser","writeSiteWideChange","useFirestoreCollection","writeGuardedBySeed","listQueryConstraints","useListQuery","useHostResourceApi","useOrgPlan","ProductEditorDialog","productSlugLedger","productCollectionFields","readSmartCollections","ProductsHubZone","pluginDocsHelp","useConsoleWidgetSlot","PRODUCT_IMPORT_ZONE","EXPORT_PAGE","EXPORT_CEILING","KEY_POOL_CEILING","STATUS_COLOR","active","draft","archived","PRODUCT_VISIBLE_COLUMNS","ProductsHubCard","props","importing","hostId","firestore","data","user","enqueueSnackbar","createHostResource","org","ready","planReady","confirm","gridFilter","selectFields","filtering","clauses","length","searchWords","some","word","trim","editing","setEditing","creating","setCreating","adjusting","setAdjusting","setImporting","keysFor","setKeysFor","keysText","setKeysText","importOptions","setImportOptions","lastImport","setLastImport","WidgetSlot","rows","productDocs","productWindow","hasMore","page","setPage","pageSize","setPageSize","plan","status","productsStatus","fromCache","productsFromCache","declaration","request","search","base","deps","idField","confirmDisappearances","keyDocs","$id","keyPoolTruncated","locationDocs","products","map","product","liftLegacyProduct","nowMs","setNowMs","Date","now","anyHeld","heldProductUnits","undefined","timer","setInterval","clearInterval","productCountEpoch","setProductCountEpoch","serverProductCount","setServerProductCount","then","snapshot","count","catch","loadedProductCount","productCount","productQuota","checkQuota","handleDuplicate","allowed","variant","persist","_sourceId","skus","_skus","barcodes","_barcodes","copy","slug","claim","commerceSlug","resource","productSearchFields","name","variants","productStockFields","createdAtMs","updatedAtMs","epoch","error","message","handleStatus","productPriceMissing","write","batch","update","updatedAt","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","deletedAt","exporting","setExporting","handleExport","productsRef","constraints","exported","cursor","more","room","Math","min","empty","docs","push","id","csv","productsToCsv","blob","Blob","type","anchor","document","createElement","href","URL","createObjectURL","download","click","revokeObjectURL","toLocaleString","handleImportApply","parsed","batchQuota","slugs","created","ask","smartCollections","collectionIds","productCollectionIds","priceUsd","imageUrl","mediaUrls","key","toString","productIds","options","handleAdjustSave","delta","round","Number","adjustVariantInventory","variantId","locationId","verdict","subject","unreadable","productId","reason","atMs","ok","roomFor","onCatalogCreated","setImportOption","on","current","formatPrice","max","productPriceRange","formatStock","total","productInventory","String","formatHeld","held","productColumns","field","headerName","flex","minWidth","renderCell","row","sx","noWrap","width","label","size","sortable","span","component","valueGetter","_value","direction","spacing","justifyContent","whiteSpace","onClick","find","inventory","location","isDefault","header","help","contentGutterX","contentGutterY","xs","sm","disabled","text","used","noun","onCreated","fields","headers","onChange","setClauses","refused","notices","aria-label","columns","loading","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","sortingMode","disableColumnSorting","initialState","columnVisibilityModel","noRowsLabel","noRowsDescription","rowCount","onPageChange","onPageSizeChange","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","productKeys","slice","available","filter","assignedAtMs","retired","revokedAtMs","assigned","mt","alignItems","fontFamily","value","event","target","multiline","minRows","keys","split","alignSelf","input","accept","hidden","file","files","parseProductsCsv","maxRows","errors","slot","orgId","setOption","prev","select","Object","values","join","heldVariantUnits","placeholder","replace","seedFromCache","seedUnreadable","displayName"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { ConsoleProductsHubZoneProps } from '@aglyn/aglyn/plugin-manager/feature-plugins';
1
+ import { type ConsoleProductsHubZoneProps } from './product-zones';
2
2
  import * as CommerceModel from '../../model';
3
3
  /**
4
4
  * The products hub's zone (AGL-2916): the page of catalog rows the hub's table
@@ -18,6 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx } from "react/jsx-runtime";
19
19
  import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
20
20
  import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
21
+ import { PRODUCTS_HUB_ZONE } from "./product-zones.js";
21
22
  import { Timestamp } from "@aglyn/shared-util-timestamp";
22
23
  import { useFirestore, useHostResourceApi, useUser } from "@aglyn/tenant-feature-instance";
23
24
  import { commitWithSiteWideEntry, settleSiteWideChange } from "@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change";
@@ -217,7 +218,7 @@ export function ProductsHubZone(props) {
217
218
  ]);
218
219
  if (!Slot) return null;
219
220
  return /*#__PURE__*/ _jsx(Slot, {
220
- slot: "productsHub",
221
+ slot: PRODUCTS_HUB_ZONE.id,
221
222
  hostId: hostId,
222
223
  orgId: undefined,
223
224
  products: summaries,
@@ -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 type {\n ConsoleProductCopyValues,\n ConsoleProductSummary,\n ConsoleProductsHubZoneProps,\n ConsoleProposedDiscount,\n ConsoleProposedProduct,\n} from '@aglyn/aglyn/plugin-manager/feature-plugins'\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 with no price (`unpricedProductDraft`).\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.unpricedProductDraft(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=\"productsHub\"\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","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","unpricedProductDraft","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 with no price (`unpricedProductDraft`).\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.unpricedProductDraft(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","unpricedProductDraft","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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"}
@@ -164,7 +164,7 @@ import { writeSiteWideChange } from "@aglyn/tenant-feature-instance/hooks/helper
164
164
  }),
165
165
  size: "small",
166
166
  select: true,
167
- helperText: 'Screen rendered at /products/{slug} — design it in the ' + 'besigner with the Product detail block and {{product.*}} tokens.',
167
+ helperText: 'Page rendered at /products/{slug} — design it in the ' + 'Besigner with the Product detail block and {{product.*}} tokens.',
168
168
  children: [
169
169
  /*#__PURE__*/ _jsx(MenuItem, {
170
170
  value: "",
@@ -184,7 +184,7 @@ import { writeSiteWideChange } from "@aglyn/tenant-feature-instance/hooks/helper
184
184
  }),
185
185
  size: "small",
186
186
  select: true,
187
- helperText: 'Screen rendered at /collections/{slug} — drop a Product grid ' + 'with source "A collection" and {{collection.*}} tokens.',
187
+ helperText: 'Page rendered at /collections/{slug} — drop a Product grid ' + 'with source "A collection" and {{collection.*}} tokens.',
188
188
  children: [
189
189
  /*#__PURE__*/ _jsx(MenuItem, {
190
190
  value: "",