@aglyn/plugins-crm 1.0.0-beta.269 → 1.0.0-beta.270
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/deal-products-card.d.ts +3 -2
- package/src/lib/components/deal-products-card.js +20 -4
- package/src/lib/components/deal-products-card.js.map +1 -1
- package/src/lib/declarations.console-server.js +10 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/native-nav.d.ts +19 -0
- package/src/lib/native-nav.js +95 -0
- package/src/lib/native-nav.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-crm",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.270",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.270",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.270",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.270",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.270",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.270",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.270",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.270",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.270",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.270",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.270",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.270",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.270",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.270",
|
|
41
41
|
"@dnd-kit/core": "^6.3.1",
|
|
42
42
|
"@dnd-kit/utilities": "^3.2.2",
|
|
43
43
|
"@swc/helpers": "0.5.23",
|
|
@@ -35,8 +35,9 @@ export interface DealProductsCardProps {
|
|
|
35
35
|
* variant priced. The catalog is USD; a deal in another currency is told
|
|
36
36
|
* so and can edit the unit amount after. **By hand** takes a name and a
|
|
37
37
|
* price with no product behind them, which is also all a plan without
|
|
38
|
-
* commerce gets: the catalog door is not drawn for it,
|
|
39
|
-
*
|
|
38
|
+
* commerce gets: the catalog door is not drawn for it, and the dialog says
|
|
39
|
+
* so with a link to Billing (AGL-3982), since a plan with the store is the
|
|
40
|
+
* one step that opens it.
|
|
40
41
|
*
|
|
41
42
|
* Adding opens a dialog rather than a row of fields above the table, and
|
|
42
43
|
* a quantity is edited in place and written on blur.
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
18
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
19
|
import { createElement as _createElement } from "react";
|
|
20
|
-
import { checkEntitlement, CRM_COLLECTIONS, DEAL_LINE_ITEM_NAME_MAX, DEAL_LINE_ITEM_QUANTITY_MAX, DEAL_LINE_ITEMS_MAX, lineItemsTotalCents, pluginDocsHelp, readDealLineItems } from "@aglyn/aglyn";
|
|
20
|
+
import { buildRoute, checkEntitlement, CRM_COLLECTIONS, DEAL_LINE_ITEM_NAME_MAX, DEAL_LINE_ITEM_QUANTITY_MAX, DEAL_LINE_ITEMS_MAX, lineItemsTotalCents, pluginDocsHelp, readDealLineItems, Route } from "@aglyn/aglyn";
|
|
21
21
|
import { mdiDeleteOutline, mdiPlus } from "@aglyn/shared-data-mdi";
|
|
22
22
|
import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
23
|
+
import { PrerequisiteNotice } from "@aglyn/shared-ui-jsx/components/prerequisite-notice.component";
|
|
23
24
|
import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
|
|
24
25
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
25
26
|
import { useFirestore, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
|
|
@@ -50,8 +51,9 @@ import { amountInputValue, DEFAULT_DEAL_CURRENCY, formatMoney, parseAmountInput
|
|
|
50
51
|
* variant priced. The catalog is USD; a deal in another currency is told
|
|
51
52
|
* so and can edit the unit amount after. **By hand** takes a name and a
|
|
52
53
|
* price with no product behind them, which is also all a plan without
|
|
53
|
-
* commerce gets: the catalog door is not drawn for it,
|
|
54
|
-
*
|
|
54
|
+
* commerce gets: the catalog door is not drawn for it, and the dialog says
|
|
55
|
+
* so with a link to Billing (AGL-3982), since a plan with the store is the
|
|
56
|
+
* one step that opens it.
|
|
55
57
|
*
|
|
56
58
|
* Adding opens a dialog rather than a row of fields above the table, and
|
|
57
59
|
* a quantity is edited in place and written on blur.
|
|
@@ -70,6 +72,13 @@ import { amountInputValue, DEFAULT_DEAL_CURRENCY, formatMoney, parseAmountInput
|
|
|
70
72
|
deal.lineItems
|
|
71
73
|
]);
|
|
72
74
|
const hasCommerce = checkEntitlement(org != null ? org : null, 'commerce');
|
|
75
|
+
// Where a plan with the store is chosen. The org doc carries its own slug,
|
|
76
|
+
// so the link costs no read; without one the notice still names the step.
|
|
77
|
+
const slug = org == null ? void 0 : org['slug'];
|
|
78
|
+
const orgSlug = typeof slug === 'string' ? slug : '';
|
|
79
|
+
const billingHref = orgSlug ? `${buildRoute(Route.MANAGE_BILLING, {
|
|
80
|
+
orgSlug
|
|
81
|
+
})}#plans` : undefined;
|
|
73
82
|
const [busy, setBusy] = useState(false);
|
|
74
83
|
const [adding, setAdding] = useState(false);
|
|
75
84
|
/** The one write: the lines and their sum, together. */ const save = useCallback(async (next, done)=>{
|
|
@@ -308,6 +317,8 @@ import { amountInputValue, DEFAULT_DEAL_CURRENCY, formatMoney, parseAmountInput
|
|
|
308
317
|
hostId: catalogHostId,
|
|
309
318
|
currency: currency,
|
|
310
319
|
catalog: hasCommerce && Boolean(catalogHostId),
|
|
320
|
+
catalogNeedsPlan: !hasCommerce && Boolean(catalogHostId),
|
|
321
|
+
billingHref: billingHref,
|
|
311
322
|
busy: busy,
|
|
312
323
|
onAdd: handleAdd
|
|
313
324
|
})
|
|
@@ -360,7 +371,7 @@ DealProductsCard.displayName = 'DealProductsCard';
|
|
|
360
371
|
}
|
|
361
372
|
QuantityField.displayName = 'QuantityField';
|
|
362
373
|
function AddLineDialog(props) {
|
|
363
|
-
const { open, onClose, hostId, currency, catalog, busy, onAdd } = props;
|
|
374
|
+
const { open, onClose, hostId, currency, catalog, catalogNeedsPlan, billingHref, busy, onAdd } = props;
|
|
364
375
|
const firestore = useFirestore();
|
|
365
376
|
const [door, setDoor] = useState(catalog ? 'catalog' : 'manual');
|
|
366
377
|
const [picked, setPicked] = useState(null);
|
|
@@ -478,6 +489,11 @@ function AddLineDialog(props) {
|
|
|
478
489
|
pt: 1
|
|
479
490
|
},
|
|
480
491
|
children: [
|
|
492
|
+
catalogNeedsPlan ? /*#__PURE__*/ _jsx(PrerequisiteNotice, {
|
|
493
|
+
href: billingHref,
|
|
494
|
+
actionLabel: "See plans",
|
|
495
|
+
children: 'Adding lines from your product catalog needs a plan with the store. Add this line by hand, or choose a plan with the store in Billing.'
|
|
496
|
+
}) : null,
|
|
481
497
|
catalog ? /*#__PURE__*/ _jsxs(ToggleButtonGroup, {
|
|
482
498
|
exclusive: true,
|
|
483
499
|
size: "small",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-products-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n checkEntitlement,\n CRM_COLLECTIONS,\n type CrmDealLineItem,\n DEAL_LINE_ITEM_NAME_MAX,\n DEAL_LINE_ITEM_QUANTITY_MAX,\n DEAL_LINE_ITEMS_MAX,\n lineItemsTotalCents,\n pluginDocsHelp,\n readDealLineItems,\n} from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Autocomplete,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n IconButton,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { doc, getDocs, updateDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { pluginRecordListSource } from '@aglyn/aglyn/plugin-manager/plugin-record-lists'\nimport type { CrmOrgDoc } from '../hooks/use-crm-scope'\nimport {\n amountInputValue,\n type DealDoc,\n DEFAULT_DEAL_CURRENCY,\n formatMoney,\n parseAmountInput,\n} from '../model/deal-board-model'\n\n/** Catalog matches offered per keystroke; a longer list is a scroll, not a pick. */\nconst CATALOG_MATCHES = 8\n\n/** One thing the catalog search offers: a product, or one variant of it. */\ninterface CatalogChoice {\n key: string\n productId: string\n label: string\n unitAmountCents: number\n}\n\nexport interface DealProductsCardProps {\n deal: DealDoc\n orgId: string\n /**\n * The site whose catalog the search reads — the console's own, or\n * `null` at the organization level (AGL-2630), where the deal's own\n * capturing site is searched; a deal with none takes lines by hand only.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The listener's verdict on the deal, for the stale-seed guard. */\n fromCache: boolean\n unreadable: boolean\n}\n\n/**\n * The products behind a deal's amount (AGL-2620).\n *\n * ## The amount follows the lines\n *\n * Every write here stores `lineItems` AND `amountCents` as their sum, in\n * one update, so the board, the reports and the API keep reading the one\n * field they always read and no reader adds up lines. Once a deal has a\n * line, the drawer's Amount goes read-only and says why; remove the last\n * line and the amount is typed again, keeping the last sum rather than\n * dropping to nothing. The currency is the deal's — a line cannot be in\n * another, because their sum is one number.\n *\n * ## Two doors to a line\n *\n * **From the catalog** searches THIS site's products — a prefix on\n * `nameTokens` over active products, the query the products hub runs and\n * the `(status, nameTokens, nameLower)` index serves — and offers each\n * variant priced. The catalog is USD; a deal in another currency is told\n * so and can edit the unit amount after. **By hand** takes a name and a\n * price with no product behind them, which is also all a plan without\n * commerce gets: the catalog door is not drawn for it, because the site\n * has no catalog to search.\n *\n * Adding opens a dialog rather than a row of fields above the table, and\n * a quantity is edited in place and written on blur.\n */\nexport function DealProductsCard(props: DealProductsCardProps) {\n const { deal, orgId, hostId, org, fromCache, unreadable } = props\n const firestore = useFirestore()\n // The catalog searched: the mounted site's, or the deal's own.\n const catalogHostId = hostId ?? deal.hostId ?? null\n const { enqueueSnackbar } = useSnackbar()\n const currency = String(deal.currency || DEFAULT_DEAL_CURRENCY).toLowerCase()\n const items = useMemo(() => deal.lineItems ?? [], [deal.lineItems])\n const hasCommerce = checkEntitlement(\n (org ?? null) as Partial<AglynOrgBilling> | null,\n 'commerce',\n )\n const [busy, setBusy] = useState(false)\n const [adding, setAdding] = useState(false)\n\n /** The one write: the lines and their sum, together. */\n const save = useCallback(\n async (next: CrmDealLineItem[], done: string) => {\n const read = readDealLineItems(next, currency)\n if ('error' in read) {\n enqueueSnackbar(read.error, { variant: 'warning', persist: false })\n return false\n }\n setBusy(true)\n try {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id), {\n lineItems: read.items,\n // The last sum stays on a deal whose lines were all removed:\n // the forecast should not drop to zero because a quote was\n // cleared to be retyped.\n ...(read.items.length ? { amountCents: lineItemsTotalCents(read.items) } : {}),\n updatedAt: new Date(),\n })\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return false\n }\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n return false\n } finally {\n setBusy(false)\n }\n },\n [currency, enqueueSnackbar, unreadable, fromCache, firestore, orgId, deal.$id],\n )\n\n const handleRemove = useCallback(\n (index: number) =>\n void save(\n items.filter((_item, at) => at !== index),\n 'Line removed',\n ),\n [items, save],\n )\n const handleQuantity = useCallback(\n (index: number, quantity: number) => {\n if (quantity === items[index]?.quantity) return\n void save(\n items.map((item, at) => (at === index ? { ...item, quantity } : item)),\n 'Quantity saved',\n )\n },\n [items, save],\n )\n const handleAdd = useCallback(\n async (line: CrmDealLineItem) => {\n const ok = await save([...items, line], 'Line added')\n if (ok) setAdding(false)\n },\n [items, save],\n )\n\n const total = lineItemsTotalCents(items)\n const full = items.length >= DEAL_LINE_ITEMS_MAX\n\n return (\n <>\n <CardDisplay\n header={'Products'}\n help={pluginDocsHelp('deals', { anchor: '#line-items' })}\n HeaderProps={{\n action: (\n <Tooltip title={full ? `A deal carries at most ${DEAL_LINE_ITEMS_MAX} lines` : ''}>\n <span>\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={busy || full}\n onClick={() => setAdding(true)}\n >\n {'Add line'}\n </Button>\n </span>\n </Tooltip>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n {items.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {hasCommerce\n ? 'No line items yet. Add products from your catalog or by hand; the amount becomes their sum.'\n : 'No line items yet. Add what the deal is for, priced by hand; the amount becomes their sum.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\" aria-label=\"Line items\">\n <TableHead>\n <TableRow>\n <TableCell>{'Item'}</TableCell>\n <TableCell align=\"right\" sx={{ width: 110 }}>\n {'Quantity'}\n </TableCell>\n <TableCell align=\"right\">{'Unit'}</TableCell>\n <TableCell align=\"right\">{'Total'}</TableCell>\n <TableCell sx={{ width: 48 }} />\n </TableRow>\n </TableHead>\n <TableBody>\n {items.map((item, index) => (\n <TableRow key={`${index}:${item.productId ?? item.name}`}>\n <TableCell>\n <Typography variant=\"body2\">{item.name}</Typography>\n {item.productId ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'From the catalog'}\n </Typography>\n ) : null}\n </TableCell>\n <TableCell align=\"right\">\n <QuantityField\n value={item.quantity}\n disabled={busy}\n label={`Quantity of ${item.name}`}\n onCommit={(quantity) => handleQuantity(index, quantity)}\n />\n </TableCell>\n <TableCell align=\"right\">{formatMoney(item.unitAmountCents, currency)}</TableCell>\n <TableCell align=\"right\">\n {formatMoney(item.quantity * item.unitAmountCents, currency)}\n </TableCell>\n <TableCell padding=\"none\" align=\"right\">\n <Tooltip title=\"Remove line\">\n <span>\n <IconButton\n size=\"small\"\n color=\"error\"\n disabled={busy}\n aria-label={`Remove ${item.name}`}\n onClick={() => handleRemove(index)}\n >\n <MdiIcon path={mdiDeleteOutline.path} size={0.8} />\n </IconButton>\n </span>\n </Tooltip>\n </TableCell>\n </TableRow>\n ))}\n <TableRow>\n <TableCell colSpan={3}>\n <Typography variant=\"subtitle2\">{'Amount'}</Typography>\n </TableCell>\n <TableCell align=\"right\">\n <Typography variant=\"subtitle2\" data-testid=\"line-items-total\">\n {formatMoney(total, currency)}\n </Typography>\n </TableCell>\n <TableCell padding=\"none\" />\n </TableRow>\n </TableBody>\n </ScrollTable>\n )}\n </CardDisplay>\n <AddLineDialog\n open={adding}\n onClose={() => setAdding(false)}\n hostId={catalogHostId}\n currency={currency}\n catalog={hasCommerce && Boolean(catalogHostId)}\n busy={busy}\n onAdd={handleAdd}\n />\n </>\n )\n}\nDealProductsCard.displayName = 'DealProductsCard'\n\ninterface QuantityFieldProps {\n value: number\n disabled: boolean\n label: string\n onCommit: (quantity: number) => void\n}\n\n/** A quantity edited in place, committed on blur or Enter, refused below one. */\nfunction QuantityField(props: QuantityFieldProps) {\n const { value, disabled, label, onCommit } = props\n const [draft, setDraft] = useState(String(value))\n useEffect(() => setDraft(String(value)), [value])\n const commit = () => {\n const quantity = Number(draft)\n if (Number.isInteger(quantity) && quantity >= 1 && quantity <= DEAL_LINE_ITEM_QUANTITY_MAX) {\n onCommit(quantity)\n } else {\n setDraft(String(value))\n }\n }\n return (\n <TextField\n size=\"small\"\n type=\"number\"\n value={draft}\n disabled={disabled}\n onChange={(event) => setDraft(event.target.value)}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n ;(event.target as HTMLInputElement).blur()\n }\n }}\n sx={{ width: 88 }}\n slotProps={{\n htmlInput: {\n min: 1,\n max: DEAL_LINE_ITEM_QUANTITY_MAX,\n step: 1,\n 'aria-label': label,\n style: { textAlign: 'right' },\n },\n }}\n />\n )\n}\nQuantityField.displayName = 'QuantityField'\n\ninterface AddLineDialogProps {\n open: boolean\n onClose: () => void\n /** The site whose catalog is searched; `null` draws no catalog door. */\n hostId: string | null\n currency: string\n /** Whether the catalog door is drawn — the plan has commerce. */\n catalog: boolean\n busy: boolean\n onAdd: (line: CrmDealLineItem) => Promise<void>\n}\n\ntype Door = 'catalog' | 'manual'\n\nfunction AddLineDialog(props: AddLineDialogProps) {\n const { open, onClose, hostId, currency, catalog, busy, onAdd } = props\n const firestore = useFirestore()\n const [door, setDoor] = useState<Door>(catalog ? 'catalog' : 'manual')\n const [picked, setPicked] = useState<CatalogChoice | null>(null)\n const [name, setName] = useState('')\n const [quantity, setQuantity] = useState('1')\n const [unit, setUnit] = useState('')\n const [search, setSearch] = useState('')\n const [choices, setChoices] = useState<CatalogChoice[]>([])\n\n useEffect(() => {\n if (!open) return\n setDoor(catalog ? 'catalog' : 'manual')\n setPicked(null)\n setName('')\n setQuantity('1')\n setUnit('')\n setSearch('')\n setChoices([])\n }, [open, catalog])\n\n /*\n * THE CATALOG SEARCH: what is typed, matched the way the plugin that keeps\n * products matches it — through the source it publishes (AGL-3080), which\n * offers active products only — debounced so a name typed at speed is one\n * query rather than one per letter. Nothing typed, or no plugin keeping\n * products, matches nothing. Each variant is its own choice, priced from\n * its `priceUsd` in cents.\n */\n useEffect(() => {\n if (!open || door !== 'catalog' || !hostId) return\n const products = pluginRecordListSource('product')?.source ?? null\n const catalogQuery = products?.query(firestore, { hostId, search, limit: CATALOG_MATCHES }) ?? null\n if (!products || !catalogQuery) {\n setChoices([])\n return\n }\n let active = true\n const timer = setTimeout(() => {\n void getDocs(catalogQuery)\n .then((snapshot) => {\n if (!active) return\n const next: CatalogChoice[] = []\n for (const entry of snapshot.docs) {\n const product = products.record(entry.id, entry.data())\n if (!product) continue\n const variants = Array.isArray(product.facts['variants'])\n ? (product.facts['variants'] as Array<Record<string, unknown>>)\n : []\n const priced = variants.length\n ? variants\n : [{ id: '', options: {}, priceUsd: product.facts['priceUsd'] }]\n for (const variant of priced) {\n const options = Object.values((variant['options'] as Record<string, string>) ?? {})\n const label = options.length ? `${product.name} — ${options.join(' / ')}` : product.name\n next.push({\n key: `${product.id}:${String(variant['id'] ?? '')}`,\n productId: product.id,\n label,\n unitAmountCents: Math.max(0, Math.round((Number(variant['priceUsd']) || 0) * 100)),\n })\n }\n }\n setChoices(next)\n })\n .catch(() => {\n if (active) setChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, door, hostId, search])\n\n const unitCents = parseAmountInput(unit)\n const quantityNumber = Number(quantity)\n const lineName = (door === 'catalog' ? (name || picked?.label || '') : name).trim()\n const problem = !lineName\n ? door === 'catalog'\n ? 'Pick a product.'\n : 'A line needs a name.'\n : !Number.isInteger(quantityNumber) || quantityNumber < 1\n ? 'The quantity has to be a whole number of one or more.'\n : unitCents === null\n ? 'The unit amount has to be a number of zero or more.'\n : null\n\n const handleAdd = () => {\n if (problem || unitCents === null) return\n void onAdd({\n ...(door === 'catalog' && picked ? { productId: picked.productId } : {}),\n name: lineName.slice(0, DEAL_LINE_ITEM_NAME_MAX),\n quantity: quantityNumber,\n unitAmountCents: unitCents,\n currency,\n })\n }\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} fullWidth maxWidth=\"xs\">\n <DialogTitle>{'Add a line'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n {catalog ? (\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={door}\n onChange={(_event, next) => {\n if (next) setDoor(next as Door)\n }}\n aria-label=\"Where the line comes from\"\n >\n <ToggleButton value=\"catalog\">{'From the catalog'}</ToggleButton>\n <ToggleButton value=\"manual\">{'By hand'}</ToggleButton>\n </ToggleButtonGroup>\n ) : null}\n {door === 'catalog' ? (\n <Autocomplete<CatalogChoice, false, false, false>\n options={choices}\n value={picked}\n inputValue={search}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setSearch(next)\n }}\n onChange={(_event, choice) => {\n setPicked(choice)\n setName(choice?.label ?? '')\n setUnit(choice ? amountInputValue(choice.unitAmountCents) : '')\n }}\n getOptionLabel={(choice) => choice.label}\n isOptionEqualToValue={(option, value) => option.key === value.key}\n // The match is the query's own prefix over `nameTokens`, never\n // MUI's substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.key}>\n <Stack direction=\"row\" spacing={1} sx={{ width: '100%', alignItems: 'baseline' }}>\n <Typography variant=\"body2\" sx={{ flex: 1 }}>\n {choice.label}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {formatMoney(choice.unitAmountCents, 'usd')}\n </Typography>\n </Stack>\n </li>\n )}\n noOptionsText={search.trim() ? 'No active product matches' : 'Type a product name'}\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Product\"\n placeholder=\"Search this site's catalog\"\n autoFocus\n helperText={\n currency !== 'usd'\n ? `Catalog prices are in USD; this deal is in ${currency.toUpperCase()}. Check the unit amount.`\n : undefined\n }\n />\n )}\n />\n ) : (\n <TextField\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_LINE_ITEM_NAME_MAX } }}\n />\n )}\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Quantity\"\n type=\"number\"\n value={quantity}\n onChange={(event) => setQuantity(event.target.value)}\n sx={{ width: 120 }}\n slotProps={{ htmlInput: { min: 1, max: DEAL_LINE_ITEM_QUANTITY_MAX, step: 1 } }}\n />\n <TextField\n label={`Unit amount (${currency.toUpperCase()})`}\n value={unit}\n onChange={(event) => setUnit(event.target.value)}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n </Stack>\n {problem && (lineName || unit) ? (\n <Typography variant=\"caption\" color=\"warning.main\">\n {problem}\n </Typography>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" disabled={busy || Boolean(problem)} onClick={handleAdd}>\n {'Add line'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nAddLineDialog.displayName = 'AddLineDialog'\n\nexport default DealProductsCard\n"],"names":["checkEntitlement","CRM_COLLECTIONS","DEAL_LINE_ITEM_NAME_MAX","DEAL_LINE_ITEM_QUANTITY_MAX","DEAL_LINE_ITEMS_MAX","lineItemsTotalCents","pluginDocsHelp","readDealLineItems","mdiDeleteOutline","mdiPlus","CardDisplay","MdiIcon","ScrollTable","useSnackbar","useFirestore","writeGuardedBySeed","Autocomplete","Button","Dialog","DialogActions","DialogContent","DialogTitle","IconButton","Stack","TableBody","TableCell","TableHead","TableRow","TextField","ToggleButton","ToggleButtonGroup","Tooltip","Typography","doc","getDocs","updateDoc","useCallback","useEffect","useMemo","useState","pluginRecordListSource","amountInputValue","DEFAULT_DEAL_CURRENCY","formatMoney","parseAmountInput","CATALOG_MATCHES","DealProductsCard","props","hostId","deal","orgId","org","fromCache","unreadable","firestore","catalogHostId","enqueueSnackbar","currency","String","toLowerCase","items","lineItems","hasCommerce","busy","setBusy","adding","setAdding","save","next","done","read","error","variant","persist","verdict","subject","deals","$id","length","amountCents","updatedAt","Date","ok","message","console","allowDuplicate","handleRemove","index","filter","_item","at","handleQuantity","quantity","map","item","handleAdd","line","total","full","header","help","anchor","HeaderProps","action","title","span","size","startIcon","path","disabled","onClick","contentGutterX","contentGutterY","color","aria-label","align","sx","width","name","productId","QuantityField","value","label","onCommit","unitAmountCents","padding","colSpan","data-testid","AddLineDialog","open","onClose","catalog","Boolean","onAdd","displayName","draft","setDraft","commit","Number","isInteger","type","onChange","event","target","onBlur","onKeyDown","key","preventDefault","blur","slotProps","htmlInput","min","max","step","style","textAlign","door","setDoor","picked","setPicked","setName","setQuantity","unit","setUnit","search","setSearch","choices","setChoices","products","source","catalogQuery","query","limit","active","timer","setTimeout","then","snapshot","entry","docs","product","record","id","data","variants","Array","isArray","facts","priced","options","priceUsd","Object","values","join","push","Math","round","catch","clearTimeout","unitCents","quantityNumber","lineName","trim","problem","slice","undefined","fullWidth","maxWidth","spacing","pt","exclusive","_event","inputValue","onInputChange","reason","choice","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","direction","alignItems","flex","noOptionsText","renderInput","params","placeholder","autoFocus","helperText","toUpperCase","maxLength","inputMode"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-products-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n buildRoute,\n checkEntitlement,\n CRM_COLLECTIONS,\n type CrmDealLineItem,\n DEAL_LINE_ITEM_NAME_MAX,\n DEAL_LINE_ITEM_QUANTITY_MAX,\n DEAL_LINE_ITEMS_MAX,\n lineItemsTotalCents,\n pluginDocsHelp,\n readDealLineItems,\n Route,\n} from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { PrerequisiteNotice } from '@aglyn/shared-ui-jsx/components/prerequisite-notice.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Autocomplete,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n IconButton,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { doc, getDocs, updateDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { pluginRecordListSource } from '@aglyn/aglyn/plugin-manager/plugin-record-lists'\nimport type { CrmOrgDoc } from '../hooks/use-crm-scope'\nimport {\n amountInputValue,\n type DealDoc,\n DEFAULT_DEAL_CURRENCY,\n formatMoney,\n parseAmountInput,\n} from '../model/deal-board-model'\n\n/** Catalog matches offered per keystroke; a longer list is a scroll, not a pick. */\nconst CATALOG_MATCHES = 8\n\n/** One thing the catalog search offers: a product, or one variant of it. */\ninterface CatalogChoice {\n key: string\n productId: string\n label: string\n unitAmountCents: number\n}\n\nexport interface DealProductsCardProps {\n deal: DealDoc\n orgId: string\n /**\n * The site whose catalog the search reads — the console's own, or\n * `null` at the organization level (AGL-2630), where the deal's own\n * capturing site is searched; a deal with none takes lines by hand only.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The listener's verdict on the deal, for the stale-seed guard. */\n fromCache: boolean\n unreadable: boolean\n}\n\n/**\n * The products behind a deal's amount (AGL-2620).\n *\n * ## The amount follows the lines\n *\n * Every write here stores `lineItems` AND `amountCents` as their sum, in\n * one update, so the board, the reports and the API keep reading the one\n * field they always read and no reader adds up lines. Once a deal has a\n * line, the drawer's Amount goes read-only and says why; remove the last\n * line and the amount is typed again, keeping the last sum rather than\n * dropping to nothing. The currency is the deal's — a line cannot be in\n * another, because their sum is one number.\n *\n * ## Two doors to a line\n *\n * **From the catalog** searches THIS site's products — a prefix on\n * `nameTokens` over active products, the query the products hub runs and\n * the `(status, nameTokens, nameLower)` index serves — and offers each\n * variant priced. The catalog is USD; a deal in another currency is told\n * so and can edit the unit amount after. **By hand** takes a name and a\n * price with no product behind them, which is also all a plan without\n * commerce gets: the catalog door is not drawn for it, and the dialog says\n * so with a link to Billing (AGL-3982), since a plan with the store is the\n * one step that opens it.\n *\n * Adding opens a dialog rather than a row of fields above the table, and\n * a quantity is edited in place and written on blur.\n */\nexport function DealProductsCard(props: DealProductsCardProps) {\n const { deal, orgId, hostId, org, fromCache, unreadable } = props\n const firestore = useFirestore()\n // The catalog searched: the mounted site's, or the deal's own.\n const catalogHostId = hostId ?? deal.hostId ?? null\n const { enqueueSnackbar } = useSnackbar()\n const currency = String(deal.currency || DEFAULT_DEAL_CURRENCY).toLowerCase()\n const items = useMemo(() => deal.lineItems ?? [], [deal.lineItems])\n const hasCommerce = checkEntitlement(\n (org ?? null) as Partial<AglynOrgBilling> | null,\n 'commerce',\n )\n // Where a plan with the store is chosen. The org doc carries its own slug,\n // so the link costs no read; without one the notice still names the step.\n const slug = (org as Record<string, unknown> | null | undefined)?.['slug']\n const orgSlug = typeof slug === 'string' ? slug : ''\n const billingHref = orgSlug ? `${buildRoute(Route.MANAGE_BILLING, { orgSlug })}#plans` : undefined\n const [busy, setBusy] = useState(false)\n const [adding, setAdding] = useState(false)\n\n /** The one write: the lines and their sum, together. */\n const save = useCallback(\n async (next: CrmDealLineItem[], done: string) => {\n const read = readDealLineItems(next, currency)\n if ('error' in read) {\n enqueueSnackbar(read.error, { variant: 'warning', persist: false })\n return false\n }\n setBusy(true)\n try {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id), {\n lineItems: read.items,\n // The last sum stays on a deal whose lines were all removed:\n // the forecast should not drop to zero because a quote was\n // cleared to be retyped.\n ...(read.items.length ? { amountCents: lineItemsTotalCents(read.items) } : {}),\n updatedAt: new Date(),\n })\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return false\n }\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n return false\n } finally {\n setBusy(false)\n }\n },\n [currency, enqueueSnackbar, unreadable, fromCache, firestore, orgId, deal.$id],\n )\n\n const handleRemove = useCallback(\n (index: number) =>\n void save(\n items.filter((_item, at) => at !== index),\n 'Line removed',\n ),\n [items, save],\n )\n const handleQuantity = useCallback(\n (index: number, quantity: number) => {\n if (quantity === items[index]?.quantity) return\n void save(\n items.map((item, at) => (at === index ? { ...item, quantity } : item)),\n 'Quantity saved',\n )\n },\n [items, save],\n )\n const handleAdd = useCallback(\n async (line: CrmDealLineItem) => {\n const ok = await save([...items, line], 'Line added')\n if (ok) setAdding(false)\n },\n [items, save],\n )\n\n const total = lineItemsTotalCents(items)\n const full = items.length >= DEAL_LINE_ITEMS_MAX\n\n return (\n <>\n <CardDisplay\n header={'Products'}\n help={pluginDocsHelp('deals', { anchor: '#line-items' })}\n HeaderProps={{\n action: (\n <Tooltip title={full ? `A deal carries at most ${DEAL_LINE_ITEMS_MAX} lines` : ''}>\n <span>\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={busy || full}\n onClick={() => setAdding(true)}\n >\n {'Add line'}\n </Button>\n </span>\n </Tooltip>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n {items.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {hasCommerce\n ? 'No line items yet. Add products from your catalog or by hand; the amount becomes their sum.'\n : 'No line items yet. Add what the deal is for, priced by hand; the amount becomes their sum.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\" aria-label=\"Line items\">\n <TableHead>\n <TableRow>\n <TableCell>{'Item'}</TableCell>\n <TableCell align=\"right\" sx={{ width: 110 }}>\n {'Quantity'}\n </TableCell>\n <TableCell align=\"right\">{'Unit'}</TableCell>\n <TableCell align=\"right\">{'Total'}</TableCell>\n <TableCell sx={{ width: 48 }} />\n </TableRow>\n </TableHead>\n <TableBody>\n {items.map((item, index) => (\n <TableRow key={`${index}:${item.productId ?? item.name}`}>\n <TableCell>\n <Typography variant=\"body2\">{item.name}</Typography>\n {item.productId ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'From the catalog'}\n </Typography>\n ) : null}\n </TableCell>\n <TableCell align=\"right\">\n <QuantityField\n value={item.quantity}\n disabled={busy}\n label={`Quantity of ${item.name}`}\n onCommit={(quantity) => handleQuantity(index, quantity)}\n />\n </TableCell>\n <TableCell align=\"right\">{formatMoney(item.unitAmountCents, currency)}</TableCell>\n <TableCell align=\"right\">\n {formatMoney(item.quantity * item.unitAmountCents, currency)}\n </TableCell>\n <TableCell padding=\"none\" align=\"right\">\n <Tooltip title=\"Remove line\">\n <span>\n <IconButton\n size=\"small\"\n color=\"error\"\n disabled={busy}\n aria-label={`Remove ${item.name}`}\n onClick={() => handleRemove(index)}\n >\n <MdiIcon path={mdiDeleteOutline.path} size={0.8} />\n </IconButton>\n </span>\n </Tooltip>\n </TableCell>\n </TableRow>\n ))}\n <TableRow>\n <TableCell colSpan={3}>\n <Typography variant=\"subtitle2\">{'Amount'}</Typography>\n </TableCell>\n <TableCell align=\"right\">\n <Typography variant=\"subtitle2\" data-testid=\"line-items-total\">\n {formatMoney(total, currency)}\n </Typography>\n </TableCell>\n <TableCell padding=\"none\" />\n </TableRow>\n </TableBody>\n </ScrollTable>\n )}\n </CardDisplay>\n <AddLineDialog\n open={adding}\n onClose={() => setAdding(false)}\n hostId={catalogHostId}\n currency={currency}\n catalog={hasCommerce && Boolean(catalogHostId)}\n catalogNeedsPlan={!hasCommerce && Boolean(catalogHostId)}\n billingHref={billingHref}\n busy={busy}\n onAdd={handleAdd}\n />\n </>\n )\n}\nDealProductsCard.displayName = 'DealProductsCard'\n\ninterface QuantityFieldProps {\n value: number\n disabled: boolean\n label: string\n onCommit: (quantity: number) => void\n}\n\n/** A quantity edited in place, committed on blur or Enter, refused below one. */\nfunction QuantityField(props: QuantityFieldProps) {\n const { value, disabled, label, onCommit } = props\n const [draft, setDraft] = useState(String(value))\n useEffect(() => setDraft(String(value)), [value])\n const commit = () => {\n const quantity = Number(draft)\n if (Number.isInteger(quantity) && quantity >= 1 && quantity <= DEAL_LINE_ITEM_QUANTITY_MAX) {\n onCommit(quantity)\n } else {\n setDraft(String(value))\n }\n }\n return (\n <TextField\n size=\"small\"\n type=\"number\"\n value={draft}\n disabled={disabled}\n onChange={(event) => setDraft(event.target.value)}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n ;(event.target as HTMLInputElement).blur()\n }\n }}\n sx={{ width: 88 }}\n slotProps={{\n htmlInput: {\n min: 1,\n max: DEAL_LINE_ITEM_QUANTITY_MAX,\n step: 1,\n 'aria-label': label,\n style: { textAlign: 'right' },\n },\n }}\n />\n )\n}\nQuantityField.displayName = 'QuantityField'\n\ninterface AddLineDialogProps {\n open: boolean\n onClose: () => void\n /** The site whose catalog is searched; `null` draws no catalog door. */\n hostId: string | null\n currency: string\n /** Whether the catalog door is drawn — the plan has commerce. */\n catalog: boolean\n /**\n * The deal names a site, but the plan has no store, so its catalog is not\n * offered: the dialog says why and links to Billing.\n */\n catalogNeedsPlan?: boolean\n /** The workspace's Billing plans, for that notice. */\n billingHref?: string\n busy: boolean\n onAdd: (line: CrmDealLineItem) => Promise<void>\n}\n\ntype Door = 'catalog' | 'manual'\n\nfunction AddLineDialog(props: AddLineDialogProps) {\n const { open, onClose, hostId, currency, catalog, catalogNeedsPlan, billingHref, busy, onAdd } = props\n const firestore = useFirestore()\n const [door, setDoor] = useState<Door>(catalog ? 'catalog' : 'manual')\n const [picked, setPicked] = useState<CatalogChoice | null>(null)\n const [name, setName] = useState('')\n const [quantity, setQuantity] = useState('1')\n const [unit, setUnit] = useState('')\n const [search, setSearch] = useState('')\n const [choices, setChoices] = useState<CatalogChoice[]>([])\n\n useEffect(() => {\n if (!open) return\n setDoor(catalog ? 'catalog' : 'manual')\n setPicked(null)\n setName('')\n setQuantity('1')\n setUnit('')\n setSearch('')\n setChoices([])\n }, [open, catalog])\n\n /*\n * THE CATALOG SEARCH: what is typed, matched the way the plugin that keeps\n * products matches it — through the source it publishes (AGL-3080), which\n * offers active products only — debounced so a name typed at speed is one\n * query rather than one per letter. Nothing typed, or no plugin keeping\n * products, matches nothing. Each variant is its own choice, priced from\n * its `priceUsd` in cents.\n */\n useEffect(() => {\n if (!open || door !== 'catalog' || !hostId) return\n const products = pluginRecordListSource('product')?.source ?? null\n const catalogQuery = products?.query(firestore, { hostId, search, limit: CATALOG_MATCHES }) ?? null\n if (!products || !catalogQuery) {\n setChoices([])\n return\n }\n let active = true\n const timer = setTimeout(() => {\n void getDocs(catalogQuery)\n .then((snapshot) => {\n if (!active) return\n const next: CatalogChoice[] = []\n for (const entry of snapshot.docs) {\n const product = products.record(entry.id, entry.data())\n if (!product) continue\n const variants = Array.isArray(product.facts['variants'])\n ? (product.facts['variants'] as Array<Record<string, unknown>>)\n : []\n const priced = variants.length\n ? variants\n : [{ id: '', options: {}, priceUsd: product.facts['priceUsd'] }]\n for (const variant of priced) {\n const options = Object.values((variant['options'] as Record<string, string>) ?? {})\n const label = options.length ? `${product.name} — ${options.join(' / ')}` : product.name\n next.push({\n key: `${product.id}:${String(variant['id'] ?? '')}`,\n productId: product.id,\n label,\n unitAmountCents: Math.max(0, Math.round((Number(variant['priceUsd']) || 0) * 100)),\n })\n }\n }\n setChoices(next)\n })\n .catch(() => {\n if (active) setChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, door, hostId, search])\n\n const unitCents = parseAmountInput(unit)\n const quantityNumber = Number(quantity)\n const lineName = (door === 'catalog' ? (name || picked?.label || '') : name).trim()\n const problem = !lineName\n ? door === 'catalog'\n ? 'Pick a product.'\n : 'A line needs a name.'\n : !Number.isInteger(quantityNumber) || quantityNumber < 1\n ? 'The quantity has to be a whole number of one or more.'\n : unitCents === null\n ? 'The unit amount has to be a number of zero or more.'\n : null\n\n const handleAdd = () => {\n if (problem || unitCents === null) return\n void onAdd({\n ...(door === 'catalog' && picked ? { productId: picked.productId } : {}),\n name: lineName.slice(0, DEAL_LINE_ITEM_NAME_MAX),\n quantity: quantityNumber,\n unitAmountCents: unitCents,\n currency,\n })\n }\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} fullWidth maxWidth=\"xs\">\n <DialogTitle>{'Add a line'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n {catalogNeedsPlan ? (\n <PrerequisiteNotice href={billingHref} actionLabel=\"See plans\">\n {'Adding lines from your product catalog needs a plan with the store. Add this line by hand, or choose a plan with the store in Billing.'}\n </PrerequisiteNotice>\n ) : null}\n {catalog ? (\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={door}\n onChange={(_event, next) => {\n if (next) setDoor(next as Door)\n }}\n aria-label=\"Where the line comes from\"\n >\n <ToggleButton value=\"catalog\">{'From the catalog'}</ToggleButton>\n <ToggleButton value=\"manual\">{'By hand'}</ToggleButton>\n </ToggleButtonGroup>\n ) : null}\n {door === 'catalog' ? (\n <Autocomplete<CatalogChoice, false, false, false>\n options={choices}\n value={picked}\n inputValue={search}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setSearch(next)\n }}\n onChange={(_event, choice) => {\n setPicked(choice)\n setName(choice?.label ?? '')\n setUnit(choice ? amountInputValue(choice.unitAmountCents) : '')\n }}\n getOptionLabel={(choice) => choice.label}\n isOptionEqualToValue={(option, value) => option.key === value.key}\n // The match is the query's own prefix over `nameTokens`, never\n // MUI's substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.key}>\n <Stack direction=\"row\" spacing={1} sx={{ width: '100%', alignItems: 'baseline' }}>\n <Typography variant=\"body2\" sx={{ flex: 1 }}>\n {choice.label}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {formatMoney(choice.unitAmountCents, 'usd')}\n </Typography>\n </Stack>\n </li>\n )}\n noOptionsText={search.trim() ? 'No active product matches' : 'Type a product name'}\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Product\"\n placeholder=\"Search this site's catalog\"\n autoFocus\n helperText={\n currency !== 'usd'\n ? `Catalog prices are in USD; this deal is in ${currency.toUpperCase()}. Check the unit amount.`\n : undefined\n }\n />\n )}\n />\n ) : (\n <TextField\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_LINE_ITEM_NAME_MAX } }}\n />\n )}\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Quantity\"\n type=\"number\"\n value={quantity}\n onChange={(event) => setQuantity(event.target.value)}\n sx={{ width: 120 }}\n slotProps={{ htmlInput: { min: 1, max: DEAL_LINE_ITEM_QUANTITY_MAX, step: 1 } }}\n />\n <TextField\n label={`Unit amount (${currency.toUpperCase()})`}\n value={unit}\n onChange={(event) => setUnit(event.target.value)}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n </Stack>\n {problem && (lineName || unit) ? (\n <Typography variant=\"caption\" color=\"warning.main\">\n {problem}\n </Typography>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" disabled={busy || Boolean(problem)} onClick={handleAdd}>\n {'Add line'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nAddLineDialog.displayName = 'AddLineDialog'\n\nexport default DealProductsCard\n"],"names":["buildRoute","checkEntitlement","CRM_COLLECTIONS","DEAL_LINE_ITEM_NAME_MAX","DEAL_LINE_ITEM_QUANTITY_MAX","DEAL_LINE_ITEMS_MAX","lineItemsTotalCents","pluginDocsHelp","readDealLineItems","Route","mdiDeleteOutline","mdiPlus","CardDisplay","MdiIcon","PrerequisiteNotice","ScrollTable","useSnackbar","useFirestore","writeGuardedBySeed","Autocomplete","Button","Dialog","DialogActions","DialogContent","DialogTitle","IconButton","Stack","TableBody","TableCell","TableHead","TableRow","TextField","ToggleButton","ToggleButtonGroup","Tooltip","Typography","doc","getDocs","updateDoc","useCallback","useEffect","useMemo","useState","pluginRecordListSource","amountInputValue","DEFAULT_DEAL_CURRENCY","formatMoney","parseAmountInput","CATALOG_MATCHES","DealProductsCard","props","hostId","deal","orgId","org","fromCache","unreadable","firestore","catalogHostId","enqueueSnackbar","currency","String","toLowerCase","items","lineItems","hasCommerce","slug","orgSlug","billingHref","MANAGE_BILLING","undefined","busy","setBusy","adding","setAdding","save","next","done","read","error","variant","persist","verdict","subject","deals","$id","length","amountCents","updatedAt","Date","ok","message","console","allowDuplicate","handleRemove","index","filter","_item","at","handleQuantity","quantity","map","item","handleAdd","line","total","full","header","help","anchor","HeaderProps","action","title","span","size","startIcon","path","disabled","onClick","contentGutterX","contentGutterY","color","aria-label","align","sx","width","name","productId","QuantityField","value","label","onCommit","unitAmountCents","padding","colSpan","data-testid","AddLineDialog","open","onClose","catalog","Boolean","catalogNeedsPlan","onAdd","displayName","draft","setDraft","commit","Number","isInteger","type","onChange","event","target","onBlur","onKeyDown","key","preventDefault","blur","slotProps","htmlInput","min","max","step","style","textAlign","door","setDoor","picked","setPicked","setName","setQuantity","unit","setUnit","search","setSearch","choices","setChoices","products","source","catalogQuery","query","limit","active","timer","setTimeout","then","snapshot","entry","docs","product","record","id","data","variants","Array","isArray","facts","priced","options","priceUsd","Object","values","join","push","Math","round","catch","clearTimeout","unitCents","quantityNumber","lineName","trim","problem","slice","fullWidth","maxWidth","spacing","pt","href","actionLabel","exclusive","_event","inputValue","onInputChange","reason","choice","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","direction","alignItems","flex","noOptionsText","renderInput","params","placeholder","autoFocus","helperText","toUpperCase","maxLength","inputMode"],"mappings":"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"}
|
|
@@ -19,6 +19,8 @@ import { registerApiV1Resource, registerApiV1UsageFigures } from "@aglyn/tenant-
|
|
|
19
19
|
import { listPluginMembershipDetachers, registerPluginMembershipDetacher } from "@aglyn/aglyn/plugin-manager/plugin-membership-detach";
|
|
20
20
|
import { registerPluginPersonRecordsEraser } from "@aglyn/aglyn/plugin-manager/plugin-person-erasure";
|
|
21
21
|
import { registerPluginTransferResource } from "@aglyn/aglyn/plugin-manager/plugin-transfer-resources";
|
|
22
|
+
import { registerPluginSearchSources } from "@aglyn/aglyn/plugin-manager/plugin-search-sources";
|
|
23
|
+
import { CRM_SEARCH_SOURCES } from "./model/crm-search-sources.js";
|
|
22
24
|
import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
|
|
23
25
|
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
24
26
|
import { checkCrmFieldSetup, checkCrmPipelineSetup, CRM_FIELD_SETUP_RESOURCE, CRM_PIPELINE_SETUP_RESOURCE } from "./model/crm-setup-content.js";
|
|
@@ -184,6 +186,14 @@ let setupLoading = null;
|
|
|
184
186
|
pluginId: BUNDLE_ID
|
|
185
187
|
});
|
|
186
188
|
}
|
|
189
|
+
// What the native apps' one box finds in the record system (AGL-3943): the
|
|
190
|
+
// palette's own six groups, under the gates the CRM's console extension
|
|
191
|
+
// puts on them.
|
|
192
|
+
registerPluginSearchSources(CRM_SEARCH_SOURCES, {
|
|
193
|
+
pluginId: BUNDLE_ID,
|
|
194
|
+
featureFlag: 'crm',
|
|
195
|
+
permission: 'data.manage'
|
|
196
|
+
});
|
|
187
197
|
}
|
|
188
198
|
|
|
189
199
|
//# sourceMappingURL=declarations.console-server.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/declarations.console-server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The registry's own module, not the data layer's barrel: boot needs one\n// registry, not the whole server surface.\nimport {\n type ApiV1ResourceHandler,\n registerApiV1Resource,\n registerApiV1UsageFigures,\n} from '@aglyn/tenant-data-admin/server/api-v1-resources'\nimport {\n listPluginMembershipDetachers,\n registerPluginMembershipDetacher,\n} from '@aglyn/aglyn/plugin-manager/plugin-membership-detach'\nimport { registerPluginPersonRecordsEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { registerPluginTransferResource } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport {\n registerPluginResourceDraftWriter,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport {\n checkCrmFieldSetup,\n checkCrmPipelineSetup,\n CRM_FIELD_SETUP_RESOURCE,\n CRM_PIPELINE_SETUP_RESOURCE,\n} from './model/crm-setup-content'\nimport { checkCrmPicklistValuesSetup, CRM_PICKLIST_VALUES_SETUP_RESOURCE } from './model/crm-picklist-setup-content'\nimport { registerCrmSetupAiCapabilities } from './server/crm-setup-ai-capabilities'\nimport { crmTransferPlanGate, registerCrmTransferResources } from './transfer/register'\nimport {\n CRM_EMAIL_TEMPLATE_PACKAGE_RULES,\n CRM_EMAIL_TEMPLATES_TRANSFER_KEY,\n crmEmailTemplateDependencies,\n remapCrmEmailTemplateIds,\n type CrmEmailTemplatePackageContent,\n} from './transfer/email-templates-package'\n\ntype CrmSetupWriters = typeof import('./server/crm-setup-writers.server')\nlet setupLoading: Promise<CrmSetupWriters> | null = null\n\n/** The CRM's setup writers over the Admin SDK (AGL-3746), loaded on their first read or write. */\nfunction setupWriters(): Promise<CrmSetupWriters> {\n setupLoading ??= import('./server/crm-setup-writers.server').catch((error: unknown) => {\n setupLoading = null\n throw error\n })\n return setupLoading\n}\n\n/**\n * A setup writer as boot registers it: the check is pure and answers at once;\n * the hooks that read or write load the Admin SDK half on first use.\n */\nfunction lazySetupWriter(\n check: (content: Readonly<Record<string, unknown>>) => ReturnType<PluginResourceDraftWriter['check']>,\n writer: (module: CrmSetupWriters) => PluginResourceDraftWriter,\n): PluginResourceDraftWriter {\n return {\n check: (content) => check(content),\n refusal: async (context) => writer(await setupWriters()).refusal(context),\n read: async (context) => writer(await setupWriters()).read(context),\n write: async (request) => writer(await setupWriters()).write(request),\n }\n}\n\n/** The `crm-pipeline` writer as boot registers it. */\nexport const lazyCrmPipelineSetupWriter = lazySetupWriter(\n checkCrmPipelineSetup,\n (module) => module.crmPipelineSetupWriter,\n)\n\n/** The `crm-field` writer as boot registers it. */\nexport const lazyCrmFieldSetupWriter = lazySetupWriter(\n checkCrmFieldSetup,\n (module) => module.crmFieldSetupWriter,\n)\n\n/** The `crm-picklist-values` writer as boot registers it (AGL-3756). */\nexport const lazyCrmPicklistValuesSetupWriter = lazySetupWriter(\n checkCrmPicklistValuesSetup,\n (module) => module.crmPicklistValuesSetupWriter,\n)\n\n/** The email templates package's server half (AGL-3535), loaded when an import or export first asks. */\nconst templatesPackage = async () =>\n (await import('./transfer/email-templates-package.server')).createCrmEmailTemplatesPackage()\n\n/**\n * The plan feature every CRM resource of `/v1` needs, and the sentence its\n * refusal says (AGL-2611, AGL-2851): the whole CRM is included from\n * Starter, so a Free workspace reads and changes none of it over the API. A\n * capture still records people on every plan; working them is the CRM.\n */\nconst CRM_SUITE = {\n feature: 'crm',\n message:\n 'The CRM — contacts, leads, companies, deals, tasks, pipelines, ' +\n 'activities and email templates — is not included in this organization’s plan',\n} as const\n\n/**\n * The CRM's resources on the customer REST API, each handler loaded with the\n * first request for it. In the order the API root lists them: the people\n * first, then the records a team keeps about them, then a site's leads\n * (`/v1/leads?siteId=`, a top-level path with the site as a parameter), then\n * the letters sent from a record (AGL-2658).\n */\nconst CRM_API_V1_RESOURCES: ReadonlyArray<readonly [string, ApiV1ResourceHandler]> = [\n ['contacts', async (...args) => (await import('./server/api-v1/contacts')).handleContacts(...args)],\n ['companies', async (...args) => (await import('./server/api-v1/crm-companies')).handleCompanies(...args)],\n ['pipelines', async (...args) => (await import('./server/api-v1/crm-pipelines')).handlePipelines(...args)],\n ['deals', async (...args) => (await import('./server/api-v1/crm-deals')).handleDeals(...args)],\n ['tasks', async (...args) => (await import('./server/api-v1/crm-tasks')).handleTasks(...args)],\n ['activities', async (...args) => (await import('./server/api-v1/crm-activities')).handleActivities(...args)],\n ['leads', async (...args) => (await import('./server/api-v1/crm-leads')).handleLeads(...args)],\n [\n 'email-templates',\n async (...args) => (await import('./server/api-v1/crm-email-templates')).handleEmailTemplates(...args),\n ],\n]\n\n/**\n * The CRM's CONSOLE-ONLY server declarations, named under\n * `consoleServerDeclarations` in `plugins.config.json` and run at the\n * console's boot.\n *\n * It serves the CRM's resources of `/v1` (AGL-2606): the console's router\n * owns the pipeline in front of them — the key, the plan's API access, the\n * quota, the rate limit, the error envelope — and hands every request under\n * `/v1/contacts`, `/v1/deals` and the rest to the CRM once the key is\n * authenticated and the organization's plan carries the CRM. Each resource\n * brings its description for the API's OpenAPI document, and the CRM's\n * records band and collection sizes join `GET /v1/usage`. The API is the\n * console's alone, so the tenant runtime does not register them.\n *\n * It registers the server half of the CRM's import and export resources\n * (`transfer/register.ts`).\n *\n * It also clears a removed container off the CRM's records: a campaign is\n * deleted from the console, and its owner asks every plugin's membership\n * detacher first (`server/container-detach.ts`).\n *\n * Light at boot: each resource's module is imported with its first request,\n * the descriptions when the document is first built, and the usage reader\n * with the first usage call.\n * Registering again replaces this plugin's own entries, so a second call (a\n * hot reload, a spec) is harmless.\n */\nexport function registerCrmConsoleServerDeclarations(): void {\n // Email templates in a workspace package (AGL-3535).\n registerPluginTransferResource(\n CRM_EMAIL_TEMPLATES_TRANSFER_KEY,\n {\n items: async (ctx) => (await templatesPackage()).items(ctx),\n dependencies: (item) => crmEmailTemplateDependencies(item as CrmEmailTemplatePackageContent),\n remapIds: (item, idMap) => remapCrmEmailTemplateIds(item as CrmEmailTemplatePackageContent, idMap),\n readItems: async (ctx, ids) => (await templatesPackage()).readItems(ctx, ids),\n writeItems: async (ctx, items, writer) => (await templatesPackage()).writeItems(ctx, items as never, writer),\n revertItems: async (ctx, steps) => (await templatesPackage()).revertItems(ctx, steps as never),\n problems: async (ctx, write) => (await templatesPackage()).problems(ctx, write as never),\n rules: CRM_EMAIL_TEMPLATE_PACKAGE_RULES,\n planGate: crmTransferPlanGate('email templates'),\n },\n { pluginId: BUNDLE_ID },\n )\n // The record system's share of a person erasure (AGL-2623, AGL-3080): it\n // names the person's contacts before anybody erases, and erases them, their\n // satellites and their lead after everybody else. Required, and loaded with\n // the first erasure.\n registerPluginPersonRecordsEraser(\n {\n locate: async (target) => (await import('./server/person-eraser')).crmPersonEraser().locate(target),\n erase: async (request) => (await import('./server/person-eraser')).crmPersonEraser().erase(request),\n },\n { pluginId: BUNDLE_ID },\n )\n for (const [resource, handle] of CRM_API_V1_RESOURCES) {\n registerApiV1Resource(\n resource,\n {\n handle,\n entitlement: CRM_SUITE,\n describe: async () =>\n (await import('./server/api-v1/openapi')).CRM_API_V1_DESCRIPTIONS[resource],\n },\n { pluginId: BUNDLE_ID },\n )\n }\n registerApiV1UsageFigures(\n async (ctx) => (await import('./server/api-v1/usage')).crmUsageFigures(ctx),\n { pluginId: BUNDLE_ID },\n )\n // The CRM's setup an AI build asks for (AGL-3746): a pipeline and a custom\n // field, saved by these writers under the CRM's rules, and the operations\n // the build plans for them. The console runs AI jobs (AGL-3026).\n registerPluginResourceDraftWriter(CRM_PIPELINE_SETUP_RESOURCE, lazyCrmPipelineSetupWriter, { pluginId: BUNDLE_ID })\n registerPluginResourceDraftWriter(CRM_FIELD_SETUP_RESOURCE, lazyCrmFieldSetupWriter, { pluginId: BUNDLE_ID })\n registerPluginResourceDraftWriter(CRM_PICKLIST_VALUES_SETUP_RESOURCE, lazyCrmPicklistValuesSetupWriter, {\n pluginId: BUNDLE_ID,\n })\n registerCrmSetupAiCapabilities()\n // What the CRM imports and exports (AGL-3527): each resource's module\n // loads with the first transfer that asks for it.\n registerCrmTransferResources(BUNDLE_ID)\n // A removed container — a deleted campaign — comes off every lead and every\n // contact facet naming it, before its owner removes it (AGL-3080).\n if (!listPluginMembershipDetachers().includes(BUNDLE_ID)) {\n registerPluginMembershipDetacher(\n async (request) => (await import('./server/container-detach')).crmContainerDetacher(request),\n { pluginId: BUNDLE_ID },\n )\n }\n}\n"],"names":["registerApiV1Resource","registerApiV1UsageFigures","listPluginMembershipDetachers","registerPluginMembershipDetacher","registerPluginPersonRecordsEraser","registerPluginTransferResource","registerPluginResourceDraftWriter","BUNDLE_ID","checkCrmFieldSetup","checkCrmPipelineSetup","CRM_FIELD_SETUP_RESOURCE","CRM_PIPELINE_SETUP_RESOURCE","checkCrmPicklistValuesSetup","CRM_PICKLIST_VALUES_SETUP_RESOURCE","registerCrmSetupAiCapabilities","crmTransferPlanGate","registerCrmTransferResources","CRM_EMAIL_TEMPLATE_PACKAGE_RULES","CRM_EMAIL_TEMPLATES_TRANSFER_KEY","crmEmailTemplateDependencies","remapCrmEmailTemplateIds","setupLoading","setupWriters","catch","error","lazySetupWriter","check","writer","content","refusal","context","read","write","request","lazyCrmPipelineSetupWriter","module","crmPipelineSetupWriter","lazyCrmFieldSetupWriter","crmFieldSetupWriter","lazyCrmPicklistValuesSetupWriter","crmPicklistValuesSetupWriter","templatesPackage","createCrmEmailTemplatesPackage","CRM_SUITE","feature","message","CRM_API_V1_RESOURCES","args","handleContacts","handleCompanies","handlePipelines","handleDeals","handleTasks","handleActivities","handleLeads","handleEmailTemplates","registerCrmConsoleServerDeclarations","items","ctx","dependencies","item","remapIds","idMap","readItems","ids","writeItems","revertItems","steps","problems","rules","planGate","pluginId","locate","target","crmPersonEraser","erase","resource","handle","entitlement","describe","CRM_API_V1_DESCRIPTIONS","crmUsageFigures","includes","crmContainerDetacher"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/declarations.console-server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The registry's own module, not the data layer's barrel: boot needs one\n// registry, not the whole server surface.\nimport {\n type ApiV1ResourceHandler,\n registerApiV1Resource,\n registerApiV1UsageFigures,\n} from '@aglyn/tenant-data-admin/server/api-v1-resources'\nimport {\n listPluginMembershipDetachers,\n registerPluginMembershipDetacher,\n} from '@aglyn/aglyn/plugin-manager/plugin-membership-detach'\nimport { registerPluginPersonRecordsEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { registerPluginTransferResource } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { registerPluginSearchSources } from '@aglyn/aglyn/plugin-manager/plugin-search-sources'\nimport { CRM_SEARCH_SOURCES } from './model/crm-search-sources'\nimport {\n registerPluginResourceDraftWriter,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport {\n checkCrmFieldSetup,\n checkCrmPipelineSetup,\n CRM_FIELD_SETUP_RESOURCE,\n CRM_PIPELINE_SETUP_RESOURCE,\n} from './model/crm-setup-content'\nimport { checkCrmPicklistValuesSetup, CRM_PICKLIST_VALUES_SETUP_RESOURCE } from './model/crm-picklist-setup-content'\nimport { registerCrmSetupAiCapabilities } from './server/crm-setup-ai-capabilities'\nimport { crmTransferPlanGate, registerCrmTransferResources } from './transfer/register'\nimport {\n CRM_EMAIL_TEMPLATE_PACKAGE_RULES,\n CRM_EMAIL_TEMPLATES_TRANSFER_KEY,\n crmEmailTemplateDependencies,\n remapCrmEmailTemplateIds,\n type CrmEmailTemplatePackageContent,\n} from './transfer/email-templates-package'\n\ntype CrmSetupWriters = typeof import('./server/crm-setup-writers.server')\nlet setupLoading: Promise<CrmSetupWriters> | null = null\n\n/** The CRM's setup writers over the Admin SDK (AGL-3746), loaded on their first read or write. */\nfunction setupWriters(): Promise<CrmSetupWriters> {\n setupLoading ??= import('./server/crm-setup-writers.server').catch((error: unknown) => {\n setupLoading = null\n throw error\n })\n return setupLoading\n}\n\n/**\n * A setup writer as boot registers it: the check is pure and answers at once;\n * the hooks that read or write load the Admin SDK half on first use.\n */\nfunction lazySetupWriter(\n check: (content: Readonly<Record<string, unknown>>) => ReturnType<PluginResourceDraftWriter['check']>,\n writer: (module: CrmSetupWriters) => PluginResourceDraftWriter,\n): PluginResourceDraftWriter {\n return {\n check: (content) => check(content),\n refusal: async (context) => writer(await setupWriters()).refusal(context),\n read: async (context) => writer(await setupWriters()).read(context),\n write: async (request) => writer(await setupWriters()).write(request),\n }\n}\n\n/** The `crm-pipeline` writer as boot registers it. */\nexport const lazyCrmPipelineSetupWriter = lazySetupWriter(\n checkCrmPipelineSetup,\n (module) => module.crmPipelineSetupWriter,\n)\n\n/** The `crm-field` writer as boot registers it. */\nexport const lazyCrmFieldSetupWriter = lazySetupWriter(\n checkCrmFieldSetup,\n (module) => module.crmFieldSetupWriter,\n)\n\n/** The `crm-picklist-values` writer as boot registers it (AGL-3756). */\nexport const lazyCrmPicklistValuesSetupWriter = lazySetupWriter(\n checkCrmPicklistValuesSetup,\n (module) => module.crmPicklistValuesSetupWriter,\n)\n\n/** The email templates package's server half (AGL-3535), loaded when an import or export first asks. */\nconst templatesPackage = async () =>\n (await import('./transfer/email-templates-package.server')).createCrmEmailTemplatesPackage()\n\n/**\n * The plan feature every CRM resource of `/v1` needs, and the sentence its\n * refusal says (AGL-2611, AGL-2851): the whole CRM is included from\n * Starter, so a Free workspace reads and changes none of it over the API. A\n * capture still records people on every plan; working them is the CRM.\n */\nconst CRM_SUITE = {\n feature: 'crm',\n message:\n 'The CRM — contacts, leads, companies, deals, tasks, pipelines, ' +\n 'activities and email templates — is not included in this organization’s plan',\n} as const\n\n/**\n * The CRM's resources on the customer REST API, each handler loaded with the\n * first request for it. In the order the API root lists them: the people\n * first, then the records a team keeps about them, then a site's leads\n * (`/v1/leads?siteId=`, a top-level path with the site as a parameter), then\n * the letters sent from a record (AGL-2658).\n */\nconst CRM_API_V1_RESOURCES: ReadonlyArray<readonly [string, ApiV1ResourceHandler]> = [\n ['contacts', async (...args) => (await import('./server/api-v1/contacts')).handleContacts(...args)],\n ['companies', async (...args) => (await import('./server/api-v1/crm-companies')).handleCompanies(...args)],\n ['pipelines', async (...args) => (await import('./server/api-v1/crm-pipelines')).handlePipelines(...args)],\n ['deals', async (...args) => (await import('./server/api-v1/crm-deals')).handleDeals(...args)],\n ['tasks', async (...args) => (await import('./server/api-v1/crm-tasks')).handleTasks(...args)],\n ['activities', async (...args) => (await import('./server/api-v1/crm-activities')).handleActivities(...args)],\n ['leads', async (...args) => (await import('./server/api-v1/crm-leads')).handleLeads(...args)],\n [\n 'email-templates',\n async (...args) => (await import('./server/api-v1/crm-email-templates')).handleEmailTemplates(...args),\n ],\n]\n\n/**\n * The CRM's CONSOLE-ONLY server declarations, named under\n * `consoleServerDeclarations` in `plugins.config.json` and run at the\n * console's boot.\n *\n * It serves the CRM's resources of `/v1` (AGL-2606): the console's router\n * owns the pipeline in front of them — the key, the plan's API access, the\n * quota, the rate limit, the error envelope — and hands every request under\n * `/v1/contacts`, `/v1/deals` and the rest to the CRM once the key is\n * authenticated and the organization's plan carries the CRM. Each resource\n * brings its description for the API's OpenAPI document, and the CRM's\n * records band and collection sizes join `GET /v1/usage`. The API is the\n * console's alone, so the tenant runtime does not register them.\n *\n * It registers the server half of the CRM's import and export resources\n * (`transfer/register.ts`).\n *\n * It also clears a removed container off the CRM's records: a campaign is\n * deleted from the console, and its owner asks every plugin's membership\n * detacher first (`server/container-detach.ts`).\n *\n * Light at boot: each resource's module is imported with its first request,\n * the descriptions when the document is first built, and the usage reader\n * with the first usage call.\n * Registering again replaces this plugin's own entries, so a second call (a\n * hot reload, a spec) is harmless.\n */\nexport function registerCrmConsoleServerDeclarations(): void {\n // Email templates in a workspace package (AGL-3535).\n registerPluginTransferResource(\n CRM_EMAIL_TEMPLATES_TRANSFER_KEY,\n {\n items: async (ctx) => (await templatesPackage()).items(ctx),\n dependencies: (item) => crmEmailTemplateDependencies(item as CrmEmailTemplatePackageContent),\n remapIds: (item, idMap) => remapCrmEmailTemplateIds(item as CrmEmailTemplatePackageContent, idMap),\n readItems: async (ctx, ids) => (await templatesPackage()).readItems(ctx, ids),\n writeItems: async (ctx, items, writer) => (await templatesPackage()).writeItems(ctx, items as never, writer),\n revertItems: async (ctx, steps) => (await templatesPackage()).revertItems(ctx, steps as never),\n problems: async (ctx, write) => (await templatesPackage()).problems(ctx, write as never),\n rules: CRM_EMAIL_TEMPLATE_PACKAGE_RULES,\n planGate: crmTransferPlanGate('email templates'),\n },\n { pluginId: BUNDLE_ID },\n )\n // The record system's share of a person erasure (AGL-2623, AGL-3080): it\n // names the person's contacts before anybody erases, and erases them, their\n // satellites and their lead after everybody else. Required, and loaded with\n // the first erasure.\n registerPluginPersonRecordsEraser(\n {\n locate: async (target) => (await import('./server/person-eraser')).crmPersonEraser().locate(target),\n erase: async (request) => (await import('./server/person-eraser')).crmPersonEraser().erase(request),\n },\n { pluginId: BUNDLE_ID },\n )\n for (const [resource, handle] of CRM_API_V1_RESOURCES) {\n registerApiV1Resource(\n resource,\n {\n handle,\n entitlement: CRM_SUITE,\n describe: async () =>\n (await import('./server/api-v1/openapi')).CRM_API_V1_DESCRIPTIONS[resource],\n },\n { pluginId: BUNDLE_ID },\n )\n }\n registerApiV1UsageFigures(\n async (ctx) => (await import('./server/api-v1/usage')).crmUsageFigures(ctx),\n { pluginId: BUNDLE_ID },\n )\n // The CRM's setup an AI build asks for (AGL-3746): a pipeline and a custom\n // field, saved by these writers under the CRM's rules, and the operations\n // the build plans for them. The console runs AI jobs (AGL-3026).\n registerPluginResourceDraftWriter(CRM_PIPELINE_SETUP_RESOURCE, lazyCrmPipelineSetupWriter, { pluginId: BUNDLE_ID })\n registerPluginResourceDraftWriter(CRM_FIELD_SETUP_RESOURCE, lazyCrmFieldSetupWriter, { pluginId: BUNDLE_ID })\n registerPluginResourceDraftWriter(CRM_PICKLIST_VALUES_SETUP_RESOURCE, lazyCrmPicklistValuesSetupWriter, {\n pluginId: BUNDLE_ID,\n })\n registerCrmSetupAiCapabilities()\n // What the CRM imports and exports (AGL-3527): each resource's module\n // loads with the first transfer that asks for it.\n registerCrmTransferResources(BUNDLE_ID)\n // A removed container — a deleted campaign — comes off every lead and every\n // contact facet naming it, before its owner removes it (AGL-3080).\n if (!listPluginMembershipDetachers().includes(BUNDLE_ID)) {\n registerPluginMembershipDetacher(\n async (request) => (await import('./server/container-detach')).crmContainerDetacher(request),\n { pluginId: BUNDLE_ID },\n )\n }\n // What the native apps' one box finds in the record system (AGL-3943): the\n // palette's own six groups, under the gates the CRM's console extension\n // puts on them.\n registerPluginSearchSources(CRM_SEARCH_SOURCES, {\n pluginId: BUNDLE_ID,\n featureFlag: 'crm',\n permission: 'data.manage',\n })\n}\n"],"names":["registerApiV1Resource","registerApiV1UsageFigures","listPluginMembershipDetachers","registerPluginMembershipDetacher","registerPluginPersonRecordsEraser","registerPluginTransferResource","registerPluginSearchSources","CRM_SEARCH_SOURCES","registerPluginResourceDraftWriter","BUNDLE_ID","checkCrmFieldSetup","checkCrmPipelineSetup","CRM_FIELD_SETUP_RESOURCE","CRM_PIPELINE_SETUP_RESOURCE","checkCrmPicklistValuesSetup","CRM_PICKLIST_VALUES_SETUP_RESOURCE","registerCrmSetupAiCapabilities","crmTransferPlanGate","registerCrmTransferResources","CRM_EMAIL_TEMPLATE_PACKAGE_RULES","CRM_EMAIL_TEMPLATES_TRANSFER_KEY","crmEmailTemplateDependencies","remapCrmEmailTemplateIds","setupLoading","setupWriters","catch","error","lazySetupWriter","check","writer","content","refusal","context","read","write","request","lazyCrmPipelineSetupWriter","module","crmPipelineSetupWriter","lazyCrmFieldSetupWriter","crmFieldSetupWriter","lazyCrmPicklistValuesSetupWriter","crmPicklistValuesSetupWriter","templatesPackage","createCrmEmailTemplatesPackage","CRM_SUITE","feature","message","CRM_API_V1_RESOURCES","args","handleContacts","handleCompanies","handlePipelines","handleDeals","handleTasks","handleActivities","handleLeads","handleEmailTemplates","registerCrmConsoleServerDeclarations","items","ctx","dependencies","item","remapIds","idMap","readItems","ids","writeItems","revertItems","steps","problems","rules","planGate","pluginId","locate","target","crmPersonEraser","erase","resource","handle","entitlement","describe","CRM_API_V1_DESCRIPTIONS","crmUsageFigures","includes","crmContainerDetacher","featureFlag","permission"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type NativeNavContribution, type NativeNavCreateContribution } from '@aglyn/aglyn/app-utils/native-nav';
|
|
18
|
+
export declare const NATIVE_NAV_ROWS: readonly NativeNavContribution[];
|
|
19
|
+
export declare const NATIVE_NAV_CREATE: readonly NativeNavCreateContribution[];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { nativeNavHubSection, nativeNavIcon, nativeNavRef } from "@aglyn/aglyn/app-utils/native-nav";
|
|
17
|
+
/**
|
|
18
|
+
* The CRM in the native apps' navigation (AGL-3943): the rows this plugin
|
|
19
|
+
* adds to the manifest the shells draw, placed into the core's lists by
|
|
20
|
+
* `order` (`assembleNativeNav`, `generate:native-contracts`). Labels are the
|
|
21
|
+
* console's words, held by `apps/console/specs/native-nav-manifest.spec.ts`.
|
|
22
|
+
*
|
|
23
|
+
* Imports only the core's plain nav module, so the native contracts read it.
|
|
24
|
+
*/ /**
|
|
25
|
+
* The row at either scope. The workspace's opens the console's org page
|
|
26
|
+
* (`orgNavItems`, `/[orgSlug]/crm`), the site's the site's hub; both list
|
|
27
|
+
* the same sections under the same ids.
|
|
28
|
+
*/ function row(scope, order) {
|
|
29
|
+
return {
|
|
30
|
+
id: 'crm',
|
|
31
|
+
label: 'CRM',
|
|
32
|
+
icon: nativeNavIcon('person.text.rectangle', 'contact_mail'),
|
|
33
|
+
screen: 'crm.contacts',
|
|
34
|
+
pluginId: 'crm',
|
|
35
|
+
order,
|
|
36
|
+
keywords: [
|
|
37
|
+
'contacts',
|
|
38
|
+
'people',
|
|
39
|
+
'deals',
|
|
40
|
+
'tasks'
|
|
41
|
+
],
|
|
42
|
+
sections: [
|
|
43
|
+
nativeNavHubSection(scope, '/crm', 'contacts', 'Contacts', 'crm.contacts'),
|
|
44
|
+
nativeNavHubSection(scope, '/crm', 'leads', 'Leads', 'crm.leads'),
|
|
45
|
+
nativeNavHubSection(scope, '/crm', 'companies', 'Companies', 'crm.companies'),
|
|
46
|
+
nativeNavHubSection(scope, '/crm', 'deals', 'Deals', 'crm.deals'),
|
|
47
|
+
nativeNavHubSection(scope, '/crm', 'tasks', 'Tasks', 'crm.tasks'),
|
|
48
|
+
nativeNavHubSection(scope, '/crm', 'reports', 'Reports', 'crm.reports'),
|
|
49
|
+
nativeNavHubSection(scope, '/crm', 'fields', 'Fields', 'crm.fields'),
|
|
50
|
+
nativeNavHubSection(scope, '/crm', 'settings', 'Settings', 'crm.settings')
|
|
51
|
+
],
|
|
52
|
+
docs: scope === 'org' ? '/content-and-data/crm/overview#at-the-organization-level' : '/content-and-data/crm/overview',
|
|
53
|
+
console: nativeNavRef(scope === 'org' ? 'orgPluginTab' : 'hostPluginTab', '/crm')
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export const NATIVE_NAV_ROWS = [
|
|
57
|
+
{
|
|
58
|
+
list: 'workspace',
|
|
59
|
+
group: 'workspace',
|
|
60
|
+
row: row('org', 20)
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
list: 'site',
|
|
64
|
+
group: 'customers',
|
|
65
|
+
row: row('host', 20)
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
/** A contact, at either scope: the console's "New contact" on the CRM's Contacts. */ function contact(scope, order) {
|
|
69
|
+
return {
|
|
70
|
+
id: 'contact',
|
|
71
|
+
label: 'Contact',
|
|
72
|
+
icon: nativeNavIcon('person.badge.plus', 'person_add'),
|
|
73
|
+
screen: 'crm.contacts',
|
|
74
|
+
params: {
|
|
75
|
+
new: '1'
|
|
76
|
+
},
|
|
77
|
+
pluginId: 'crm',
|
|
78
|
+
permission: 'data.manage',
|
|
79
|
+
planFeature: 'crm',
|
|
80
|
+
order,
|
|
81
|
+
console: nativeNavRef(scope === 'org' ? 'orgPluginSection' : 'hostPluginSection', '/crm/contacts')
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export const NATIVE_NAV_CREATE = [
|
|
85
|
+
{
|
|
86
|
+
scope: 'workspace',
|
|
87
|
+
item: contact('org', 20)
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
scope: 'site',
|
|
91
|
+
item: contact('host', 70)
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
//# sourceMappingURL=native-nav.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/native-nav.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type NativeNavContribution,\n type NativeNavCreateContribution,\n type NativeNavRow,\n nativeNavHubSection,\n nativeNavIcon,\n nativeNavRef,\n} from '@aglyn/aglyn/app-utils/native-nav'\n\n/**\n * The CRM in the native apps' navigation (AGL-3943): the rows this plugin\n * adds to the manifest the shells draw, placed into the core's lists by\n * `order` (`assembleNativeNav`, `generate:native-contracts`). Labels are the\n * console's words, held by `apps/console/specs/native-nav-manifest.spec.ts`.\n *\n * Imports only the core's plain nav module, so the native contracts read it.\n */\n\n/**\n * The row at either scope. The workspace's opens the console's org page\n * (`orgNavItems`, `/[orgSlug]/crm`), the site's the site's hub; both list\n * the same sections under the same ids.\n */\nfunction row(scope: 'org' | 'host', order: number): NativeNavRow {\n return {\n id: 'crm',\n label: 'CRM',\n icon: nativeNavIcon('person.text.rectangle', 'contact_mail'),\n screen: 'crm.contacts',\n pluginId: 'crm',\n order,\n keywords: ['contacts', 'people', 'deals', 'tasks'],\n sections: [\n nativeNavHubSection(scope, '/crm', 'contacts', 'Contacts', 'crm.contacts'),\n nativeNavHubSection(scope, '/crm', 'leads', 'Leads', 'crm.leads'),\n nativeNavHubSection(scope, '/crm', 'companies', 'Companies', 'crm.companies'),\n nativeNavHubSection(scope, '/crm', 'deals', 'Deals', 'crm.deals'),\n nativeNavHubSection(scope, '/crm', 'tasks', 'Tasks', 'crm.tasks'),\n nativeNavHubSection(scope, '/crm', 'reports', 'Reports', 'crm.reports'),\n nativeNavHubSection(scope, '/crm', 'fields', 'Fields', 'crm.fields'),\n nativeNavHubSection(scope, '/crm', 'settings', 'Settings', 'crm.settings'),\n ],\n docs: scope === 'org' ? '/content-and-data/crm/overview#at-the-organization-level' : '/content-and-data/crm/overview',\n console: nativeNavRef(scope === 'org' ? 'orgPluginTab' : 'hostPluginTab', '/crm'),\n }\n}\n\nexport const NATIVE_NAV_ROWS: readonly NativeNavContribution[] = [\n { list: 'workspace', group: 'workspace', row: row('org', 20) },\n { list: 'site', group: 'customers', row: row('host', 20) },\n]\n\n/** A contact, at either scope: the console's \"New contact\" on the CRM's Contacts. */\nfunction contact(scope: 'org' | 'host', order: number): NativeNavCreateContribution['item'] {\n return {\n id: 'contact',\n label: 'Contact',\n icon: nativeNavIcon('person.badge.plus', 'person_add'),\n screen: 'crm.contacts',\n params: { new: '1' },\n pluginId: 'crm',\n permission: 'data.manage',\n planFeature: 'crm',\n order,\n console: nativeNavRef(scope === 'org' ? 'orgPluginSection' : 'hostPluginSection', '/crm/contacts'),\n }\n}\n\nexport const NATIVE_NAV_CREATE: readonly NativeNavCreateContribution[] = [\n { scope: 'workspace', item: contact('org', 20) },\n { scope: 'site', item: contact('host', 70) },\n]\n"],"names":["nativeNavHubSection","nativeNavIcon","nativeNavRef","row","scope","order","id","label","icon","screen","pluginId","keywords","sections","docs","console","NATIVE_NAV_ROWS","list","group","contact","params","new","permission","planFeature","NATIVE_NAV_CREATE","item"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAIEA,mBAAmB,EACnBC,aAAa,EACbC,YAAY,QACP,oCAAmC;AAE1C;;;;;;;CAOC,GAED;;;;CAIC,GACD,SAASC,IAAIC,KAAqB,EAAEC,KAAa;IAC/C,OAAO;QACLC,IAAI;QACJC,OAAO;QACPC,MAAMP,cAAc,yBAAyB;QAC7CQ,QAAQ;QACRC,UAAU;QACVL;QACAM,UAAU;YAAC;YAAY;YAAU;YAAS;SAAQ;QAClDC,UAAU;YACRZ,oBAAoBI,OAAO,QAAQ,YAAY,YAAY;YAC3DJ,oBAAoBI,OAAO,QAAQ,SAAS,SAAS;YACrDJ,oBAAoBI,OAAO,QAAQ,aAAa,aAAa;YAC7DJ,oBAAoBI,OAAO,QAAQ,SAAS,SAAS;YACrDJ,oBAAoBI,OAAO,QAAQ,SAAS,SAAS;YACrDJ,oBAAoBI,OAAO,QAAQ,WAAW,WAAW;YACzDJ,oBAAoBI,OAAO,QAAQ,UAAU,UAAU;YACvDJ,oBAAoBI,OAAO,QAAQ,YAAY,YAAY;SAC5D;QACDS,MAAMT,UAAU,QAAQ,6DAA6D;QACrFU,SAASZ,aAAaE,UAAU,QAAQ,iBAAiB,iBAAiB;IAC5E;AACF;AAEA,OAAO,MAAMW,kBAAoD;IAC/D;QAAEC,MAAM;QAAaC,OAAO;QAAad,KAAKA,IAAI,OAAO;IAAI;IAC7D;QAAEa,MAAM;QAAQC,OAAO;QAAad,KAAKA,IAAI,QAAQ;IAAI;CAC1D,CAAA;AAED,mFAAmF,GACnF,SAASe,QAAQd,KAAqB,EAAEC,KAAa;IACnD,OAAO;QACLC,IAAI;QACJC,OAAO;QACPC,MAAMP,cAAc,qBAAqB;QACzCQ,QAAQ;QACRU,QAAQ;YAAEC,KAAK;QAAI;QACnBV,UAAU;QACVW,YAAY;QACZC,aAAa;QACbjB;QACAS,SAASZ,aAAaE,UAAU,QAAQ,qBAAqB,qBAAqB;IACpF;AACF;AAEA,OAAO,MAAMmB,oBAA4D;IACvE;QAAEnB,OAAO;QAAaoB,MAAMN,QAAQ,OAAO;IAAI;IAC/C;QAAEd,OAAO;QAAQoB,MAAMN,QAAQ,QAAQ;IAAI;CAC5C,CAAA"}
|