@aglyn/plugins-crm 1.0.0-beta.269 → 1.0.0-beta.271
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/model/native-vocabulary.d.ts +15 -0
- package/src/lib/model/native-vocabulary.js +20 -0
- package/src/lib/model/native-vocabulary.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/src/lib/plugin.js +13 -2
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server.js +35 -1
- package/src/lib/server.js.map +1 -1
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.271",
|
|
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.271",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.271",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.271",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.271",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.271",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.271",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.271",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.271",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.271",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.271",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.271",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.271",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.271",
|
|
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":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,yEAAyE;AACzE,0CAA0C;AAC1C,SAEEA,qBAAqB,EACrBC,yBAAyB,QACpB,mDAAkD;AACzD,SACEC,6BAA6B,EAC7BC,gCAAgC,QAC3B,uDAAsD;AAC7D,SAASC,iCAAiC,QAAQ,oDAAmD;AACrG,SAASC,8BAA8B,QAAQ,wDAAuD;AACtG,SAASC,2BAA2B,QAAQ,oDAAmD;AAC/F,SAASC,kBAAkB,QAAQ,gCAA4B;AAC/D,SACEC,iCAAiC,QAE5B,qDAAoD;AAC3D,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SACEC,kBAAkB,EAClBC,qBAAqB,EACrBC,wBAAwB,EACxBC,2BAA2B,QACtB,+BAA2B;AAClC,SAASC,2BAA2B,EAAEC,kCAAkC,QAAQ,wCAAoC;AACpH,SAASC,8BAA8B,QAAQ,wCAAoC;AACnF,SAASC,mBAAmB,EAAEC,4BAA4B,QAAQ,yBAAqB;AACvF,SACEC,gCAAgC,EAChCC,gCAAgC,EAChCC,4BAA4B,EAC5BC,wBAAwB,QAEnB,wCAAoC;AAG3C,IAAIC,eAAgD;AAEpD,gGAAgG,GAChG,SAASC;IACPD,uBAAAA,eAAAA,eAAiB,MAAM,CAAC,wCAAqCE,KAAK,CAAC,CAACC;QAClEH,eAAe;QACf,MAAMG;IACR;IACA,OAAOH;AACT;AAEA;;;CAGC,GACD,SAASI,gBACPC,KAAqG,EACrGC,MAA8D;IAE9D,OAAO;QACLD,OAAO,CAACE,UAAYF,MAAME;QAC1BC,SAAS,OAAOC,UAAYH,OAAO,MAAML,gBAAgBO,OAAO,CAACC;QACjEC,MAAM,OAAOD,UAAYH,OAAO,MAAML,gBAAgBS,IAAI,CAACD;QAC3DE,OAAO,OAAOC,UAAYN,OAAO,MAAML,gBAAgBU,KAAK,CAACC;IAC/D;AACF;AAEA,oDAAoD,GACpD,OAAO,MAAMC,6BAA6BT,gBACxChB,uBACA,CAAC0B,SAAWA,OAAOC,sBAAsB,EAC1C;AAED,iDAAiD,GACjD,OAAO,MAAMC,0BAA0BZ,gBACrCjB,oBACA,CAAC2B,SAAWA,OAAOG,mBAAmB,EACvC;AAED,sEAAsE,GACtE,OAAO,MAAMC,mCAAmCd,gBAC9Cb,6BACA,CAACuB,SAAWA,OAAOK,4BAA4B,EAChD;AAED,sGAAsG,GACtG,MAAMC,mBAAmB,UACvB,AAAC,CAAA,MAAM,MAAM,CAAC,+CAA2C,EAAGC,8BAA8B;AAE5F;;;;;CAKC,GACD,MAAMC,YAAY;IAChBC,SAAS;IACTC,SACE,oEACA;AACJ;AAEA;;;;;;CAMC,GACD,MAAMC,uBAA+E;IACnF;QAAC;QAAY,OAAO,GAAGC,OAAS,AAAC,CAAA,MAAM,MAAM,CAAC,8BAA0B,EAAGC,cAAc,IAAID;KAAM;IACnG;QAAC;QAAa,OAAO,GAAGA,OAAS,AAAC,CAAA,MAAM,MAAM,CAAC,mCAA+B,EAAGE,eAAe,IAAIF;KAAM;IAC1G;QAAC;QAAa,OAAO,GAAGA,OAAS,AAAC,CAAA,MAAM,MAAM,CAAC,mCAA+B,EAAGG,eAAe,IAAIH;KAAM;IAC1G;QAAC;QAAS,OAAO,GAAGA,OAAS,AAAC,CAAA,MAAM,MAAM,CAAC,+BAA2B,EAAGI,WAAW,IAAIJ;KAAM;IAC9F;QAAC;QAAS,OAAO,GAAGA,OAAS,AAAC,CAAA,MAAM,MAAM,CAAC,+BAA2B,EAAGK,WAAW,IAAIL;KAAM;IAC9F;QAAC;QAAc,OAAO,GAAGA,OAAS,AAAC,CAAA,MAAM,MAAM,CAAC,oCAAgC,EAAGM,gBAAgB,IAAIN;KAAM;IAC7G;QAAC;QAAS,OAAO,GAAGA,OAAS,AAAC,CAAA,MAAM,MAAM,CAAC,+BAA2B,EAAGO,WAAW,IAAIP;KAAM;IAC9F;QACE;QACA,OAAO,GAAGA,OAAS,AAAC,CAAA,MAAM,MAAM,CAAC,yCAAqC,EAAGQ,oBAAoB,IAAIR;KAClG;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,SAASS;IACd,qDAAqD;IACrDrD,+BACEe,kCACA;QACEuC,OAAO,OAAOC,MAAQ,AAAC,CAAA,MAAMjB,kBAAiB,EAAGgB,KAAK,CAACC;QACvDC,cAAc,CAACC,OAASzC,6BAA6ByC;QACrDC,UAAU,CAACD,MAAME,QAAU1C,yBAAyBwC,MAAwCE;QAC5FC,WAAW,OAAOL,KAAKM,MAAQ,AAAC,CAAA,MAAMvB,kBAAiB,EAAGsB,SAAS,CAACL,KAAKM;QACzEC,YAAY,OAAOP,KAAKD,OAAO9B,SAAW,AAAC,CAAA,MAAMc,kBAAiB,EAAGwB,UAAU,CAACP,KAAKD,OAAgB9B;QACrGuC,aAAa,OAAOR,KAAKS,QAAU,AAAC,CAAA,MAAM1B,kBAAiB,EAAGyB,WAAW,CAACR,KAAKS;QAC/EC,UAAU,OAAOV,KAAK1B,QAAU,AAAC,CAAA,MAAMS,kBAAiB,EAAG2B,QAAQ,CAACV,KAAK1B;QACzEqC,OAAOpD;QACPqD,UAAUvD,oBAAoB;IAChC,GACA;QAAEwD,UAAUhE;IAAU;IAExB,yEAAyE;IACzE,4EAA4E;IAC5E,4EAA4E;IAC5E,qBAAqB;IACrBL,kCACE;QACEsE,QAAQ,OAAOC,SAAW,AAAC,CAAA,MAAM,MAAM,CAAC,4BAAwB,EAAGC,eAAe,GAAGF,MAAM,CAACC;QAC5FE,OAAO,OAAO1C,UAAY,AAAC,CAAA,MAAM,MAAM,CAAC,4BAAwB,EAAGyC,eAAe,GAAGC,KAAK,CAAC1C;IAC7F,GACA;QAAEsC,UAAUhE;IAAU;IAExB,KAAK,MAAM,CAACqE,UAAUC,OAAO,IAAI/B,qBAAsB;QACrDhD,sBACE8E,UACA;YACEC;YACAC,aAAanC;YACboC,UAAU,UACR,AAAC,CAAA,MAAM,MAAM,CAAC,6BAAyB,EAAGC,uBAAuB,CAACJ,SAAS;QAC/E,GACA;YAAEL,UAAUhE;QAAU;IAE1B;IACAR,0BACE,OAAO2D,MAAQ,AAAC,CAAA,MAAM,MAAM,CAAC,2BAAuB,EAAGuB,eAAe,CAACvB,MACvE;QAAEa,UAAUhE;IAAU;IAExB,2EAA2E;IAC3E,0EAA0E;IAC1E,iEAAiE;IACjED,kCAAkCK,6BAA6BuB,4BAA4B;QAAEqC,UAAUhE;IAAU;IACjHD,kCAAkCI,0BAA0B2B,yBAAyB;QAAEkC,UAAUhE;IAAU;IAC3GD,kCAAkCO,oCAAoC0B,kCAAkC;QACtGgC,UAAUhE;IACZ;IACAO;IACA,sEAAsE;IACtE,kDAAkD;IAClDE,6BAA6BT;IAC7B,4EAA4E;IAC5E,mEAAmE;IACnE,IAAI,CAACP,gCAAgCkF,QAAQ,CAAC3E,YAAY;QACxDN,iCACE,OAAOgC,UAAY,AAAC,CAAA,MAAM,MAAM,CAAC,+BAA2B,EAAGkD,oBAAoB,CAAClD,UACpF;YAAEsC,UAAUhE;QAAU;IAE1B;IACA,2EAA2E;IAC3E,wEAAwE;IACxE,gBAAgB;IAChBH,4BAA4BC,oBAAoB;QAC9CkE,UAAUhE;QACV6E,aAAa;QACbC,YAAY;IACd;AACF"}
|
|
@@ -39,3 +39,18 @@ export interface NativeCrmPicklist {
|
|
|
39
39
|
}
|
|
40
40
|
/** Every picklist the CRM keeps, with the labels a workspace starts with. */
|
|
41
41
|
export declare const NATIVE_CRM_PICKLISTS: NativeCrmPicklist[];
|
|
42
|
+
/** Every custom field type, in the order the console's field drawer lists them (AGL-3971). */
|
|
43
|
+
export declare const NATIVE_CRM_FIELD_TYPES: string[];
|
|
44
|
+
/** How each custom field type reads, the console's own words. */
|
|
45
|
+
export declare const NATIVE_CRM_FIELD_TYPE_LABELS: Record<string, string>;
|
|
46
|
+
/** The currencies a currency field's picker offers: the deal amount picker's list, lowercase ISO 4217. */
|
|
47
|
+
export declare const NATIVE_CRM_FIELD_CURRENCIES: string[];
|
|
48
|
+
/** The currency a currency field is in when it names none. */
|
|
49
|
+
export declare const NATIVE_CRM_FIELD_DEFAULT_CURRENCY: string;
|
|
50
|
+
/**
|
|
51
|
+
* How many decimals each currency's minor unit has where it is not two, as
|
|
52
|
+
* the console's `Intl` reads them (`currencyFractionDigits`): JPY, KRW and
|
|
53
|
+
* VND 0; BHD, KWD and OMR 3. A code not listed has two, as the console's
|
|
54
|
+
* fallback does.
|
|
55
|
+
*/
|
|
56
|
+
export declare const NATIVE_CURRENCY_FRACTION_DIGITS: Record<string, number>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -21,7 +22,9 @@
|
|
|
21
22
|
* is the same data re-read as plain arrays and maps; nothing in the web
|
|
22
23
|
* apps imports this file, so it adds nothing to their bundles.
|
|
23
24
|
*/ import { CONTACT_LIFECYCLE_STAGES, CRM_ACTIVITY_DIRECTIONS, CRM_ACTIVITY_KINDS, CRM_FIELD_OBJECTS, CRM_LEAD_OPEN_STATUSES, CRM_LEAD_STATUSES, CRM_PICKLIST_DEFINITIONS, CRM_TASK_KINDS } from "@aglyn/aglyn/app-utils/crm";
|
|
25
|
+
import { CONTACT_FIELD_DEFAULT_CURRENCY, CONTACT_FIELD_TYPE_LABELS, CONTACT_FIELD_TYPES, currencyFractionDigits } from "@aglyn/aglyn/app-utils/contact-custom-fields";
|
|
24
26
|
import { CRM_REPORT_PERIODS } from "./crm-reports.js";
|
|
27
|
+
import { DEAL_CURRENCIES } from "./deal-board-model.js";
|
|
25
28
|
/** Every lead status, in the console's order. */ export const NATIVE_CRM_LEAD_STATUSES = [
|
|
26
29
|
...CRM_LEAD_STATUSES
|
|
27
30
|
];
|
|
@@ -55,5 +58,22 @@ import { CRM_REPORT_PERIODS } from "./crm-reports.js";
|
|
|
55
58
|
object: definition.object,
|
|
56
59
|
standardLabels: definition.standardValues.map((value)=>value.label)
|
|
57
60
|
}));
|
|
61
|
+
/** Every custom field type, in the order the console's field drawer lists them (AGL-3971). */ export const NATIVE_CRM_FIELD_TYPES = [
|
|
62
|
+
...CONTACT_FIELD_TYPES
|
|
63
|
+
];
|
|
64
|
+
/** How each custom field type reads, the console's own words. */ export const NATIVE_CRM_FIELD_TYPE_LABELS = _extends({}, CONTACT_FIELD_TYPE_LABELS);
|
|
65
|
+
/** The currencies a currency field's picker offers: the deal amount picker's list, lowercase ISO 4217. */ export const NATIVE_CRM_FIELD_CURRENCIES = [
|
|
66
|
+
...DEAL_CURRENCIES
|
|
67
|
+
];
|
|
68
|
+
/** The currency a currency field is in when it names none. */ export const NATIVE_CRM_FIELD_DEFAULT_CURRENCY = CONTACT_FIELD_DEFAULT_CURRENCY;
|
|
69
|
+
/**
|
|
70
|
+
* How many decimals each currency's minor unit has where it is not two, as
|
|
71
|
+
* the console's `Intl` reads them (`currencyFractionDigits`): JPY, KRW and
|
|
72
|
+
* VND 0; BHD, KWD and OMR 3. A code not listed has two, as the console's
|
|
73
|
+
* fallback does.
|
|
74
|
+
*/ export const NATIVE_CURRENCY_FRACTION_DIGITS = Object.fromEntries(Intl.supportedValuesOf('currency').map((code)=>[
|
|
75
|
+
code.toLowerCase(),
|
|
76
|
+
currencyFractionDigits(code)
|
|
77
|
+
]).filter(([, digits])=>digits !== 2));
|
|
58
78
|
|
|
59
79
|
//# sourceMappingURL=native-vocabulary.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/native-vocabulary.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/*\n * THE CRM'S VOCABULARIES, FLAT, FOR THE NATIVE APPS (AGL-3669).\n *\n * The console reads these off `as const` tuples and typed definitions,\n * shapes the native contracts generator does not translate. Each value here\n * is the same data re-read as plain arrays and maps; nothing in the web\n * apps imports this file, so it adds nothing to their bundles.\n */\n\nimport {\n CONTACT_LIFECYCLE_STAGES,\n CRM_ACTIVITY_DIRECTIONS,\n CRM_ACTIVITY_KINDS,\n CRM_FIELD_OBJECTS,\n CRM_LEAD_OPEN_STATUSES,\n CRM_LEAD_STATUSES,\n CRM_PICKLIST_DEFINITIONS,\n CRM_TASK_KINDS,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { CRM_REPORT_PERIODS } from './crm-reports'\n\n/** Every lead status, in the console's order. */\nexport const NATIVE_CRM_LEAD_STATUSES: string[] = [...CRM_LEAD_STATUSES]\n\n/** The statuses a lead still needs working in. */\nexport const NATIVE_CRM_LEAD_OPEN_STATUSES: string[] = [...CRM_LEAD_OPEN_STATUSES]\n\n/** Every contact lifecycle stage, in order. */\nexport const NATIVE_CONTACT_LIFECYCLE_STAGES: string[] = [...CONTACT_LIFECYCLE_STAGES]\n\n/** Every task kind, in order. */\nexport const NATIVE_CRM_TASK_KINDS: string[] = [...CRM_TASK_KINDS]\n\n/** Every activity kind, in order. */\nexport const NATIVE_CRM_ACTIVITY_KINDS: string[] = [...CRM_ACTIVITY_KINDS]\n\n/** The objects custom fields are kept for. */\nexport const NATIVE_CRM_FIELD_OBJECTS: string[] = [...CRM_FIELD_OBJECTS]\n\n/** The report periods, in the console's order. */\nexport const NATIVE_CRM_REPORT_PERIODS: string[] = [...CRM_REPORT_PERIODS]\n\n/** The directions an activity of each kind can take. */\nexport const NATIVE_CRM_ACTIVITY_DIRECTIONS: Record<string, string[]> = Object.fromEntries(\n Object.entries(CRM_ACTIVITY_DIRECTIONS).map(([kind, directions]) => [kind, [...(directions ?? [])]]),\n)\n\n/** One picklist as the native pickers start it: its id, label, object and standard labels. */\nexport interface NativeCrmPicklist {\n id: string\n label: string\n object: string\n standardLabels: string[]\n}\n\n/** Every picklist the CRM keeps, with the labels a workspace starts with. */\nexport const NATIVE_CRM_PICKLISTS: NativeCrmPicklist[] = CRM_PICKLIST_DEFINITIONS.map((definition) => ({\n id: definition.id,\n label: definition.label,\n object: definition.object,\n standardLabels: definition.standardValues.map((value) => value.label),\n}))\n"],"names":["CONTACT_LIFECYCLE_STAGES","CRM_ACTIVITY_DIRECTIONS","CRM_ACTIVITY_KINDS","CRM_FIELD_OBJECTS","CRM_LEAD_OPEN_STATUSES","CRM_LEAD_STATUSES","CRM_PICKLIST_DEFINITIONS","CRM_TASK_KINDS","CRM_REPORT_PERIODS","NATIVE_CRM_LEAD_STATUSES","NATIVE_CRM_LEAD_OPEN_STATUSES","NATIVE_CONTACT_LIFECYCLE_STAGES","NATIVE_CRM_TASK_KINDS","NATIVE_CRM_ACTIVITY_KINDS","NATIVE_CRM_FIELD_OBJECTS","NATIVE_CRM_REPORT_PERIODS","NATIVE_CRM_ACTIVITY_DIRECTIONS","Object","fromEntries","entries","map","kind","directions","NATIVE_CRM_PICKLISTS","definition","id","label","object","standardLabels","standardValues","value"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;CAOC,GAED,SACEA,wBAAwB,EACxBC,uBAAuB,EACvBC,kBAAkB,EAClBC,iBAAiB,EACjBC,sBAAsB,EACtBC,iBAAiB,EACjBC,wBAAwB,EACxBC,cAAc,QACT,6BAA4B;AACnC,SAASC,kBAAkB,QAAQ,mBAAe;
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/native-vocabulary.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/*\n * THE CRM'S VOCABULARIES, FLAT, FOR THE NATIVE APPS (AGL-3669).\n *\n * The console reads these off `as const` tuples and typed definitions,\n * shapes the native contracts generator does not translate. Each value here\n * is the same data re-read as plain arrays and maps; nothing in the web\n * apps imports this file, so it adds nothing to their bundles.\n */\n\nimport {\n CONTACT_LIFECYCLE_STAGES,\n CRM_ACTIVITY_DIRECTIONS,\n CRM_ACTIVITY_KINDS,\n CRM_FIELD_OBJECTS,\n CRM_LEAD_OPEN_STATUSES,\n CRM_LEAD_STATUSES,\n CRM_PICKLIST_DEFINITIONS,\n CRM_TASK_KINDS,\n} from '@aglyn/aglyn/app-utils/crm'\nimport {\n CONTACT_FIELD_DEFAULT_CURRENCY,\n CONTACT_FIELD_TYPE_LABELS,\n CONTACT_FIELD_TYPES,\n currencyFractionDigits,\n} from '@aglyn/aglyn/app-utils/contact-custom-fields'\nimport { CRM_REPORT_PERIODS } from './crm-reports'\nimport { DEAL_CURRENCIES } from './deal-board-model'\n\n/** Every lead status, in the console's order. */\nexport const NATIVE_CRM_LEAD_STATUSES: string[] = [...CRM_LEAD_STATUSES]\n\n/** The statuses a lead still needs working in. */\nexport const NATIVE_CRM_LEAD_OPEN_STATUSES: string[] = [...CRM_LEAD_OPEN_STATUSES]\n\n/** Every contact lifecycle stage, in order. */\nexport const NATIVE_CONTACT_LIFECYCLE_STAGES: string[] = [...CONTACT_LIFECYCLE_STAGES]\n\n/** Every task kind, in order. */\nexport const NATIVE_CRM_TASK_KINDS: string[] = [...CRM_TASK_KINDS]\n\n/** Every activity kind, in order. */\nexport const NATIVE_CRM_ACTIVITY_KINDS: string[] = [...CRM_ACTIVITY_KINDS]\n\n/** The objects custom fields are kept for. */\nexport const NATIVE_CRM_FIELD_OBJECTS: string[] = [...CRM_FIELD_OBJECTS]\n\n/** The report periods, in the console's order. */\nexport const NATIVE_CRM_REPORT_PERIODS: string[] = [...CRM_REPORT_PERIODS]\n\n/** The directions an activity of each kind can take. */\nexport const NATIVE_CRM_ACTIVITY_DIRECTIONS: Record<string, string[]> = Object.fromEntries(\n Object.entries(CRM_ACTIVITY_DIRECTIONS).map(([kind, directions]) => [kind, [...(directions ?? [])]]),\n)\n\n/** One picklist as the native pickers start it: its id, label, object and standard labels. */\nexport interface NativeCrmPicklist {\n id: string\n label: string\n object: string\n standardLabels: string[]\n}\n\n/** Every picklist the CRM keeps, with the labels a workspace starts with. */\nexport const NATIVE_CRM_PICKLISTS: NativeCrmPicklist[] = CRM_PICKLIST_DEFINITIONS.map((definition) => ({\n id: definition.id,\n label: definition.label,\n object: definition.object,\n standardLabels: definition.standardValues.map((value) => value.label),\n}))\n\n/** Every custom field type, in the order the console's field drawer lists them (AGL-3971). */\nexport const NATIVE_CRM_FIELD_TYPES: string[] = [...CONTACT_FIELD_TYPES]\n\n/** How each custom field type reads, the console's own words. */\nexport const NATIVE_CRM_FIELD_TYPE_LABELS: Record<string, string> = { ...CONTACT_FIELD_TYPE_LABELS }\n\n/** The currencies a currency field's picker offers: the deal amount picker's list, lowercase ISO 4217. */\nexport const NATIVE_CRM_FIELD_CURRENCIES: string[] = [...DEAL_CURRENCIES]\n\n/** The currency a currency field is in when it names none. */\nexport const NATIVE_CRM_FIELD_DEFAULT_CURRENCY: string = CONTACT_FIELD_DEFAULT_CURRENCY\n\n/**\n * How many decimals each currency's minor unit has where it is not two, as\n * the console's `Intl` reads them (`currencyFractionDigits`): JPY, KRW and\n * VND 0; BHD, KWD and OMR 3. A code not listed has two, as the console's\n * fallback does.\n */\nexport const NATIVE_CURRENCY_FRACTION_DIGITS: Record<string, number> = Object.fromEntries(\n Intl.supportedValuesOf('currency')\n .map((code) => [code.toLowerCase(), currencyFractionDigits(code)] as const)\n .filter(([, digits]) => digits !== 2),\n)\n"],"names":["CONTACT_LIFECYCLE_STAGES","CRM_ACTIVITY_DIRECTIONS","CRM_ACTIVITY_KINDS","CRM_FIELD_OBJECTS","CRM_LEAD_OPEN_STATUSES","CRM_LEAD_STATUSES","CRM_PICKLIST_DEFINITIONS","CRM_TASK_KINDS","CONTACT_FIELD_DEFAULT_CURRENCY","CONTACT_FIELD_TYPE_LABELS","CONTACT_FIELD_TYPES","currencyFractionDigits","CRM_REPORT_PERIODS","DEAL_CURRENCIES","NATIVE_CRM_LEAD_STATUSES","NATIVE_CRM_LEAD_OPEN_STATUSES","NATIVE_CONTACT_LIFECYCLE_STAGES","NATIVE_CRM_TASK_KINDS","NATIVE_CRM_ACTIVITY_KINDS","NATIVE_CRM_FIELD_OBJECTS","NATIVE_CRM_REPORT_PERIODS","NATIVE_CRM_ACTIVITY_DIRECTIONS","Object","fromEntries","entries","map","kind","directions","NATIVE_CRM_PICKLISTS","definition","id","label","object","standardLabels","standardValues","value","NATIVE_CRM_FIELD_TYPES","NATIVE_CRM_FIELD_TYPE_LABELS","NATIVE_CRM_FIELD_CURRENCIES","NATIVE_CRM_FIELD_DEFAULT_CURRENCY","NATIVE_CURRENCY_FRACTION_DIGITS","Intl","supportedValuesOf","code","toLowerCase","filter","digits"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;CAOC,GAED,SACEA,wBAAwB,EACxBC,uBAAuB,EACvBC,kBAAkB,EAClBC,iBAAiB,EACjBC,sBAAsB,EACtBC,iBAAiB,EACjBC,wBAAwB,EACxBC,cAAc,QACT,6BAA4B;AACnC,SACEC,8BAA8B,EAC9BC,yBAAyB,EACzBC,mBAAmB,EACnBC,sBAAsB,QACjB,+CAA8C;AACrD,SAASC,kBAAkB,QAAQ,mBAAe;AAClD,SAASC,eAAe,QAAQ,wBAAoB;AAEpD,+CAA+C,GAC/C,OAAO,MAAMC,2BAAqC;OAAIT;CAAkB,CAAA;AAExE,gDAAgD,GAChD,OAAO,MAAMU,gCAA0C;OAAIX;CAAuB,CAAA;AAElF,6CAA6C,GAC7C,OAAO,MAAMY,kCAA4C;OAAIhB;CAAyB,CAAA;AAEtF,+BAA+B,GAC/B,OAAO,MAAMiB,wBAAkC;OAAIV;CAAe,CAAA;AAElE,mCAAmC,GACnC,OAAO,MAAMW,4BAAsC;OAAIhB;CAAmB,CAAA;AAE1E,4CAA4C,GAC5C,OAAO,MAAMiB,2BAAqC;OAAIhB;CAAkB,CAAA;AAExE,gDAAgD,GAChD,OAAO,MAAMiB,4BAAsC;OAAIR;CAAmB,CAAA;AAE1E,sDAAsD,GACtD,OAAO,MAAMS,iCAA2DC,OAAOC,WAAW,CACxFD,OAAOE,OAAO,CAACvB,yBAAyBwB,GAAG,CAAC,CAAC,CAACC,MAAMC,WAAW,GAAK;QAACD;QAAM;eAAKC,qBAAAA,aAAc,EAAE;SAAE;KAAC,GACpG;AAUD,2EAA2E,GAC3E,OAAO,MAAMC,uBAA4CtB,yBAAyBmB,GAAG,CAAC,CAACI,aAAgB,CAAA;QACrGC,IAAID,WAAWC,EAAE;QACjBC,OAAOF,WAAWE,KAAK;QACvBC,QAAQH,WAAWG,MAAM;QACzBC,gBAAgBJ,WAAWK,cAAc,CAACT,GAAG,CAAC,CAACU,QAAUA,MAAMJ,KAAK;IACtE,CAAA,GAAG;AAEH,4FAA4F,GAC5F,OAAO,MAAMK,yBAAmC;OAAI1B;CAAoB,CAAA;AAExE,+DAA+D,GAC/D,OAAO,MAAM2B,+BAAuD,aAAK5B,2BAA2B;AAEpG,wGAAwG,GACxG,OAAO,MAAM6B,8BAAwC;OAAIzB;CAAgB,CAAA;AAEzE,4DAA4D,GAC5D,OAAO,MAAM0B,oCAA4C/B,+BAA8B;AAEvF;;;;;CAKC,GACD,OAAO,MAAMgC,kCAA0DlB,OAAOC,WAAW,CACvFkB,KAAKC,iBAAiB,CAAC,YACpBjB,GAAG,CAAC,CAACkB,OAAS;QAACA,KAAKC,WAAW;QAAIjC,uBAAuBgC;KAAM,EAChEE,MAAM,CAAC,CAAC,GAAGC,OAAO,GAAKA,WAAW,IACtC"}
|
|
@@ -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"}
|
package/src/lib/plugin.js
CHANGED
|
@@ -32,6 +32,13 @@ import { CRM_RECORD_BOOKING_ZONE } from "./components/crm-booking-zone.js";
|
|
|
32
32
|
/** Code-split: the CRM hub only loads when opened. */ const CrmConsolePage = lazy(()=>import("./components/crm-console-page.js"));
|
|
33
33
|
/** The dashboard glance, split the same way: it loads where the slot mounts it. */ /** Where a form's fields save on a contact, drawn on the form's own page. */ const FormContactFieldsCard = lazy(()=>import("./components/form-contact-fields-card.js"));
|
|
34
34
|
const CrmTasksDueCard = lazy(()=>import("./components/crm-tasks-due-card.js"));
|
|
35
|
+
/**
|
|
36
|
+
* What a plan without the CRM sees where the glance card would be (AGL-3977):
|
|
37
|
+
* the suite named, with the plan that includes it. One card, not two — the
|
|
38
|
+
* tasks card has nothing to sell that this one does not.
|
|
39
|
+
*/ const CRM_GLANCE_LOCKED_PREVIEW = {
|
|
40
|
+
description: 'Follow every person your site captures as a lead, contact or deal, ' + 'with tasks and reports, from one place.'
|
|
41
|
+
};
|
|
35
42
|
/**
|
|
36
43
|
* CRM feature plugin (AGL-395, the hub since AGL-2595). Console-only — its
|
|
37
44
|
* records live in Firestore and have no canvas element, so there is no UI
|
|
@@ -141,8 +148,10 @@ const CrmTasksDueCard = lazy(()=>import("./components/crm-tasks-due-card.js"));
|
|
|
141
148
|
* card is four server-counted figures, each a link into the hub.
|
|
142
149
|
*
|
|
143
150
|
* Both name `features.crm`, as the extension does (AGL-2611, AGL-2851):
|
|
144
|
-
* the slot composes the two, so a plan without the CRM gets neither card
|
|
145
|
-
*
|
|
151
|
+
* the slot composes the two, so a plan without the CRM gets neither card.
|
|
152
|
+
* The glance card declares a `lockedPreview` (AGL-3977), so in its place
|
|
153
|
+
* the shell draws one locked card naming the plan that includes the CRM;
|
|
154
|
+
* the tasks card stays absent.
|
|
146
155
|
*
|
|
147
156
|
* The same two cards again on the organization's `orgDashboard` slot
|
|
148
157
|
* (AGL-2636) — the org's sites page — under the same flag, with the same
|
|
@@ -176,6 +185,7 @@ const CrmTasksDueCard = lazy(()=>import("./components/crm-tasks-due-card.js"));
|
|
|
176
185
|
widgetId: 'crm-glance',
|
|
177
186
|
title: 'CRM at a glance',
|
|
178
187
|
featureFlag: 'crm',
|
|
188
|
+
lockedPreview: CRM_GLANCE_LOCKED_PREVIEW,
|
|
179
189
|
Component: CrmGlanceCard
|
|
180
190
|
},
|
|
181
191
|
{
|
|
@@ -190,6 +200,7 @@ const CrmTasksDueCard = lazy(()=>import("./components/crm-tasks-due-card.js"));
|
|
|
190
200
|
widgetId: 'crm-glance',
|
|
191
201
|
title: 'CRM at a glance',
|
|
192
202
|
featureFlag: 'crm',
|
|
203
|
+
lockedPreview: CRM_GLANCE_LOCKED_PREVIEW,
|
|
193
204
|
Component: withCrmOrgMount(CrmGlanceCard)
|
|
194
205
|
}
|
|
195
206
|
],
|
package/src/lib/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { mdiCardAccountDetailsOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { CRM_CONSOLE_SECTIONS } from './components/crm-console-sections'\nimport { CrmGlanceCard } from './components/crm-glance-card'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerCrmTransferResourceUis } from './transfer/ui'\nimport { withCrmOrgMount } from './hooks/use-crm-org-mount'\nimport { registerCrmRecordCounts } from './model/crm-record-counts'\nimport { registerCrmRecordRoutes } from './model/crm-record-routes'\nimport { registerCrmRecordLists } from './model/crm-record-lists'\nimport { CRM_SEARCH_SOURCES } from './model/crm-search-sources'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { CRM_RECORD_ATTRIBUTION_ZONE } from './components/crm-attribution-zone'\nimport { CRM_RECORD_BOOKING_ZONE } from './components/crm-booking-zone'\n\n/** Code-split: the CRM hub only loads when opened. */\nconst CrmConsolePage = lazy(\n () => import('./components/crm-console-page'),\n)\n/** The dashboard glance, split the same way: it loads where the slot mounts it. */\n/** Where a form's fields save on a contact, drawn on the form's own page. */\nconst FormContactFieldsCard = lazy(\n () => import('./components/form-contact-fields-card'),\n)\n\nconst CrmTasksDueCard = lazy(\n () => import('./components/crm-tasks-due-card'),\n)\n\n/**\n * CRM feature plugin (AGL-395, the hub since AGL-2595). Console-only — its\n * records live in Firestore and have no canvas element, so there is no UI\n * bundle. The console half declares the CRM nav + hub through the\n * ConsoleExtension registry (release_crm gate via the nav tab); the\n * contacts section reads the `contactsPerHost` quota off the shell-passed\n * `org`.\n *\n * The plugin id is `crm` (AGL-2595). It was `contacts` while the surface was\n * one list; the id is persisted in every org's `enabledPlugins` and every\n * host's `disabledPlugins`, so the runtime read the old value as this one\n * until the stored documents had been rewritten, and the alias was retired\n * once that backfill reported nothing left (AGL-2614). The `/contacts`\n * address is a URL, not a stored id, and the nav item keeps redirecting it.\n */\nexport function registerCrmConsole(): void {\n // How the Import & export hub names email templates in a workspace package (AGL-3535).\n registerPluginTransferResourceUi('crm.email-templates', { label: 'Email templates' }, { pluginId: BUNDLE_ID })\n registerCrmRecordRoutes()\n // How the import wizard and the export dialog name the CRM's records (AGL-3527).\n registerCrmTransferResourceUis(BUNDLE_ID)\n // The CRM's records as another plugin's picker lists them (AGL-3080).\n registerCrmRecordLists()\n // The leads a site captured and the organization's contacts, counted for\n // another plugin's figure.\n registerCrmRecordCounts()\n registerPluginZone(\n {\n zone: CRM_RECORD_BOOKING_ZONE,\n label: 'Book a meeting from a CRM record',\n surface: 'console',\n // One control in a record header's row of actions, and one beside the\n // composer's fields: the page lays them out, so the zone adds nothing.\n layout: 'bare',\n description:\n 'In a record’s header and beside the one-to-one composer. A widget here offers the site’s bookable services and the link a visitor books one at; it is handed the site, the record and, from the composer, a way to insert the chosen link. It writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: CRM_RECORD_ATTRIBUTION_ZONE,\n label: 'Where a CRM record came from',\n surface: 'console',\n description:\n 'On a contact’s page and in a lead’s history. A widget here says which campaign or link brought the person; it is handed the site, what the record is and its id, and it writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'CRM',\n /*\n * WHO may open the CRM, declared so the shell enforces it.\n *\n * `navTabId` below is a release flag, not authorization: it says whether\n * the surface has shipped, and `FeatureGate` reads it as\n * `released || isStaff`. Nothing above this page decided who among a\n * workspace's members may read it, and the page is a full editor over\n * org-shared people data — names, addresses, order history, notes,\n * consent, and a CSV export of all of it.\n *\n * `data.manage` rather than a key of this plugin's own, because it is\n * the catalog key whose subject is exactly this data. The Firestore\n * rules place contacts in the org-shared data block with datasets and\n * gate writes on `canWriteOrgData()` — owner, admin, editor — which is\n * the population `data.manage` defaults to, so the console gate and the\n * rules agree on the built-in roles by construction. A custom role or a\n * per-member override then narrows the console further, and the rules\n * stay the boundary for the data itself.\n *\n * Minting a `contacts.*` key instead would advertise a control that does\n * not exist. Console contact writes are client-direct against the rules,\n * so a new key would have no server enforcement point to sit on, and a\n * permission a customer can untick that changes nothing is worse than\n * its absence.\n *\n * A SITE COLLABORATOR holding it reaches this page deliberately. They\n * are a real org member document, so this gate is the first thing that\n * has ever asked about them here — and the answer is yes, scoped: the\n * listener filters on `visibleTo`, the rules prove the same predicate\n * per document, and every field the page shows is read through the\n * viewing group's own facet. Refusing them outright would delete a\n * shipped capability rather than close a hole.\n */\n permission: 'data.manage',\n /*\n * WHAT the workspace must have bought (AGL-2851). The CRM is included\n * from Starter and a Free workspace has none of it, so the flag is\n * declared once, here, and the shell refuses every surface this\n * registers on a plan without it: the hub body at both mounts, every\n * section and record beneath it, drawn locked beside the upgrade notice,\n * and the dashboard cards. The nav tab stays, as every plan-locked tab\n * does, because it is the way to that notice. The CRM's routes, `/v1`\n * and the Firestore rules ask the same question where the data is\n * (`server/suite-gate.ts`).\n */\n featureFlag: 'crm',\n // What the console's search finds here, under the two gates above.\n searchSources: CRM_SEARCH_SOURCES,\n /*\n * Dashboard cards (AGL-2599, AGL-2604): the site dashboard's `hostDashboard`\n * slot composes this extension's permission over each, so a card appears\n * only where the CRM is enabled and only for a reader who may open it —\n * registered here rather than imported by the dashboard so the shell's\n * enablement and entitlement gate decides whether a site that never turned\n * the CRM on sees them, the rule every card on that row follows. The tasks\n * card is a glance at the reader's own overdue and due-today work and\n * renders nothing on a workspace that has never made a task; the glance\n * card is four server-counted figures, each a link into the hub.\n *\n * Both name `features.crm`, as the extension does (AGL-2611, AGL-2851):\n * the slot composes the two, so a plan without the CRM gets neither card\n * — absent, not upsold, the treatment every gated card gets.\n *\n * The same two cards again on the organization's `orgDashboard` slot\n * (AGL-2636) — the org's sites page — under the same flag, with the same\n * ids: the id names the card, and it is one card totaling the whole\n * organization there instead of one site. The shell hands that slot the\n * org mount as a prop, and `withCrmOrgMount` is what puts the mount's\n * provider around each card, since the sites page cannot import one.\n */\n widgets: [\n // Which contact field each of a form's fields saves to, on the form's\n // own page. The forms plugin hosts the zone and writes the form\n // document; what a contact's fields ARE is this plugin's, so the card\n // is too.\n {\n slot: 'formContactFields',\n widgetId: 'crm-form-contact-fields',\n title: 'Saves to contact fields',\n // A contact's custom fields are part of the suite, so a workspace\n // without it has none to map a form's field onto.\n featureFlag: 'crm',\n Component: FormContactFieldsCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: CrmTasksDueCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n Component: CrmGlanceCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: withCrmOrgMount(CrmTasksDueCard),\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n Component: withCrmOrgMount(CrmGlanceCard),\n },\n ],\n navItems: [\n {\n label: 'CRM',\n href: '/crm',\n // The address the surface had while it was one list. A link kept\n // from then — a bookmark, a docs page, an email — still opens the\n // hub rather than the shell's \"not available\" notice.\n legacyHrefs: ['/contacts'],\n // Sections as ROUTES (AGL-2595): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read and a\n // bare `/crm` lands on the first. Every section inherits this\n // item's `release_crm` gate.\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n },\n Component: CrmConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's CRM hub (AGL-2630), at `/[orgSlug]/crm`: the same\n * sections over every site at once, mounted with no site and the org's\n * sites as `orgMount`. Its listeners carry no scope clause, so the\n * shell's org route refuses a site collaborator on REACH before it asks\n * the `data.manage` declared above; a permission alone, which a\n * collaborator's role can carry, never opens it.\n *\n * It carries the SITE tab's id on purpose: `release_contacts` names\n * `nav-tab-contacts`, so one flag holds both halves of the CRM, and the\n * org half cannot ship while the site half is switched off. First among\n * the organization's plugin tabs, beside Data, the org's other shared\n * collection of records. The help opens the docs heading about this\n * mount. Its old address, `/[orgSlug]/contacts`, is a console redirect\n * (`consoleRedirects`), so it needs no `legacyHrefs` here.\n */\n orgNavItems: [\n {\n label: 'CRM',\n href: '/crm',\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n tabOrder: -100,\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n docsAnchor: '#at-the-organization-level',\n },\n Component: CrmConsolePage,\n },\n ],\n })\n}\n"],"names":["Aglyn","mdiCardAccountDetailsOutline","lazy","CRM_CONSOLE_SECTIONS","CrmGlanceCard","BUNDLE_ID","registerCrmTransferResourceUis","withCrmOrgMount","registerCrmRecordCounts","registerCrmRecordRoutes","registerCrmRecordLists","CRM_SEARCH_SOURCES","registerPluginZone","registerPluginTransferResourceUi","CRM_RECORD_ATTRIBUTION_ZONE","CRM_RECORD_BOOKING_ZONE","CrmConsolePage","FormContactFieldsCard","CrmTasksDueCard","registerCrmConsole","label","pluginId","zone","surface","layout","description","registerConsoleExtension","displayName","permission","featureFlag","searchSources","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","hostDashboard","orgDashboard","navItems","href","legacyHrefs","sections","navTabId","icon","path","header","docsTopic","orgNavItems","tabOrder","docsAnchor"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { mdiCardAccountDetailsOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { CRM_CONSOLE_SECTIONS } from './components/crm-console-sections'\nimport { CrmGlanceCard } from './components/crm-glance-card'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerCrmTransferResourceUis } from './transfer/ui'\nimport { withCrmOrgMount } from './hooks/use-crm-org-mount'\nimport { registerCrmRecordCounts } from './model/crm-record-counts'\nimport { registerCrmRecordRoutes } from './model/crm-record-routes'\nimport { registerCrmRecordLists } from './model/crm-record-lists'\nimport { CRM_SEARCH_SOURCES } from './model/crm-search-sources'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { CRM_RECORD_ATTRIBUTION_ZONE } from './components/crm-attribution-zone'\nimport { CRM_RECORD_BOOKING_ZONE } from './components/crm-booking-zone'\n\n/** Code-split: the CRM hub only loads when opened. */\nconst CrmConsolePage = lazy(\n () => import('./components/crm-console-page'),\n)\n/** The dashboard glance, split the same way: it loads where the slot mounts it. */\n/** Where a form's fields save on a contact, drawn on the form's own page. */\nconst FormContactFieldsCard = lazy(\n () => import('./components/form-contact-fields-card'),\n)\n\nconst CrmTasksDueCard = lazy(\n () => import('./components/crm-tasks-due-card'),\n)\n\n/**\n * What a plan without the CRM sees where the glance card would be (AGL-3977):\n * the suite named, with the plan that includes it. One card, not two — the\n * tasks card has nothing to sell that this one does not.\n */\nconst CRM_GLANCE_LOCKED_PREVIEW = {\n description:\n 'Follow every person your site captures as a lead, contact or deal, ' +\n 'with tasks and reports, from one place.',\n} as const\n\n/**\n * CRM feature plugin (AGL-395, the hub since AGL-2595). Console-only — its\n * records live in Firestore and have no canvas element, so there is no UI\n * bundle. The console half declares the CRM nav + hub through the\n * ConsoleExtension registry (release_crm gate via the nav tab); the\n * contacts section reads the `contactsPerHost` quota off the shell-passed\n * `org`.\n *\n * The plugin id is `crm` (AGL-2595). It was `contacts` while the surface was\n * one list; the id is persisted in every org's `enabledPlugins` and every\n * host's `disabledPlugins`, so the runtime read the old value as this one\n * until the stored documents had been rewritten, and the alias was retired\n * once that backfill reported nothing left (AGL-2614). The `/contacts`\n * address is a URL, not a stored id, and the nav item keeps redirecting it.\n */\nexport function registerCrmConsole(): void {\n // How the Import & export hub names email templates in a workspace package (AGL-3535).\n registerPluginTransferResourceUi('crm.email-templates', { label: 'Email templates' }, { pluginId: BUNDLE_ID })\n registerCrmRecordRoutes()\n // How the import wizard and the export dialog name the CRM's records (AGL-3527).\n registerCrmTransferResourceUis(BUNDLE_ID)\n // The CRM's records as another plugin's picker lists them (AGL-3080).\n registerCrmRecordLists()\n // The leads a site captured and the organization's contacts, counted for\n // another plugin's figure.\n registerCrmRecordCounts()\n registerPluginZone(\n {\n zone: CRM_RECORD_BOOKING_ZONE,\n label: 'Book a meeting from a CRM record',\n surface: 'console',\n // One control in a record header's row of actions, and one beside the\n // composer's fields: the page lays them out, so the zone adds nothing.\n layout: 'bare',\n description:\n 'In a record’s header and beside the one-to-one composer. A widget here offers the site’s bookable services and the link a visitor books one at; it is handed the site, the record and, from the composer, a way to insert the chosen link. It writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: CRM_RECORD_ATTRIBUTION_ZONE,\n label: 'Where a CRM record came from',\n surface: 'console',\n description:\n 'On a contact’s page and in a lead’s history. A widget here says which campaign or link brought the person; it is handed the site, what the record is and its id, and it writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'CRM',\n /*\n * WHO may open the CRM, declared so the shell enforces it.\n *\n * `navTabId` below is a release flag, not authorization: it says whether\n * the surface has shipped, and `FeatureGate` reads it as\n * `released || isStaff`. Nothing above this page decided who among a\n * workspace's members may read it, and the page is a full editor over\n * org-shared people data — names, addresses, order history, notes,\n * consent, and a CSV export of all of it.\n *\n * `data.manage` rather than a key of this plugin's own, because it is\n * the catalog key whose subject is exactly this data. The Firestore\n * rules place contacts in the org-shared data block with datasets and\n * gate writes on `canWriteOrgData()` — owner, admin, editor — which is\n * the population `data.manage` defaults to, so the console gate and the\n * rules agree on the built-in roles by construction. A custom role or a\n * per-member override then narrows the console further, and the rules\n * stay the boundary for the data itself.\n *\n * Minting a `contacts.*` key instead would advertise a control that does\n * not exist. Console contact writes are client-direct against the rules,\n * so a new key would have no server enforcement point to sit on, and a\n * permission a customer can untick that changes nothing is worse than\n * its absence.\n *\n * A SITE COLLABORATOR holding it reaches this page deliberately. They\n * are a real org member document, so this gate is the first thing that\n * has ever asked about them here — and the answer is yes, scoped: the\n * listener filters on `visibleTo`, the rules prove the same predicate\n * per document, and every field the page shows is read through the\n * viewing group's own facet. Refusing them outright would delete a\n * shipped capability rather than close a hole.\n */\n permission: 'data.manage',\n /*\n * WHAT the workspace must have bought (AGL-2851). The CRM is included\n * from Starter and a Free workspace has none of it, so the flag is\n * declared once, here, and the shell refuses every surface this\n * registers on a plan without it: the hub body at both mounts, every\n * section and record beneath it, drawn locked beside the upgrade notice,\n * and the dashboard cards. The nav tab stays, as every plan-locked tab\n * does, because it is the way to that notice. The CRM's routes, `/v1`\n * and the Firestore rules ask the same question where the data is\n * (`server/suite-gate.ts`).\n */\n featureFlag: 'crm',\n // What the console's search finds here, under the two gates above.\n searchSources: CRM_SEARCH_SOURCES,\n /*\n * Dashboard cards (AGL-2599, AGL-2604): the site dashboard's `hostDashboard`\n * slot composes this extension's permission over each, so a card appears\n * only where the CRM is enabled and only for a reader who may open it —\n * registered here rather than imported by the dashboard so the shell's\n * enablement and entitlement gate decides whether a site that never turned\n * the CRM on sees them, the rule every card on that row follows. The tasks\n * card is a glance at the reader's own overdue and due-today work and\n * renders nothing on a workspace that has never made a task; the glance\n * card is four server-counted figures, each a link into the hub.\n *\n * Both name `features.crm`, as the extension does (AGL-2611, AGL-2851):\n * the slot composes the two, so a plan without the CRM gets neither card.\n * The glance card declares a `lockedPreview` (AGL-3977), so in its place\n * the shell draws one locked card naming the plan that includes the CRM;\n * the tasks card stays absent.\n *\n * The same two cards again on the organization's `orgDashboard` slot\n * (AGL-2636) — the org's sites page — under the same flag, with the same\n * ids: the id names the card, and it is one card totaling the whole\n * organization there instead of one site. The shell hands that slot the\n * org mount as a prop, and `withCrmOrgMount` is what puts the mount's\n * provider around each card, since the sites page cannot import one.\n */\n widgets: [\n // Which contact field each of a form's fields saves to, on the form's\n // own page. The forms plugin hosts the zone and writes the form\n // document; what a contact's fields ARE is this plugin's, so the card\n // is too.\n {\n slot: 'formContactFields',\n widgetId: 'crm-form-contact-fields',\n title: 'Saves to contact fields',\n // A contact's custom fields are part of the suite, so a workspace\n // without it has none to map a form's field onto.\n featureFlag: 'crm',\n Component: FormContactFieldsCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: CrmTasksDueCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n lockedPreview: CRM_GLANCE_LOCKED_PREVIEW,\n Component: CrmGlanceCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: withCrmOrgMount(CrmTasksDueCard),\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n lockedPreview: CRM_GLANCE_LOCKED_PREVIEW,\n Component: withCrmOrgMount(CrmGlanceCard),\n },\n ],\n navItems: [\n {\n label: 'CRM',\n href: '/crm',\n // The address the surface had while it was one list. A link kept\n // from then — a bookmark, a docs page, an email — still opens the\n // hub rather than the shell's \"not available\" notice.\n legacyHrefs: ['/contacts'],\n // Sections as ROUTES (AGL-2595): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read and a\n // bare `/crm` lands on the first. Every section inherits this\n // item's `release_crm` gate.\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n },\n Component: CrmConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's CRM hub (AGL-2630), at `/[orgSlug]/crm`: the same\n * sections over every site at once, mounted with no site and the org's\n * sites as `orgMount`. Its listeners carry no scope clause, so the\n * shell's org route refuses a site collaborator on REACH before it asks\n * the `data.manage` declared above; a permission alone, which a\n * collaborator's role can carry, never opens it.\n *\n * It carries the SITE tab's id on purpose: `release_contacts` names\n * `nav-tab-contacts`, so one flag holds both halves of the CRM, and the\n * org half cannot ship while the site half is switched off. First among\n * the organization's plugin tabs, beside Data, the org's other shared\n * collection of records. The help opens the docs heading about this\n * mount. Its old address, `/[orgSlug]/contacts`, is a console redirect\n * (`consoleRedirects`), so it needs no `legacyHrefs` here.\n */\n orgNavItems: [\n {\n label: 'CRM',\n href: '/crm',\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n tabOrder: -100,\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n docsAnchor: '#at-the-organization-level',\n },\n Component: CrmConsolePage,\n },\n ],\n })\n}\n"],"names":["Aglyn","mdiCardAccountDetailsOutline","lazy","CRM_CONSOLE_SECTIONS","CrmGlanceCard","BUNDLE_ID","registerCrmTransferResourceUis","withCrmOrgMount","registerCrmRecordCounts","registerCrmRecordRoutes","registerCrmRecordLists","CRM_SEARCH_SOURCES","registerPluginZone","registerPluginTransferResourceUi","CRM_RECORD_ATTRIBUTION_ZONE","CRM_RECORD_BOOKING_ZONE","CrmConsolePage","FormContactFieldsCard","CrmTasksDueCard","CRM_GLANCE_LOCKED_PREVIEW","description","registerCrmConsole","label","pluginId","zone","surface","layout","registerConsoleExtension","displayName","permission","featureFlag","searchSources","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","hostDashboard","lockedPreview","orgDashboard","navItems","href","legacyHrefs","sections","navTabId","icon","path","header","docsTopic","orgNavItems","tabOrder","docsAnchor"],"mappings":"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"}
|
package/src/lib/server.js
CHANGED
|
@@ -53,7 +53,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
53
53
|
* write could not be trusted to do.
|
|
54
54
|
*/ import { registerDealFigureReaders } from "./server/deal-figures.js";
|
|
55
55
|
import { registerPeopleFigureReader } from "./server/people-figures.js";
|
|
56
|
-
import { CONTACT_ERASED_MESSAGE, contactFacetPath, composeContactName, CRM_COLLECTIONS, CRM_SALUTATION_PICKLIST, crmReadTokens, isContactLifecycleStage, isOrgWideMember, judgeCrmPicklistValue, normalizeAddress, normalizeContactEmail, normalizePhone, readContactFacet, registerPluginApiRoute, visibleToHost } from "@aglyn/aglyn/server";
|
|
56
|
+
import { CONTACT_ERASED_MESSAGE, CONTACT_FIELDS_MAX_PER_ORG, contactFacetPath, composeContactName, CRM_COLLECTIONS, CRM_SALUTATION_PICKLIST, crmReadTokens, isContactLifecycleStage, isOrgWideMember, judgeCrmPicklistValue, normalizeAddress, normalizeContactEmail, normalizePhone, readContactFacet, readCrmCustomInput, registerPluginApiRoute, visibleToHost } from "@aglyn/aglyn/server";
|
|
57
57
|
import { consentGroupForSite, firebaseAdmin, getOrgForHost, logHostActivity, memberHasOrgPermission, orgDataCollectionForHost, resolveOrgMembership, restampCrmListFieldsAt } from "@aglyn/tenant-data-admin";
|
|
58
58
|
import { isRefusedIdToken } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
59
59
|
import { emitHostEvent } from "@aglyn/tenant-runtime";
|
|
@@ -425,6 +425,16 @@ import { CRM_RECIPE_INSTALL_ROUTE, CRM_RECIPE_STATUS_ROUTE, crmRecipeInstallHand
|
|
|
425
425
|
// (AGL-3320): the opt-in covers the site's group only while it is the
|
|
426
426
|
// group's current one, and the capturing site alone otherwise.
|
|
427
427
|
const disclosedConsentGroup = typed(body['disclosedConsentGroup'], 64);
|
|
428
|
+
// The org's own contact fields, sent in this same call (AGL-3971): a
|
|
429
|
+
// value the create dropped would read as saved and be gone, and a second
|
|
430
|
+
// write after it could land half. Judged below against the definitions.
|
|
431
|
+
const rawCustom = body['custom'];
|
|
432
|
+
if (rawCustom !== undefined && (!rawCustom || typeof rawCustom !== 'object' || Array.isArray(rawCustom))) {
|
|
433
|
+
res.status(400).json({
|
|
434
|
+
error: 'Custom fields must be values keyed by field key.'
|
|
435
|
+
});
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
428
438
|
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
429
439
|
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
430
440
|
if (!idToken) {
|
|
@@ -478,6 +488,28 @@ import { CRM_RECIPE_INSTALL_ROUTE, CRM_RECIPE_STATUS_ROUTE, crmRecipeInstallHand
|
|
|
478
488
|
}
|
|
479
489
|
salutation = (_judged_value = judged.value) != null ? _judged_value : '';
|
|
480
490
|
}
|
|
491
|
+
/*
|
|
492
|
+
* The custom values, judged exactly as `crm/contact-update` judges them
|
|
493
|
+
* (AGL-3971): the org's definitions read only when the body carried a
|
|
494
|
+
* map, an unknown, retired or mistyped key refused with the definition
|
|
495
|
+
* reader's own sentence, and a currency amount stored as that field's
|
|
496
|
+
* minor units. Before any write, so a refusal leaves nothing behind.
|
|
497
|
+
*/ let custom;
|
|
498
|
+
if (rawCustom !== undefined) {
|
|
499
|
+
const definitions = await firebaseAdmin.app().firestore().collection('orgs').doc(resolved.orgId).collection(CRM_COLLECTIONS.contactFields).limit(CONTACT_FIELDS_MAX_PER_ORG).get();
|
|
500
|
+
const judged = readCrmCustomInput(rawCustom, definitions.docs.map((entry)=>entry.data()), 'contact');
|
|
501
|
+
if ('errors' in judged) {
|
|
502
|
+
var _Object_entries_;
|
|
503
|
+
const [field, message] = (_Object_entries_ = Object.entries(judged.errors)[0]) != null ? _Object_entries_ : [];
|
|
504
|
+
res.status(400).json(_extends({
|
|
505
|
+
error: message != null ? message : 'A custom value could not be saved.'
|
|
506
|
+
}, field ? {
|
|
507
|
+
field
|
|
508
|
+
} : {}));
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
custom = judged.values;
|
|
512
|
+
}
|
|
481
513
|
if (companyId) {
|
|
482
514
|
const group = await consentGroupForSite(hostId, resolved.org);
|
|
483
515
|
const readable = new Set(crmReadTokens(group));
|
|
@@ -529,6 +561,8 @@ import { CRM_RECIPE_INSTALL_ROUTE, CRM_RECIPE_STATUS_ROUTE, crmRecipeInstallHand
|
|
|
529
561
|
ownerUid
|
|
530
562
|
} : {}, rawStage && isContactLifecycleStage(rawStage) ? {
|
|
531
563
|
lifecycleStage: rawStage
|
|
564
|
+
} : {}, custom && Object.keys(custom).length ? {
|
|
565
|
+
custom
|
|
532
566
|
} : {})
|
|
533
567
|
}));
|
|
534
568
|
if ('refused' in result) {
|
package/src/lib/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/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/**\n * The Contacts plugin's SERVER half (AGL-2595).\n *\n * Contacts CRM v1 had none: every console write was client-direct against\n * the Firestore rules, and the one server path that creates a contact —\n * `upsertHostContact` — belongs to the capture doors, not to this plugin.\n * v2 needs a server for the things a browser must not do alone: an import\n * that dedupes forty thousand rows, a bulk action over a selection, an\n * auto-association of contacts to companies by domain. Each of those lands\n * as a `crm/<route>` handler registered here.\n *\n * `crm/ping` exists so the wiring is PROVEN rather than assumed:\n * `plugins.config.json` names this module's register function and the\n * `crm` API prefix, the generated server manifest loads this file, and the\n * console's `/api/[...pluginApi]` dispatcher reaches the handler. A plugin\n * whose first real route also had to be its first wiring test would have\n * two things to debug at once.\n *\n * The real routes are the writes with a side effect outside the document:\n * `crm/contact-stage` (AGL-2605) exists for the EVENT — `contactStageChanged`\n * can only be emitted by a server path that performed the write, so a record\n * page that moved the stage client-direct would move the person and tell no\n * automation; the task routes (AGL-2599) carry an assignee's notification and\n * the `taskCompleted` event. Every CRM file is imported through the\n * transfer framework (AGL-3527, AGL-3528, `transfer/`), whose writes go\n * through the same doors every other server path uses.\n * `crm/deal-stage` (AGL-2598) is the one writer of a deal's stage, won and lost,\n * because a stage change is what automations listen for (`server-deal-stage.ts`).\n * `crm/contacts-create` (AGL-2596) is a person typed into the console by a\n * member of the team — a server route because the dedupe against every\n * holder's rows and the audience band are judgments the browser cannot make.\n * `crm/contact-update` (AGL-2804) is every other write to a contact's facets:\n * the rules cannot tell one field of a holder's facet from another, so they\n * leave a client nothing there, and the plan is asked here about the fields\n * a save carries. `crm/contact-remove` (AGL-3338) is a holder letting a\n * contact go: the refusals the document holds are kept, which a client\n * write could not be trusted to do.\n */\n\nimport { registerDealFigureReaders } from './server/deal-figures'\nimport { registerPeopleFigureReader } from './server/people-figures'\nimport {\n CONTACT_ERASED_MESSAGE,\n contactFacetPath,\n composeContactName,\n CRM_COLLECTIONS,\n CRM_SALUTATION_PICKLIST,\n crmReadTokens,\n isContactLifecycleStage,\n isOrgWideMember,\n judgeCrmPicklistValue,\n normalizeAddress,\n normalizeContactEmail,\n normalizePhone,\n type AglynPostalAddress,\n type PluginApiHandler,\n type PluginApiRequest,\n readContactFacet,\n registerPluginApiRoute,\n visibleToHost,\n} from '@aglyn/aglyn/server'\nimport {\n consentGroupForSite,\n firebaseAdmin,\n getOrgForHost,\n logHostActivity,\n memberHasOrgPermission,\n orgDataCollectionForHost,\n resolveOrgMembership,\n restampCrmListFieldsAt,\n} from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { emitHostEvent } from '@aglyn/tenant-runtime'\nimport { captureHostContact } from './server/capture-host-contact'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { CRM_API_ROUTES } from './constants/api-routes'\nimport { registerCrmServerDeclarations } from './declarations.server'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { CRM_NEXT_ACTIVITY_ROUTE } from './model/next-activity'\nimport { CRM_LIST_FIELDS_ROUTE } from './model/list-fields'\nimport { CRM_LEAD_SOURCE_VALUES_ROUTE, CRM_PICKLIST_VALUES_ROUTE } from './model/picklist-values'\nimport { CRM_TASK_ROUTES } from './model/task-routes'\nimport { crmNextActivityHandler } from './server/next-activity-routes'\nimport { crmListFieldsHandler } from './server/list-fields-routes'\nimport { crmLeadSourceValuesHandler, crmPicklistValuesHandler } from './server/picklist-values'\nimport { registerCrmRecordEmailStateWriter } from './server/record-email-state'\nimport { registerCrmRecordTimelineWriter } from './server/record-timeline'\nimport { crmTaskCompleteHandler, crmTaskSaveHandler } from './server/task-routes'\nimport { crmDealStageHandler } from './server-deal-stage'\nimport { crmEmailSendHandler } from './server/email-send'\nimport { CRM_EMAIL_CHECK_ROUTE, crmEmailCheckHandler } from './server/email-check'\nimport { leadConvertHandler } from './server/lead-convert'\nimport { leadCreateHandler } from './server/lead-create'\nimport {\n CONTACT_EMAIL_HISTORY_ROUTE,\n contactEmailHistoryHandler,\n} from './server/contact-email-history'\nimport { CONTACTS_MERGE_ROUTE, contactsMergeHandler } from './server/contacts-merge'\nimport { CRM_ERASE_PERSON_ROUTE, crmErasePersonHandler } from './server/erase-person'\nimport { CRM_ORG_ACTIVITY_ROUTE, crmOrgActivityHandler } from './server/org-activity'\nimport { CRM_INBOUND_ADDRESS_ROUTE, crmInboundAddressHandler } from './server/inbound-address'\nimport { crmCompanyDeleteHandler } from './server/company-delete'\nimport { crmSharingHandler } from './server/crm-sharing'\nimport { CRM_SHARING_ROUTE } from './model/crm-sharing'\nimport { CONTACT_PHONE_REFUSAL, normalizeTags, typed } from './server/contact-profile'\nimport { crmContactUpdateHandler, readContactCreateProfile } from './server/contact-update'\nimport { readCrmPicklist } from './server/read-picklist'\nimport { crmContactRemoveHandler } from './server/contact-remove'\nimport {\n CRM_EMAIL_TEMPLATE_DUPLICATE_ROUTE,\n crmEmailTemplateDuplicateHandler,\n} from './server/email-template-duplicate'\nimport { registerCrmRecordFactsReaders } from './server/record-facts'\nimport { crmSuiteRefusal } from './server/suite-gate'\nimport {\n CRM_RECIPE_INSTALL_ROUTE,\n CRM_RECIPE_STATUS_ROUTE,\n crmRecipeInstallHandler,\n crmRecipeStatusHandler,\n} from './server/recipe-routes'\n\n/**\n * `GET /api/crm/ping` → `{ ok: true, plugin: 'crm' }`.\n *\n * No auth, no host, no data: it answers whether the plugin's server bundle\n * was loaded and its routes registered, which is a fact about the process\n * and not about any org. Anything that reads a document goes behind the\n * same session and role checks the other plugins' console routes use.\n */\nexport const crmPingHandler: PluginApiHandler = (req, res) => {\n if (req.method !== 'GET') {\n res.setHeader('Allow', 'GET')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n res.status(200).json({ ok: true, plugin: BUNDLE_ID })\n}\n\ntype Refusal = { ok: false; status: number; error: string }\n\n/**\n * Who is calling, and whether they may edit this site's CRM.\n *\n * The same check the inbox, bookings and email console routes make: a\n * bearer ID token, verified, and an `admin` or `editor` role on the host\n * document. A contact is a site's business record, so the site's own role\n * map is the right authority; org-wide reach is not required, because a\n * site editor who can open the record in the console can already write\n * every other field on it client-direct.\n */\nasync function authorizeSiteEditor(\n req: PluginApiRequest,\n hostId: string,\n): Promise<{ ok: true; uid: string; email: string | null } | Refusal> {\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return { ok: false, status: 401, error: 'Unauthenticated' }\n let uid: string\n let email: string | null\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n uid = decoded.uid\n email = typeof decoded.email === 'string' ? decoded.email : null\n } catch (error) {\n // A refused credential is the caller's 401; a failure to check one is\n // ours and keeps a 5xx (AGL-2852).\n if (isRefusedIdToken(error)) return { ok: false, status: 401, error: 'Unauthenticated' }\n console.error('[crm] the site editor could not be verified', error)\n return { ok: false, status: 500, error: 'The sign-in could not be checked. Try again.' }\n }\n const hostSnapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .get()\n if (!hostSnapshot.exists) {\n return { ok: false, status: 404, error: 'Unknown site' }\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return { ok: false, status: 403, error: 'Not a site admin or editor' }\n }\n return { ok: true, uid, email }\n}\n\n/**\n * `POST /api/crm/contact-stage` with `{ hostId, contactId, lifecycleStage }`.\n *\n * Writes the stage into THIS site's facet on the contact — a dotted\n * `update()`, never a top-level field, because the row is shared by every\n * site in the org and a stage is one holder's reading of the person — and\n * then announces `contactStageChanged` with the stage it replaced.\n *\n * A stage set to what it already is writes nothing and announces nothing:\n * the response says `changed: false`, and no automation listening for a\n * change hears one that did not happen.\n *\n * `lifecycleStage: null` CLEARS the stage (AGL-2804). A facet is the\n * server's to write, so \"not placed yet\" comes through the route that owns\n * the stage rather than as a client-direct delete, and it is refused to a\n * plan without the suite the way a move is. There is no event for \"no\n * stage\", so a clear announces nothing; an empty string names no stage and\n * is refused like any other the list does not have.\n *\n * The contact is looked up by id and then checked against `visibleTo`,\n * because the Admin SDK evaluates no rules: a caller who can edit site A\n * must not be able to restage a contact only site B holds by guessing its\n * id, and the scoped query the list uses is not available to a `doc()` read.\n */\nexport const contactStageHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const hostId = String(req.body?.hostId ?? '').trim()\n const contactId = String(req.body?.contactId ?? '').trim()\n const requested: unknown = req.body?.lifecycleStage\n if (!hostId || !contactId) {\n res.status(400).json({ error: 'Missing hostId or contactId' })\n return\n }\n // `null` clears the stage; anything else has to be one of the list's.\n const clearing = requested === null\n const lifecycleStage = clearing\n ? ''\n : isContactLifecycleStage(requested)\n ? requested\n : null\n if (lifecycleStage === null) {\n res.status(400).json({ error: 'Pick a lifecycle stage' })\n return\n }\n const caller = await authorizeSiteEditor(req, hostId)\n if (caller.ok === false) {\n res.status(caller.status).json({ error: caller.error })\n return\n }\n try {\n // A lifecycle stage is the suite's (AGL-2787) — see `suite-gate.ts`.\n const owner = await getOrgForHost(hostId)\n if (!owner) {\n res.status(404).json({ error: 'Unknown site' })\n return\n }\n const suite = crmSuiteRefusal(owner.org, \"Moving a contact's lifecycle stage\")\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n const contactsRef = await orgDataCollectionForHost(hostId, 'contacts')\n const snapshot = await contactsRef.doc(contactId).get()\n if (!snapshot.exists || !visibleToHost(snapshot.get('visibleTo'), hostId)) {\n res.status(404).json({ error: 'Unknown contact' })\n return\n }\n const group = await consentGroupForSite(hostId)\n const facet = readContactFacet(\n snapshot.data() as Record<string, unknown>,\n group.groupId,\n )\n const previousStage = facet.lifecycleStage ?? ''\n if (previousStage === lifecycleStage) {\n res\n .status(200)\n .json({ ok: true, changed: false, lifecycleStage, previousStage })\n return\n }\n await snapshot.ref.update({\n [contactFacetPath(group.groupId, 'lifecycleStage')]: clearing\n ? FieldValue.delete()\n : lifecycleStage,\n updatedAt: FieldValue.serverTimestamp(),\n })\n // The stage is what the Contacts list filters by (AGL-3321).\n await restampCrmListFieldsAt(snapshot.ref, 'contacts')\n // Awaited, not floated: a serverless response ending cancels in-flight\n // work, and the event is the reason this route exists. A clear has none.\n if (!clearing) {\n await emitHostEvent(\n hostId,\n 'contactStageChanged',\n {\n contactId,\n email: String(snapshot.get('email') ?? ''),\n lifecycleStage,\n previousStage,\n },\n { actor: { kind: 'member', uid: caller.uid, email: caller.email } },\n )\n }\n res\n .status(200)\n .json({ ok: true, changed: true, lifecycleStage, previousStage })\n } catch (error) {\n console.error('[crm] contact-stage failed', hostId, contactId, error)\n res.status(500).json({ error: 'The stage could not be changed' })\n }\n}\n\n/**\n * What the create route says when the band refused (AGL-2596).\n *\n * The contacts list's own alert, in the past tense: the list says new\n * visitors are \"no longer captured\", and this says the one contact the\n * reader just tried to add was not. The remedy is the same sentence in both\n * places, so a reader who sees one and then the other is told one thing.\n */\nexport const CONTACT_BAND_FULL_MESSAGE =\n 'Contact limit reached — this contact was not added. Upgrade in Billing ' +\n 'to keep collecting.'\n\n/**\n * `POST /api/crm/contacts-create` — a person added by hand (AGL-2596).\n *\n * Body: `{ hostId, email, name?, phone?, jobTitle?, companyName?,\n * companyId?, address?, ownerUid?, lifecycleStage?, tags?,\n * marketingConsent?, disclosedConsentGroup? }`, plus any of Salesforce's\n * standard contact fields `crm/contact-update` reads but the reports-to\n * (`CONTACT_CREATE_PROFILE_FIELDS`, AGL-3515) — read by the same rules, the\n * salutation judged against the org's list, and the holder's name composed\n * from a first and last name when either is given. Answers\n * `{ contactId, created }`: `created` is\n * false when the address already belonged to somebody, in which case what\n * was typed MERGES into the existing row — the dedupe the shared address\n * book exists for, and the reason a second \"create\" of one person is not\n * an error.\n *\n * ## Who may call it\n *\n * The caller's ID token, then `data.manage` on the site's org — the same key\n * the console surface itself is gated on, resolved through the same\n * three-layer permission read the members route uses. A scoped member is\n * admitted only for a site they reach: the rules would refuse a browser\n * write outside their tokens, and a server route that admitted it would be\n * a way round the rules rather than a service in front of them.\n *\n * ## What it refuses, and how\n *\n * A malformed email or phone number is a 400 with a sentence the drawer can\n * show under the field. The band is a 409 carrying the list's own wording:\n * it is not a bad request and not a server fault, it is the plan saying no,\n * and the drawer relays the sentence rather than inventing one.\n *\n * ## The company (AGL-2613)\n *\n * `companyId` is the picker's choice, and it is checked before anything is\n * written: the company has to exist and be visible to the capturing site's\n * scope, or a caller could file a person under a record they cannot open —\n * the same refusal the lead conversion makes. It reaches the upsert as\n * `facet.companyId`, which is where the link is kept in step with its mirror\n * and the company's contacts count; a merge onto somebody already at another\n * company is a MOVE there, not a second link. The stored company's own name\n * is what is echoed, over whatever the client sent, because the name on the\n * record is the company's and not the form's.\n *\n * ## What it writes beyond the upsert\n *\n * `upsertHostContact` takes the profile — phone, title, address, owner,\n * stage, company — and writes it into the capturing group's facet. The two\n * things it does not take are written here by DOTTED path afterwards: the\n * tags, which union into whatever the person already carried, and the\n * company name — the picked company's, or free text for a person filed\n * under a name no company record carries yet. Dotted paths because this is\n * an `update()`, and only an update reads a dot as a path — a nested object\n * here would replace the facet map and take every other holder's records\n * with it.\n */\nexport const crmContactsCreateHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n\n const body = (req.body ?? {}) as Record<string, unknown>\n const hostId = typed(body['hostId'], 128)\n if (!hostId) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const email = normalizeContactEmail(body['email'])\n if (!email) {\n res.status(400).json({ error: 'Enter a valid email address.' })\n return\n }\n const rawPhone = typed(body['phone'], 40)\n const phone = rawPhone ? normalizePhone(rawPhone) : null\n if (rawPhone && !phone) {\n res.status(400).json({ error: CONTACT_PHONE_REFUSAL })\n return\n }\n const rawStage = typed(body['lifecycleStage'], 40)\n if (rawStage && !isContactLifecycleStage(rawStage)) {\n res.status(400).json({ error: 'Unknown lifecycle stage.' })\n return\n }\n const profile = readContactCreateProfile(body)\n if (profile.ok === false) {\n res.status(400).json({ error: profile.error })\n return\n }\n const extras = profile.fields\n const name = typed(body['name'], 120)\n const jobTitle = typed(body['jobTitle'], 120)\n let companyName = typed(body['companyName'], 120)\n const companyId = typed(body['companyId'], 128)\n const ownerUid = typed(body['ownerUid'], 128)\n const address =\n body['address'] && typeof body['address'] === 'object'\n ? normalizeAddress(body['address'] as AglynPostalAddress)\n : null\n const tags = normalizeTags(body['tags'])\n const marketingConsent = body['marketingConsent'] === true\n // The key of the consent-group sentence the drawer showed beside the box\n // (AGL-3320): the opt-in covers the site's group only while it is the\n // group's current one, and the capturing site alone otherwise.\n const disclosedConsentGroup = typed(body['disclosedConsentGroup'], 64)\n\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) {\n res.status(401).json({ error: 'Unauthenticated' })\n return\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const resolved = await getOrgForHost(hostId)\n if (!resolved) {\n res.status(404).json({ error: 'Unknown site' })\n return\n }\n const isStaff = decoded['staff'] === true\n if (!isStaff) {\n const membership = await resolveOrgMembership(decoded.uid, resolved.orgId)\n const member = membership?.member ?? null\n const reaches =\n isOrgWideMember(member) || Boolean(member?.hostAccess?.[hostId])\n const allowed =\n member &&\n reaches &&\n (await memberHasOrgPermission(resolved.orgId, member, 'data.manage'))\n if (!allowed) {\n res.status(403).json({\n error: 'Adding contacts requires the data.manage permission on this site',\n })\n return\n }\n }\n\n // A person typed in by the team is the suite's; the capture doors that\n // fill a Free workspace's contacts do not come through here (AGL-2787).\n const suite = crmSuiteRefusal(resolved.org, 'Adding a contact by hand')\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n\n // The salutation, judged against the org's list (AGL-3515).\n let salutation = ''\n if (extras.salutation) {\n const judged = judgeCrmPicklistValue(\n CRM_SALUTATION_PICKLIST,\n await readCrmPicklist(\n firebaseAdmin.app().firestore(),\n resolved.orgId,\n CRM_SALUTATION_PICKLIST,\n ),\n extras.salutation,\n )\n if (judged.ok === false) {\n res.status(400).json({ error: judged.error })\n return\n }\n salutation = judged.value ?? ''\n }\n\n if (companyId) {\n const group = await consentGroupForSite(\n hostId,\n resolved.org as Record<string, unknown>,\n )\n const readable = new Set<string>(crmReadTokens(group))\n const companySnapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(resolved.orgId)\n .collection(CRM_COLLECTIONS.companies)\n .doc(companyId)\n .get()\n const tokens: unknown = companySnapshot.exists\n ? companySnapshot.get('visibleTo')\n : undefined\n const visible =\n Array.isArray(tokens) && tokens.some((token) => readable.has(String(token)))\n if (!visible) {\n res.status(404).json({ error: 'Unknown company' })\n return\n }\n companyName = typed(companySnapshot.get('name'), 120) || companyName\n }\n\n const result = await captureHostContact({\n hostId,\n email,\n ...(name ? { name } : {}),\n source: 'manual',\n interaction: { summary: 'Added by hand' },\n actor: {\n kind: 'member',\n uid: decoded.uid,\n email: typeof decoded.email === 'string' ? decoded.email : null,\n },\n marketingConsent,\n ...(marketingConsent && disclosedConsentGroup\n ? { disclosedConsentGroup }\n : {}),\n facet: {\n ...(phone ? { phone } : {}),\n ...(jobTitle ? { jobTitle } : {}),\n // Salesforce's standard fields (AGL-3515); the capture composes the\n // holder's name from the first and last names.\n ...Object.fromEntries(\n Object.entries({ ...extras, salutation }).filter(\n ([key, value]) =>\n key !== 'doNotCall' && key !== 'otherAddress' && typeof value === 'string' && value,\n ),\n ),\n ...(extras.otherAddress ? { otherAddress: extras.otherAddress } : {}),\n ...(extras.doNotCall ? { doNotCall: true } : {}),\n ...(companyId ? { companyId } : {}),\n ...(address ? { address } : {}),\n ...(ownerUid ? { ownerUid } : {}),\n ...(rawStage && isContactLifecycleStage(rawStage)\n ? { lifecycleStage: rawStage }\n : {}),\n },\n })\n\n if ('refused' in result) {\n if (result.refused === 'band') {\n res.status(409).json({ error: CONTACT_BAND_FULL_MESSAGE, reason: 'band' })\n return\n }\n if (result.refused === 'invalid-email') {\n res.status(400).json({ error: 'Enter a valid email address.' })\n return\n }\n // The person was erased from this workspace (AGL-2623). A conflict,\n // like the band: the request was well-formed and the answer is a\n // standing fact about the address, not a fault in the call.\n if (result.refused === 'erased') {\n res.status(409).json({ error: CONTACT_ERASED_MESSAGE, reason: 'erased' })\n return\n }\n res.status(500).json({ error: 'The contact could not be saved.' })\n return\n }\n\n if (tags.length || companyName) {\n const group = await consentGroupForSite(\n hostId,\n resolved.org as Record<string, unknown>,\n )\n const contacts = await orgDataCollectionForHost(hostId, 'contacts')\n await contacts.doc(result.contactId).update({\n ...(tags.length\n ? {\n [contactFacetPath(group.groupId, 'tags')]: FieldValue.arrayUnion(\n ...tags,\n ),\n }\n : {}),\n ...(companyName\n ? {\n [contactFacetPath(group.groupId, 'companyName')]: companyName,\n // The search echo — see `HostContact.companyName`.\n companyName,\n }\n : {}),\n updatedAt: FieldValue.serverTimestamp(),\n })\n // The tags are what the Contacts list filters by (AGL-3321).\n await restampCrmListFieldsAt(contacts.doc(result.contactId), 'contacts')\n }\n\n /*\n * The audit line, written HERE rather than by the console (AGL-2622):\n * a route that verified the caller and performed the write is the one\n * writer that cannot record an act that did not happen, which is the\n * reason `check-activity-coverage.mjs` counts this file. A merge into\n * a person the org already held is said to be one — the row was\n * updated, not added — so the feed cannot claim two people for one.\n */\n await logHostActivity(\n hostId,\n { uid: decoded.uid, email: decoded.email ?? null },\n result.created ? 'Added contact' : 'Updated contact',\n {\n type: 'contact',\n id: result.contactId,\n name: name || composeContactName(extras.firstName, extras.lastName) || email,\n },\n )\n\n res\n .status(result.created ? 201 : 200)\n .json({ contactId: result.contactId, created: result.created })\n } catch (error) {\n console.error('[crm] contact create failed', error)\n res.status(500).json({ error: 'The contact could not be saved.' })\n }\n}\n\n/** Console API registration, named in `plugins.config.json` as `consoleApi`. */\nexport function registerCrmConsoleApi(): void {\n // The pipeline and the deals closed, as figure tables (AGL-3603), for the\n // AI plugin's insights to read by id; the console runs insight jobs.\n registerDealFigureReaders(() => firebaseAdmin.app().firestore())\n // And the leads and contacts gained over a window (AGL-3867), for the\n // weekly summary and for insights.\n registerPeopleFigureReader(() => firebaseAdmin.app().firestore())\n /*\n * The contact-capture writer, again (AGL-3080). Both apps run it at boot\n * from their generated server-declarations manifest, and this is the\n * backstop for a process whose boot did not: the first CRM door to load\n * registers it. Registering twice replaces in place.\n */\n registerCrmServerDeclarations()\n registerPluginApiRoute(CRM_API_ROUTES.ping, crmPingHandler)\n registerPluginApiRoute(CRM_API_ROUTES.contactStage, contactStageHandler)\n // Every other console write to a contact's facets (AGL-2804): the rules\n // cannot tell one facet field from another, so the plan is asked here.\n registerPluginApiRoute(CRM_API_ROUTES.contactUpdate, crmContactUpdateHandler)\n registerPluginApiRoute(CRM_API_ROUTES.contactRemove, crmContactRemoveHandler)\n // A company's contacts unlinked, then the company (AGL-2804): the unlink\n // clears a facet, which is the server's to write.\n registerPluginApiRoute(CRM_API_ROUTES.companyDelete, crmCompanyDeleteHandler)\n // Tasks (AGL-2599): the two writes with a side effect outside the document\n // — an assignee's notification, and the `taskCompleted` host event.\n registerPluginApiRoute(CRM_TASK_ROUTES.save, crmTaskSaveHandler)\n registerPluginApiRoute(CRM_TASK_ROUTES.complete, crmTaskCompleteHandler)\n // A client-direct task write's door to `nextTaskAtMs` (AGL-2661).\n registerPluginApiRoute(CRM_NEXT_ACTIVITY_ROUTE, crmNextActivityHandler)\n // What a client-direct write the browser cannot follow up asks for: the\n // fields the CRM lists query, restamped from the record (AGL-3321).\n registerPluginApiRoute(CRM_LIST_FIELDS_ROUTE, crmListFieldsHandler)\n // Sharing records with other sites (AGL-3336): a manager's share and\n // unshare, the org's sharing rules and their recompute, and the rules'\n // re-evaluation a client-direct write owes.\n registerPluginApiRoute(CRM_SHARING_ROUTE, crmSharingHandler)\n // A picklist value renamed or deleted (AGL-3298, AGL-3510): the list and\n // every record holding the old label, in one request. The lead source's\n // own address stays registered for a request that names no picklist.\n registerPluginApiRoute(CRM_PICKLIST_VALUES_ROUTE, crmPicklistValuesHandler)\n registerPluginApiRoute(CRM_LEAD_SOURCE_VALUES_ROUTE, crmLeadSourceValuesHandler)\n // The one writer of a deal's stage, won and lost (AGL-2598): the browser\n // could write the field, but only a server can emit the event an\n // automation listens for.\n registerPluginApiRoute('crm/deal-stage', crmDealStageHandler)\n // A person typed into the console (AGL-2596): the capture doors' own\n // function behind a session check, so the dedupe and the band are judged\n // where every other door judges them.\n registerPluginApiRoute('crm/contacts-create', crmContactsCreateHandler)\n // A lead becomes a contact, a company and a deal (AGL-2608) — the one CRM\n // write a browser cannot make alone, because only the server may create a\n // contact through the dedupe-and-meter door.\n registerPluginApiRoute('crm/lead-convert', leadConvertHandler)\n // A lead entered by hand (AGL-3231): the New lead drawer's route.\n registerPluginApiRoute('crm/leads-create', leadCreateHandler)\n // The one READ behind a route (AGL-2616): the per-recipient delivery log\n // is closed to clients, so a contact's campaign mail is projected here.\n registerPluginApiRoute(CONTACT_EMAIL_HISTORY_ROUTE, contactEmailHistoryHandler)\n // One email to one person from their record (AGL-2615): the recipient is\n // read off the record, the daily cap and both suppression lists are\n // judged, and the message leaves on the site's sending identity.\n registerPluginApiRoute(CRM_API_ROUTES.emailSend, crmEmailSendHandler)\n // What the platform knows about one address before somebody writes to it\n // (AGL-3328): the gateway chip on a record page, and the composer's\n // warning about a domain with no mail server or a gateway holding this\n // site's mail. DNS and the ledger are the server's.\n registerPluginApiRoute(CRM_EMAIL_CHECK_ROUTE, crmEmailCheckHandler)\n // Two records for one person become one (AGL-2625): the repoint of every\n // row naming the merged record and the transaction over both documents\n // are the server's, and the address index it writes is closed to clients.\n registerPluginApiRoute(CONTACTS_MERGE_ROUTE, contactsMergeHandler)\n // Files a person's privacy erasure for the daily job (AGL-2623);\n // workspace admins only.\n registerPluginApiRoute(CRM_ERASE_PERSON_ROUTE, crmErasePersonHandler)\n // One line in the organization's activity feed for an act the org-level\n // hub performed client-direct (AGL-2634): the feed is closed to clients,\n // so the bulk bars' lines come through here.\n registerPluginApiRoute(CRM_ORG_ACTIVITY_ROUTE, crmOrgActivityHandler)\n // A recipe's action written into one site's automations from the org\n // hub (AGL-2639), stamped and deduplicated by the server, and the stamps\n // read back per site so the hub can say which sites carry which recipe.\n registerPluginApiRoute(CRM_RECIPE_INSTALL_ROUTE, crmRecipeInstallHandler)\n registerPluginApiRoute(CRM_RECIPE_STATUS_ROUTE, crmRecipeStatusHandler)\n // The workspace's email capture address (AGL-2657): the token is minted\n // and rotated here, behind the CRM's own gate, and the org document that\n // carries it is closed to every client.\n registerPluginApiRoute(CRM_INBOUND_ADDRESS_ROUTE, crmInboundAddressHandler)\n /*\n * The three doors a MACHINE calls (AGL-3080), at the paths they have\n * always answered on: the scheduler's daily digest and hourly task\n * reminders, on the cron secret, and the mail provider's capture webhook,\n * on its signature. None names a site or a member, so each is registered\n * as a machine's route — the dispatcher leaves the plugin's release and\n * enablement gates and its write limit to it — and each judges the CRM's\n * plan and flag for every organization it resolves. Loaded with the\n * first call, not with the surface.\n */\n registerPluginApiRoute(\n CRM_API_ROUTES.dailyDigest,\n {\n web: async (request) =>\n (await import('./server/daily-digest-route')).crmDailyDigestRoute(request),\n },\n { machine: true },\n )\n registerPluginApiRoute(\n CRM_API_ROUTES.taskReminders,\n {\n web: async (request) =>\n (await import('./server/task-reminders-route')).crmTaskRemindersRoute(request),\n },\n { machine: true },\n )\n registerPluginApiRoute(\n CRM_API_ROUTES.inbound,\n {\n web: async (request) => (await import('./server/inbound-route')).crmInboundRoute(request),\n },\n { machine: true },\n )\n registerPluginApiRoute(\n CRM_EMAIL_TEMPLATE_DUPLICATE_ROUTE,\n crmEmailTemplateDuplicateHandler,\n )\n // The facts of a contact, company, deal or lead, and an import's field\n // catalog, for another plugin in this process (AGL-2917): read on the core's\n // record-facts seam under the rules every CRM door applies, and written\n // nowhere.\n registerCrmRecordFactsReaders()\n // The CRM as the workspace's record system on the core's record-timeline\n // seam (AGL-2981): another plugin files an email, a reply or a task on a\n // record through it, under the CRM's own scope, dedupe and ceiling.\n registerCrmRecordTimelineWriter()\n // And on the record email-state seam (AGL-3245): a sender that filed a\n // verdict on an address — a bounce, a complaint, an unsubscribe, a\n // do-not-contact mark — stamps the lead and the contact through it.\n registerCrmRecordEmailStateWriter()\n}\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { crmPlanEntitlements } from './plan-entitlements'\n"],"names":["registerDealFigureReaders","registerPeopleFigureReader","CONTACT_ERASED_MESSAGE","contactFacetPath","composeContactName","CRM_COLLECTIONS","CRM_SALUTATION_PICKLIST","crmReadTokens","isContactLifecycleStage","isOrgWideMember","judgeCrmPicklistValue","normalizeAddress","normalizeContactEmail","normalizePhone","readContactFacet","registerPluginApiRoute","visibleToHost","consentGroupForSite","firebaseAdmin","getOrgForHost","logHostActivity","memberHasOrgPermission","orgDataCollectionForHost","resolveOrgMembership","restampCrmListFieldsAt","isRefusedIdToken","emitHostEvent","captureHostContact","FieldValue","CRM_API_ROUTES","registerCrmServerDeclarations","BUNDLE_ID","CRM_NEXT_ACTIVITY_ROUTE","CRM_LIST_FIELDS_ROUTE","CRM_LEAD_SOURCE_VALUES_ROUTE","CRM_PICKLIST_VALUES_ROUTE","CRM_TASK_ROUTES","crmNextActivityHandler","crmListFieldsHandler","crmLeadSourceValuesHandler","crmPicklistValuesHandler","registerCrmRecordEmailStateWriter","registerCrmRecordTimelineWriter","crmTaskCompleteHandler","crmTaskSaveHandler","crmDealStageHandler","crmEmailSendHandler","CRM_EMAIL_CHECK_ROUTE","crmEmailCheckHandler","leadConvertHandler","leadCreateHandler","CONTACT_EMAIL_HISTORY_ROUTE","contactEmailHistoryHandler","CONTACTS_MERGE_ROUTE","contactsMergeHandler","CRM_ERASE_PERSON_ROUTE","crmErasePersonHandler","CRM_ORG_ACTIVITY_ROUTE","crmOrgActivityHandler","CRM_INBOUND_ADDRESS_ROUTE","crmInboundAddressHandler","crmCompanyDeleteHandler","crmSharingHandler","CRM_SHARING_ROUTE","CONTACT_PHONE_REFUSAL","normalizeTags","typed","crmContactUpdateHandler","readContactCreateProfile","readCrmPicklist","crmContactRemoveHandler","CRM_EMAIL_TEMPLATE_DUPLICATE_ROUTE","crmEmailTemplateDuplicateHandler","registerCrmRecordFactsReaders","crmSuiteRefusal","CRM_RECIPE_INSTALL_ROUTE","CRM_RECIPE_STATUS_ROUTE","crmRecipeInstallHandler","crmRecipeStatusHandler","crmPingHandler","req","res","method","setHeader","status","json","error","ok","plugin","authorizeSiteEditor","hostId","hostSnapshot","authorization","String","headers","idToken","startsWith","slice","length","undefined","uid","email","decoded","app","auth","verifyIdToken","console","firestore","collection","doc","get","exists","memberRole","contactStageHandler","body","trim","contactId","requested","lifecycleStage","clearing","caller","facet","owner","suite","org","contactsRef","snapshot","group","data","groupId","previousStage","changed","ref","update","delete","updatedAt","serverTimestamp","actor","kind","CONTACT_BAND_FULL_MESSAGE","crmContactsCreateHandler","rawPhone","phone","rawStage","profile","extras","fields","name","jobTitle","companyName","companyId","ownerUid","address","tags","marketingConsent","disclosedConsentGroup","resolved","isStaff","member","membership","orgId","reaches","Boolean","hostAccess","allowed","salutation","judged","value","readable","Set","companySnapshot","companies","tokens","visible","Array","isArray","some","token","has","result","source","interaction","summary","Object","fromEntries","entries","filter","key","otherAddress","doNotCall","refused","reason","contacts","arrayUnion","created","type","id","firstName","lastName","registerCrmConsoleApi","ping","contactStage","contactUpdate","contactRemove","companyDelete","save","complete","emailSend","dailyDigest","web","request","crmDailyDigestRoute","machine","taskReminders","crmTaskRemindersRoute","inbound","crmInboundRoute"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/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/**\n * The Contacts plugin's SERVER half (AGL-2595).\n *\n * Contacts CRM v1 had none: every console write was client-direct against\n * the Firestore rules, and the one server path that creates a contact —\n * `upsertHostContact` — belongs to the capture doors, not to this plugin.\n * v2 needs a server for the things a browser must not do alone: an import\n * that dedupes forty thousand rows, a bulk action over a selection, an\n * auto-association of contacts to companies by domain. Each of those lands\n * as a `crm/<route>` handler registered here.\n *\n * `crm/ping` exists so the wiring is PROVEN rather than assumed:\n * `plugins.config.json` names this module's register function and the\n * `crm` API prefix, the generated server manifest loads this file, and the\n * console's `/api/[...pluginApi]` dispatcher reaches the handler. A plugin\n * whose first real route also had to be its first wiring test would have\n * two things to debug at once.\n *\n * The real routes are the writes with a side effect outside the document:\n * `crm/contact-stage` (AGL-2605) exists for the EVENT — `contactStageChanged`\n * can only be emitted by a server path that performed the write, so a record\n * page that moved the stage client-direct would move the person and tell no\n * automation; the task routes (AGL-2599) carry an assignee's notification and\n * the `taskCompleted` event. Every CRM file is imported through the\n * transfer framework (AGL-3527, AGL-3528, `transfer/`), whose writes go\n * through the same doors every other server path uses.\n * `crm/deal-stage` (AGL-2598) is the one writer of a deal's stage, won and lost,\n * because a stage change is what automations listen for (`server-deal-stage.ts`).\n * `crm/contacts-create` (AGL-2596) is a person typed into the console by a\n * member of the team — a server route because the dedupe against every\n * holder's rows and the audience band are judgments the browser cannot make.\n * `crm/contact-update` (AGL-2804) is every other write to a contact's facets:\n * the rules cannot tell one field of a holder's facet from another, so they\n * leave a client nothing there, and the plan is asked here about the fields\n * a save carries. `crm/contact-remove` (AGL-3338) is a holder letting a\n * contact go: the refusals the document holds are kept, which a client\n * write could not be trusted to do.\n */\n\nimport { registerDealFigureReaders } from './server/deal-figures'\nimport { registerPeopleFigureReader } from './server/people-figures'\nimport {\n CONTACT_ERASED_MESSAGE,\n CONTACT_FIELDS_MAX_PER_ORG,\n contactFacetPath,\n composeContactName,\n type ContactCustomValue,\n type ContactFieldDefinition,\n CRM_COLLECTIONS,\n CRM_SALUTATION_PICKLIST,\n crmReadTokens,\n isContactLifecycleStage,\n isOrgWideMember,\n judgeCrmPicklistValue,\n normalizeAddress,\n normalizeContactEmail,\n normalizePhone,\n type AglynPostalAddress,\n type PluginApiHandler,\n type PluginApiRequest,\n readContactFacet,\n readCrmCustomInput,\n registerPluginApiRoute,\n visibleToHost,\n} from '@aglyn/aglyn/server'\nimport {\n consentGroupForSite,\n firebaseAdmin,\n getOrgForHost,\n logHostActivity,\n memberHasOrgPermission,\n orgDataCollectionForHost,\n resolveOrgMembership,\n restampCrmListFieldsAt,\n} from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { emitHostEvent } from '@aglyn/tenant-runtime'\nimport { captureHostContact } from './server/capture-host-contact'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { CRM_API_ROUTES } from './constants/api-routes'\nimport { registerCrmServerDeclarations } from './declarations.server'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { CRM_NEXT_ACTIVITY_ROUTE } from './model/next-activity'\nimport { CRM_LIST_FIELDS_ROUTE } from './model/list-fields'\nimport { CRM_LEAD_SOURCE_VALUES_ROUTE, CRM_PICKLIST_VALUES_ROUTE } from './model/picklist-values'\nimport { CRM_TASK_ROUTES } from './model/task-routes'\nimport { crmNextActivityHandler } from './server/next-activity-routes'\nimport { crmListFieldsHandler } from './server/list-fields-routes'\nimport { crmLeadSourceValuesHandler, crmPicklistValuesHandler } from './server/picklist-values'\nimport { registerCrmRecordEmailStateWriter } from './server/record-email-state'\nimport { registerCrmRecordTimelineWriter } from './server/record-timeline'\nimport { crmTaskCompleteHandler, crmTaskSaveHandler } from './server/task-routes'\nimport { crmDealStageHandler } from './server-deal-stage'\nimport { crmEmailSendHandler } from './server/email-send'\nimport { CRM_EMAIL_CHECK_ROUTE, crmEmailCheckHandler } from './server/email-check'\nimport { leadConvertHandler } from './server/lead-convert'\nimport { leadCreateHandler } from './server/lead-create'\nimport {\n CONTACT_EMAIL_HISTORY_ROUTE,\n contactEmailHistoryHandler,\n} from './server/contact-email-history'\nimport { CONTACTS_MERGE_ROUTE, contactsMergeHandler } from './server/contacts-merge'\nimport { CRM_ERASE_PERSON_ROUTE, crmErasePersonHandler } from './server/erase-person'\nimport { CRM_ORG_ACTIVITY_ROUTE, crmOrgActivityHandler } from './server/org-activity'\nimport { CRM_INBOUND_ADDRESS_ROUTE, crmInboundAddressHandler } from './server/inbound-address'\nimport { crmCompanyDeleteHandler } from './server/company-delete'\nimport { crmSharingHandler } from './server/crm-sharing'\nimport { CRM_SHARING_ROUTE } from './model/crm-sharing'\nimport { CONTACT_PHONE_REFUSAL, normalizeTags, typed } from './server/contact-profile'\nimport { crmContactUpdateHandler, readContactCreateProfile } from './server/contact-update'\nimport { readCrmPicklist } from './server/read-picklist'\nimport { crmContactRemoveHandler } from './server/contact-remove'\nimport {\n CRM_EMAIL_TEMPLATE_DUPLICATE_ROUTE,\n crmEmailTemplateDuplicateHandler,\n} from './server/email-template-duplicate'\nimport { registerCrmRecordFactsReaders } from './server/record-facts'\nimport { crmSuiteRefusal } from './server/suite-gate'\nimport {\n CRM_RECIPE_INSTALL_ROUTE,\n CRM_RECIPE_STATUS_ROUTE,\n crmRecipeInstallHandler,\n crmRecipeStatusHandler,\n} from './server/recipe-routes'\n\n/**\n * `GET /api/crm/ping` → `{ ok: true, plugin: 'crm' }`.\n *\n * No auth, no host, no data: it answers whether the plugin's server bundle\n * was loaded and its routes registered, which is a fact about the process\n * and not about any org. Anything that reads a document goes behind the\n * same session and role checks the other plugins' console routes use.\n */\nexport const crmPingHandler: PluginApiHandler = (req, res) => {\n if (req.method !== 'GET') {\n res.setHeader('Allow', 'GET')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n res.status(200).json({ ok: true, plugin: BUNDLE_ID })\n}\n\ntype Refusal = { ok: false; status: number; error: string }\n\n/**\n * Who is calling, and whether they may edit this site's CRM.\n *\n * The same check the inbox, bookings and email console routes make: a\n * bearer ID token, verified, and an `admin` or `editor` role on the host\n * document. A contact is a site's business record, so the site's own role\n * map is the right authority; org-wide reach is not required, because a\n * site editor who can open the record in the console can already write\n * every other field on it client-direct.\n */\nasync function authorizeSiteEditor(\n req: PluginApiRequest,\n hostId: string,\n): Promise<{ ok: true; uid: string; email: string | null } | Refusal> {\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return { ok: false, status: 401, error: 'Unauthenticated' }\n let uid: string\n let email: string | null\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n uid = decoded.uid\n email = typeof decoded.email === 'string' ? decoded.email : null\n } catch (error) {\n // A refused credential is the caller's 401; a failure to check one is\n // ours and keeps a 5xx (AGL-2852).\n if (isRefusedIdToken(error)) return { ok: false, status: 401, error: 'Unauthenticated' }\n console.error('[crm] the site editor could not be verified', error)\n return { ok: false, status: 500, error: 'The sign-in could not be checked. Try again.' }\n }\n const hostSnapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .get()\n if (!hostSnapshot.exists) {\n return { ok: false, status: 404, error: 'Unknown site' }\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return { ok: false, status: 403, error: 'Not a site admin or editor' }\n }\n return { ok: true, uid, email }\n}\n\n/**\n * `POST /api/crm/contact-stage` with `{ hostId, contactId, lifecycleStage }`.\n *\n * Writes the stage into THIS site's facet on the contact — a dotted\n * `update()`, never a top-level field, because the row is shared by every\n * site in the org and a stage is one holder's reading of the person — and\n * then announces `contactStageChanged` with the stage it replaced.\n *\n * A stage set to what it already is writes nothing and announces nothing:\n * the response says `changed: false`, and no automation listening for a\n * change hears one that did not happen.\n *\n * `lifecycleStage: null` CLEARS the stage (AGL-2804). A facet is the\n * server's to write, so \"not placed yet\" comes through the route that owns\n * the stage rather than as a client-direct delete, and it is refused to a\n * plan without the suite the way a move is. There is no event for \"no\n * stage\", so a clear announces nothing; an empty string names no stage and\n * is refused like any other the list does not have.\n *\n * The contact is looked up by id and then checked against `visibleTo`,\n * because the Admin SDK evaluates no rules: a caller who can edit site A\n * must not be able to restage a contact only site B holds by guessing its\n * id, and the scoped query the list uses is not available to a `doc()` read.\n */\nexport const contactStageHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const hostId = String(req.body?.hostId ?? '').trim()\n const contactId = String(req.body?.contactId ?? '').trim()\n const requested: unknown = req.body?.lifecycleStage\n if (!hostId || !contactId) {\n res.status(400).json({ error: 'Missing hostId or contactId' })\n return\n }\n // `null` clears the stage; anything else has to be one of the list's.\n const clearing = requested === null\n const lifecycleStage = clearing\n ? ''\n : isContactLifecycleStage(requested)\n ? requested\n : null\n if (lifecycleStage === null) {\n res.status(400).json({ error: 'Pick a lifecycle stage' })\n return\n }\n const caller = await authorizeSiteEditor(req, hostId)\n if (caller.ok === false) {\n res.status(caller.status).json({ error: caller.error })\n return\n }\n try {\n // A lifecycle stage is the suite's (AGL-2787) — see `suite-gate.ts`.\n const owner = await getOrgForHost(hostId)\n if (!owner) {\n res.status(404).json({ error: 'Unknown site' })\n return\n }\n const suite = crmSuiteRefusal(owner.org, \"Moving a contact's lifecycle stage\")\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n const contactsRef = await orgDataCollectionForHost(hostId, 'contacts')\n const snapshot = await contactsRef.doc(contactId).get()\n if (!snapshot.exists || !visibleToHost(snapshot.get('visibleTo'), hostId)) {\n res.status(404).json({ error: 'Unknown contact' })\n return\n }\n const group = await consentGroupForSite(hostId)\n const facet = readContactFacet(\n snapshot.data() as Record<string, unknown>,\n group.groupId,\n )\n const previousStage = facet.lifecycleStage ?? ''\n if (previousStage === lifecycleStage) {\n res\n .status(200)\n .json({ ok: true, changed: false, lifecycleStage, previousStage })\n return\n }\n await snapshot.ref.update({\n [contactFacetPath(group.groupId, 'lifecycleStage')]: clearing\n ? FieldValue.delete()\n : lifecycleStage,\n updatedAt: FieldValue.serverTimestamp(),\n })\n // The stage is what the Contacts list filters by (AGL-3321).\n await restampCrmListFieldsAt(snapshot.ref, 'contacts')\n // Awaited, not floated: a serverless response ending cancels in-flight\n // work, and the event is the reason this route exists. A clear has none.\n if (!clearing) {\n await emitHostEvent(\n hostId,\n 'contactStageChanged',\n {\n contactId,\n email: String(snapshot.get('email') ?? ''),\n lifecycleStage,\n previousStage,\n },\n { actor: { kind: 'member', uid: caller.uid, email: caller.email } },\n )\n }\n res\n .status(200)\n .json({ ok: true, changed: true, lifecycleStage, previousStage })\n } catch (error) {\n console.error('[crm] contact-stage failed', hostId, contactId, error)\n res.status(500).json({ error: 'The stage could not be changed' })\n }\n}\n\n/**\n * What the create route says when the band refused (AGL-2596).\n *\n * The contacts list's own alert, in the past tense: the list says new\n * visitors are \"no longer captured\", and this says the one contact the\n * reader just tried to add was not. The remedy is the same sentence in both\n * places, so a reader who sees one and then the other is told one thing.\n */\nexport const CONTACT_BAND_FULL_MESSAGE =\n 'Contact limit reached — this contact was not added. Upgrade in Billing ' +\n 'to keep collecting.'\n\n/**\n * `POST /api/crm/contacts-create` — a person added by hand (AGL-2596).\n *\n * Body: `{ hostId, email, name?, phone?, jobTitle?, companyName?,\n * companyId?, address?, ownerUid?, lifecycleStage?, tags?,\n * marketingConsent?, disclosedConsentGroup? }`, plus any of Salesforce's\n * standard contact fields `crm/contact-update` reads but the reports-to\n * (`CONTACT_CREATE_PROFILE_FIELDS`, AGL-3515) — read by the same rules, the\n * salutation judged against the org's list, and the holder's name composed\n * from a first and last name when either is given. Answers\n * `{ contactId, created }`: `created` is\n * false when the address already belonged to somebody, in which case what\n * was typed MERGES into the existing row — the dedupe the shared address\n * book exists for, and the reason a second \"create\" of one person is not\n * an error.\n *\n * ## Who may call it\n *\n * The caller's ID token, then `data.manage` on the site's org — the same key\n * the console surface itself is gated on, resolved through the same\n * three-layer permission read the members route uses. A scoped member is\n * admitted only for a site they reach: the rules would refuse a browser\n * write outside their tokens, and a server route that admitted it would be\n * a way round the rules rather than a service in front of them.\n *\n * ## What it refuses, and how\n *\n * A malformed email or phone number is a 400 with a sentence the drawer can\n * show under the field. The band is a 409 carrying the list's own wording:\n * it is not a bad request and not a server fault, it is the plan saying no,\n * and the drawer relays the sentence rather than inventing one.\n *\n * ## The company (AGL-2613)\n *\n * `companyId` is the picker's choice, and it is checked before anything is\n * written: the company has to exist and be visible to the capturing site's\n * scope, or a caller could file a person under a record they cannot open —\n * the same refusal the lead conversion makes. It reaches the upsert as\n * `facet.companyId`, which is where the link is kept in step with its mirror\n * and the company's contacts count; a merge onto somebody already at another\n * company is a MOVE there, not a second link. The stored company's own name\n * is what is echoed, over whatever the client sent, because the name on the\n * record is the company's and not the form's.\n *\n * ## What it writes beyond the upsert\n *\n * `upsertHostContact` takes the profile — phone, title, address, owner,\n * stage, company — and writes it into the capturing group's facet. The two\n * things it does not take are written here by DOTTED path afterwards: the\n * tags, which union into whatever the person already carried, and the\n * company name — the picked company's, or free text for a person filed\n * under a name no company record carries yet. Dotted paths because this is\n * an `update()`, and only an update reads a dot as a path — a nested object\n * here would replace the facet map and take every other holder's records\n * with it.\n */\nexport const crmContactsCreateHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n\n const body = (req.body ?? {}) as Record<string, unknown>\n const hostId = typed(body['hostId'], 128)\n if (!hostId) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const email = normalizeContactEmail(body['email'])\n if (!email) {\n res.status(400).json({ error: 'Enter a valid email address.' })\n return\n }\n const rawPhone = typed(body['phone'], 40)\n const phone = rawPhone ? normalizePhone(rawPhone) : null\n if (rawPhone && !phone) {\n res.status(400).json({ error: CONTACT_PHONE_REFUSAL })\n return\n }\n const rawStage = typed(body['lifecycleStage'], 40)\n if (rawStage && !isContactLifecycleStage(rawStage)) {\n res.status(400).json({ error: 'Unknown lifecycle stage.' })\n return\n }\n const profile = readContactCreateProfile(body)\n if (profile.ok === false) {\n res.status(400).json({ error: profile.error })\n return\n }\n const extras = profile.fields\n const name = typed(body['name'], 120)\n const jobTitle = typed(body['jobTitle'], 120)\n let companyName = typed(body['companyName'], 120)\n const companyId = typed(body['companyId'], 128)\n const ownerUid = typed(body['ownerUid'], 128)\n const address =\n body['address'] && typeof body['address'] === 'object'\n ? normalizeAddress(body['address'] as AglynPostalAddress)\n : null\n const tags = normalizeTags(body['tags'])\n const marketingConsent = body['marketingConsent'] === true\n // The key of the consent-group sentence the drawer showed beside the box\n // (AGL-3320): the opt-in covers the site's group only while it is the\n // group's current one, and the capturing site alone otherwise.\n const disclosedConsentGroup = typed(body['disclosedConsentGroup'], 64)\n // The org's own contact fields, sent in this same call (AGL-3971): a\n // value the create dropped would read as saved and be gone, and a second\n // write after it could land half. Judged below against the definitions.\n const rawCustom = body['custom']\n if (\n rawCustom !== undefined &&\n (!rawCustom || typeof rawCustom !== 'object' || Array.isArray(rawCustom))\n ) {\n res.status(400).json({ error: 'Custom fields must be values keyed by field key.' })\n return\n }\n\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) {\n res.status(401).json({ error: 'Unauthenticated' })\n return\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const resolved = await getOrgForHost(hostId)\n if (!resolved) {\n res.status(404).json({ error: 'Unknown site' })\n return\n }\n const isStaff = decoded['staff'] === true\n if (!isStaff) {\n const membership = await resolveOrgMembership(decoded.uid, resolved.orgId)\n const member = membership?.member ?? null\n const reaches =\n isOrgWideMember(member) || Boolean(member?.hostAccess?.[hostId])\n const allowed =\n member &&\n reaches &&\n (await memberHasOrgPermission(resolved.orgId, member, 'data.manage'))\n if (!allowed) {\n res.status(403).json({\n error: 'Adding contacts requires the data.manage permission on this site',\n })\n return\n }\n }\n\n // A person typed in by the team is the suite's; the capture doors that\n // fill a Free workspace's contacts do not come through here (AGL-2787).\n const suite = crmSuiteRefusal(resolved.org, 'Adding a contact by hand')\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n\n // The salutation, judged against the org's list (AGL-3515).\n let salutation = ''\n if (extras.salutation) {\n const judged = judgeCrmPicklistValue(\n CRM_SALUTATION_PICKLIST,\n await readCrmPicklist(\n firebaseAdmin.app().firestore(),\n resolved.orgId,\n CRM_SALUTATION_PICKLIST,\n ),\n extras.salutation,\n )\n if (judged.ok === false) {\n res.status(400).json({ error: judged.error })\n return\n }\n salutation = judged.value ?? ''\n }\n\n /*\n * The custom values, judged exactly as `crm/contact-update` judges them\n * (AGL-3971): the org's definitions read only when the body carried a\n * map, an unknown, retired or mistyped key refused with the definition\n * reader's own sentence, and a currency amount stored as that field's\n * minor units. Before any write, so a refusal leaves nothing behind.\n */\n let custom: Record<string, ContactCustomValue> | undefined\n if (rawCustom !== undefined) {\n const definitions = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(resolved.orgId)\n .collection(CRM_COLLECTIONS.contactFields)\n .limit(CONTACT_FIELDS_MAX_PER_ORG)\n .get()\n const judged = readCrmCustomInput(\n rawCustom,\n definitions.docs.map((entry) => entry.data() as ContactFieldDefinition),\n 'contact',\n )\n if ('errors' in judged) {\n const [field, message] = Object.entries(judged.errors)[0] ?? []\n res.status(400).json({\n error: message ?? 'A custom value could not be saved.',\n ...(field ? { field } : {}),\n })\n return\n }\n custom = judged.values\n }\n\n if (companyId) {\n const group = await consentGroupForSite(\n hostId,\n resolved.org as Record<string, unknown>,\n )\n const readable = new Set<string>(crmReadTokens(group))\n const companySnapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(resolved.orgId)\n .collection(CRM_COLLECTIONS.companies)\n .doc(companyId)\n .get()\n const tokens: unknown = companySnapshot.exists\n ? companySnapshot.get('visibleTo')\n : undefined\n const visible =\n Array.isArray(tokens) && tokens.some((token) => readable.has(String(token)))\n if (!visible) {\n res.status(404).json({ error: 'Unknown company' })\n return\n }\n companyName = typed(companySnapshot.get('name'), 120) || companyName\n }\n\n const result = await captureHostContact({\n hostId,\n email,\n ...(name ? { name } : {}),\n source: 'manual',\n interaction: { summary: 'Added by hand' },\n actor: {\n kind: 'member',\n uid: decoded.uid,\n email: typeof decoded.email === 'string' ? decoded.email : null,\n },\n marketingConsent,\n ...(marketingConsent && disclosedConsentGroup\n ? { disclosedConsentGroup }\n : {}),\n facet: {\n ...(phone ? { phone } : {}),\n ...(jobTitle ? { jobTitle } : {}),\n // Salesforce's standard fields (AGL-3515); the capture composes the\n // holder's name from the first and last names.\n ...Object.fromEntries(\n Object.entries({ ...extras, salutation }).filter(\n ([key, value]) =>\n key !== 'doNotCall' && key !== 'otherAddress' && typeof value === 'string' && value,\n ),\n ),\n ...(extras.otherAddress ? { otherAddress: extras.otherAddress } : {}),\n ...(extras.doNotCall ? { doNotCall: true } : {}),\n ...(companyId ? { companyId } : {}),\n ...(address ? { address } : {}),\n ...(ownerUid ? { ownerUid } : {}),\n ...(rawStage && isContactLifecycleStage(rawStage)\n ? { lifecycleStage: rawStage }\n : {}),\n // Written nested into the capturing holder's facet by the upsert,\n // on a new person and on a merge alike.\n ...(custom && Object.keys(custom).length ? { custom } : {}),\n },\n })\n\n if ('refused' in result) {\n if (result.refused === 'band') {\n res.status(409).json({ error: CONTACT_BAND_FULL_MESSAGE, reason: 'band' })\n return\n }\n if (result.refused === 'invalid-email') {\n res.status(400).json({ error: 'Enter a valid email address.' })\n return\n }\n // The person was erased from this workspace (AGL-2623). A conflict,\n // like the band: the request was well-formed and the answer is a\n // standing fact about the address, not a fault in the call.\n if (result.refused === 'erased') {\n res.status(409).json({ error: CONTACT_ERASED_MESSAGE, reason: 'erased' })\n return\n }\n res.status(500).json({ error: 'The contact could not be saved.' })\n return\n }\n\n if (tags.length || companyName) {\n const group = await consentGroupForSite(\n hostId,\n resolved.org as Record<string, unknown>,\n )\n const contacts = await orgDataCollectionForHost(hostId, 'contacts')\n await contacts.doc(result.contactId).update({\n ...(tags.length\n ? {\n [contactFacetPath(group.groupId, 'tags')]: FieldValue.arrayUnion(\n ...tags,\n ),\n }\n : {}),\n ...(companyName\n ? {\n [contactFacetPath(group.groupId, 'companyName')]: companyName,\n // The search echo — see `HostContact.companyName`.\n companyName,\n }\n : {}),\n updatedAt: FieldValue.serverTimestamp(),\n })\n // The tags are what the Contacts list filters by (AGL-3321).\n await restampCrmListFieldsAt(contacts.doc(result.contactId), 'contacts')\n }\n\n /*\n * The audit line, written HERE rather than by the console (AGL-2622):\n * a route that verified the caller and performed the write is the one\n * writer that cannot record an act that did not happen, which is the\n * reason `check-activity-coverage.mjs` counts this file. A merge into\n * a person the org already held is said to be one — the row was\n * updated, not added — so the feed cannot claim two people for one.\n */\n await logHostActivity(\n hostId,\n { uid: decoded.uid, email: decoded.email ?? null },\n result.created ? 'Added contact' : 'Updated contact',\n {\n type: 'contact',\n id: result.contactId,\n name: name || composeContactName(extras.firstName, extras.lastName) || email,\n },\n )\n\n res\n .status(result.created ? 201 : 200)\n .json({ contactId: result.contactId, created: result.created })\n } catch (error) {\n console.error('[crm] contact create failed', error)\n res.status(500).json({ error: 'The contact could not be saved.' })\n }\n}\n\n/** Console API registration, named in `plugins.config.json` as `consoleApi`. */\nexport function registerCrmConsoleApi(): void {\n // The pipeline and the deals closed, as figure tables (AGL-3603), for the\n // AI plugin's insights to read by id; the console runs insight jobs.\n registerDealFigureReaders(() => firebaseAdmin.app().firestore())\n // And the leads and contacts gained over a window (AGL-3867), for the\n // weekly summary and for insights.\n registerPeopleFigureReader(() => firebaseAdmin.app().firestore())\n /*\n * The contact-capture writer, again (AGL-3080). Both apps run it at boot\n * from their generated server-declarations manifest, and this is the\n * backstop for a process whose boot did not: the first CRM door to load\n * registers it. Registering twice replaces in place.\n */\n registerCrmServerDeclarations()\n registerPluginApiRoute(CRM_API_ROUTES.ping, crmPingHandler)\n registerPluginApiRoute(CRM_API_ROUTES.contactStage, contactStageHandler)\n // Every other console write to a contact's facets (AGL-2804): the rules\n // cannot tell one facet field from another, so the plan is asked here.\n registerPluginApiRoute(CRM_API_ROUTES.contactUpdate, crmContactUpdateHandler)\n registerPluginApiRoute(CRM_API_ROUTES.contactRemove, crmContactRemoveHandler)\n // A company's contacts unlinked, then the company (AGL-2804): the unlink\n // clears a facet, which is the server's to write.\n registerPluginApiRoute(CRM_API_ROUTES.companyDelete, crmCompanyDeleteHandler)\n // Tasks (AGL-2599): the two writes with a side effect outside the document\n // — an assignee's notification, and the `taskCompleted` host event.\n registerPluginApiRoute(CRM_TASK_ROUTES.save, crmTaskSaveHandler)\n registerPluginApiRoute(CRM_TASK_ROUTES.complete, crmTaskCompleteHandler)\n // A client-direct task write's door to `nextTaskAtMs` (AGL-2661).\n registerPluginApiRoute(CRM_NEXT_ACTIVITY_ROUTE, crmNextActivityHandler)\n // What a client-direct write the browser cannot follow up asks for: the\n // fields the CRM lists query, restamped from the record (AGL-3321).\n registerPluginApiRoute(CRM_LIST_FIELDS_ROUTE, crmListFieldsHandler)\n // Sharing records with other sites (AGL-3336): a manager's share and\n // unshare, the org's sharing rules and their recompute, and the rules'\n // re-evaluation a client-direct write owes.\n registerPluginApiRoute(CRM_SHARING_ROUTE, crmSharingHandler)\n // A picklist value renamed or deleted (AGL-3298, AGL-3510): the list and\n // every record holding the old label, in one request. The lead source's\n // own address stays registered for a request that names no picklist.\n registerPluginApiRoute(CRM_PICKLIST_VALUES_ROUTE, crmPicklistValuesHandler)\n registerPluginApiRoute(CRM_LEAD_SOURCE_VALUES_ROUTE, crmLeadSourceValuesHandler)\n // The one writer of a deal's stage, won and lost (AGL-2598): the browser\n // could write the field, but only a server can emit the event an\n // automation listens for.\n registerPluginApiRoute('crm/deal-stage', crmDealStageHandler)\n // A person typed into the console (AGL-2596): the capture doors' own\n // function behind a session check, so the dedupe and the band are judged\n // where every other door judges them.\n registerPluginApiRoute('crm/contacts-create', crmContactsCreateHandler)\n // A lead becomes a contact, a company and a deal (AGL-2608) — the one CRM\n // write a browser cannot make alone, because only the server may create a\n // contact through the dedupe-and-meter door.\n registerPluginApiRoute('crm/lead-convert', leadConvertHandler)\n // A lead entered by hand (AGL-3231): the New lead drawer's route.\n registerPluginApiRoute('crm/leads-create', leadCreateHandler)\n // The one READ behind a route (AGL-2616): the per-recipient delivery log\n // is closed to clients, so a contact's campaign mail is projected here.\n registerPluginApiRoute(CONTACT_EMAIL_HISTORY_ROUTE, contactEmailHistoryHandler)\n // One email to one person from their record (AGL-2615): the recipient is\n // read off the record, the daily cap and both suppression lists are\n // judged, and the message leaves on the site's sending identity.\n registerPluginApiRoute(CRM_API_ROUTES.emailSend, crmEmailSendHandler)\n // What the platform knows about one address before somebody writes to it\n // (AGL-3328): the gateway chip on a record page, and the composer's\n // warning about a domain with no mail server or a gateway holding this\n // site's mail. DNS and the ledger are the server's.\n registerPluginApiRoute(CRM_EMAIL_CHECK_ROUTE, crmEmailCheckHandler)\n // Two records for one person become one (AGL-2625): the repoint of every\n // row naming the merged record and the transaction over both documents\n // are the server's, and the address index it writes is closed to clients.\n registerPluginApiRoute(CONTACTS_MERGE_ROUTE, contactsMergeHandler)\n // Files a person's privacy erasure for the daily job (AGL-2623);\n // workspace admins only.\n registerPluginApiRoute(CRM_ERASE_PERSON_ROUTE, crmErasePersonHandler)\n // One line in the organization's activity feed for an act the org-level\n // hub performed client-direct (AGL-2634): the feed is closed to clients,\n // so the bulk bars' lines come through here.\n registerPluginApiRoute(CRM_ORG_ACTIVITY_ROUTE, crmOrgActivityHandler)\n // A recipe's action written into one site's automations from the org\n // hub (AGL-2639), stamped and deduplicated by the server, and the stamps\n // read back per site so the hub can say which sites carry which recipe.\n registerPluginApiRoute(CRM_RECIPE_INSTALL_ROUTE, crmRecipeInstallHandler)\n registerPluginApiRoute(CRM_RECIPE_STATUS_ROUTE, crmRecipeStatusHandler)\n // The workspace's email capture address (AGL-2657): the token is minted\n // and rotated here, behind the CRM's own gate, and the org document that\n // carries it is closed to every client.\n registerPluginApiRoute(CRM_INBOUND_ADDRESS_ROUTE, crmInboundAddressHandler)\n /*\n * The three doors a MACHINE calls (AGL-3080), at the paths they have\n * always answered on: the scheduler's daily digest and hourly task\n * reminders, on the cron secret, and the mail provider's capture webhook,\n * on its signature. None names a site or a member, so each is registered\n * as a machine's route — the dispatcher leaves the plugin's release and\n * enablement gates and its write limit to it — and each judges the CRM's\n * plan and flag for every organization it resolves. Loaded with the\n * first call, not with the surface.\n */\n registerPluginApiRoute(\n CRM_API_ROUTES.dailyDigest,\n {\n web: async (request) =>\n (await import('./server/daily-digest-route')).crmDailyDigestRoute(request),\n },\n { machine: true },\n )\n registerPluginApiRoute(\n CRM_API_ROUTES.taskReminders,\n {\n web: async (request) =>\n (await import('./server/task-reminders-route')).crmTaskRemindersRoute(request),\n },\n { machine: true },\n )\n registerPluginApiRoute(\n CRM_API_ROUTES.inbound,\n {\n web: async (request) => (await import('./server/inbound-route')).crmInboundRoute(request),\n },\n { machine: true },\n )\n registerPluginApiRoute(\n CRM_EMAIL_TEMPLATE_DUPLICATE_ROUTE,\n crmEmailTemplateDuplicateHandler,\n )\n // The facts of a contact, company, deal or lead, and an import's field\n // catalog, for another plugin in this process (AGL-2917): read on the core's\n // record-facts seam under the rules every CRM door applies, and written\n // nowhere.\n registerCrmRecordFactsReaders()\n // The CRM as the workspace's record system on the core's record-timeline\n // seam (AGL-2981): another plugin files an email, a reply or a task on a\n // record through it, under the CRM's own scope, dedupe and ceiling.\n registerCrmRecordTimelineWriter()\n // And on the record email-state seam (AGL-3245): a sender that filed a\n // verdict on an address — a bounce, a complaint, an unsubscribe, a\n // do-not-contact mark — stamps the lead and the contact through it.\n registerCrmRecordEmailStateWriter()\n}\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { crmPlanEntitlements } from './plan-entitlements'\n"],"names":["registerDealFigureReaders","registerPeopleFigureReader","CONTACT_ERASED_MESSAGE","CONTACT_FIELDS_MAX_PER_ORG","contactFacetPath","composeContactName","CRM_COLLECTIONS","CRM_SALUTATION_PICKLIST","crmReadTokens","isContactLifecycleStage","isOrgWideMember","judgeCrmPicklistValue","normalizeAddress","normalizeContactEmail","normalizePhone","readContactFacet","readCrmCustomInput","registerPluginApiRoute","visibleToHost","consentGroupForSite","firebaseAdmin","getOrgForHost","logHostActivity","memberHasOrgPermission","orgDataCollectionForHost","resolveOrgMembership","restampCrmListFieldsAt","isRefusedIdToken","emitHostEvent","captureHostContact","FieldValue","CRM_API_ROUTES","registerCrmServerDeclarations","BUNDLE_ID","CRM_NEXT_ACTIVITY_ROUTE","CRM_LIST_FIELDS_ROUTE","CRM_LEAD_SOURCE_VALUES_ROUTE","CRM_PICKLIST_VALUES_ROUTE","CRM_TASK_ROUTES","crmNextActivityHandler","crmListFieldsHandler","crmLeadSourceValuesHandler","crmPicklistValuesHandler","registerCrmRecordEmailStateWriter","registerCrmRecordTimelineWriter","crmTaskCompleteHandler","crmTaskSaveHandler","crmDealStageHandler","crmEmailSendHandler","CRM_EMAIL_CHECK_ROUTE","crmEmailCheckHandler","leadConvertHandler","leadCreateHandler","CONTACT_EMAIL_HISTORY_ROUTE","contactEmailHistoryHandler","CONTACTS_MERGE_ROUTE","contactsMergeHandler","CRM_ERASE_PERSON_ROUTE","crmErasePersonHandler","CRM_ORG_ACTIVITY_ROUTE","crmOrgActivityHandler","CRM_INBOUND_ADDRESS_ROUTE","crmInboundAddressHandler","crmCompanyDeleteHandler","crmSharingHandler","CRM_SHARING_ROUTE","CONTACT_PHONE_REFUSAL","normalizeTags","typed","crmContactUpdateHandler","readContactCreateProfile","readCrmPicklist","crmContactRemoveHandler","CRM_EMAIL_TEMPLATE_DUPLICATE_ROUTE","crmEmailTemplateDuplicateHandler","registerCrmRecordFactsReaders","crmSuiteRefusal","CRM_RECIPE_INSTALL_ROUTE","CRM_RECIPE_STATUS_ROUTE","crmRecipeInstallHandler","crmRecipeStatusHandler","crmPingHandler","req","res","method","setHeader","status","json","error","ok","plugin","authorizeSiteEditor","hostId","hostSnapshot","authorization","String","headers","idToken","startsWith","slice","length","undefined","uid","email","decoded","app","auth","verifyIdToken","console","firestore","collection","doc","get","exists","memberRole","contactStageHandler","body","trim","contactId","requested","lifecycleStage","clearing","caller","facet","owner","suite","org","contactsRef","snapshot","group","data","groupId","previousStage","changed","ref","update","delete","updatedAt","serverTimestamp","actor","kind","CONTACT_BAND_FULL_MESSAGE","crmContactsCreateHandler","rawPhone","phone","rawStage","profile","extras","fields","name","jobTitle","companyName","companyId","ownerUid","address","tags","marketingConsent","disclosedConsentGroup","rawCustom","Array","isArray","resolved","isStaff","member","membership","orgId","reaches","Boolean","hostAccess","allowed","salutation","judged","value","custom","definitions","contactFields","limit","docs","map","entry","Object","field","message","entries","errors","values","readable","Set","companySnapshot","companies","tokens","visible","some","token","has","result","source","interaction","summary","fromEntries","filter","key","otherAddress","doNotCall","keys","refused","reason","contacts","arrayUnion","created","type","id","firstName","lastName","registerCrmConsoleApi","ping","contactStage","contactUpdate","contactRemove","companyDelete","save","complete","emailSend","dailyDigest","web","request","crmDailyDigestRoute","machine","taskReminders","crmTaskRemindersRoute","inbound","crmInboundRoute"],"mappings":"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"}
|