@aglyn/plugins-crm 1.0.0-beta.222 → 1.0.0-beta.223
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 +13 -13
- package/src/lib/components/crm-next-activity-column.js +1 -1
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +2 -1
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/declarations.server.js +5 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/contact-record.js +2 -1
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/crm-next-activity.d.ts +107 -0
- package/src/lib/model/crm-next-activity.js +140 -0
- package/src/lib/model/crm-next-activity.js.map +1 -0
- package/src/lib/model/crm-recipes.js +33 -18
- package/src/lib/model/crm-recipes.js.map +1 -1
- package/src/lib/model/crm-task-reminders.d.ts +152 -0
- package/src/lib/model/crm-task-reminders.js +161 -0
- package/src/lib/model/crm-task-reminders.js.map +1 -0
- package/src/lib/model/next-activity.d.ts +1 -1
- package/src/lib/model/next-activity.js.map +1 -1
- package/src/lib/server/api-v1/crm-tasks.js +4 -2
- package/src/lib/server/api-v1/crm-tasks.js.map +1 -1
- package/src/lib/server/assign-contact-owner.d.ts +27 -1
- package/src/lib/server/assign-contact-owner.js +136 -30
- package/src/lib/server/assign-contact-owner.js.map +1 -1
- package/src/lib/server/automation-steps.d.ts +2 -1
- package/src/lib/server/automation-steps.js +210 -13
- package/src/lib/server/automation-steps.js.map +1 -1
- package/src/lib/server/capture-contact.d.ts +17 -0
- package/src/lib/server/capture-contact.js +31 -6
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/crm-email-activity.d.ts +118 -0
- package/src/lib/server/crm-email-activity.js +148 -0
- package/src/lib/server/crm-email-activity.js.map +1 -0
- package/src/lib/server/crm-inbound-email.js +1 -1
- package/src/lib/server/crm-inbound-email.js.map +1 -1
- package/src/lib/server/crm-next-activity.d.ts +74 -0
- package/src/lib/server/crm-next-activity.js +194 -0
- package/src/lib/server/crm-next-activity.js.map +1 -0
- package/src/lib/server/email-send.js +2 -1
- package/src/lib/server/email-send.js.map +1 -1
- package/src/lib/server/merge-contacts.js +1 -1
- package/src/lib/server/merge-contacts.js.map +1 -1
- package/src/lib/server/next-activity-routes.js +2 -1
- package/src/lib/server/next-activity-routes.js.map +1 -1
- package/src/lib/server/recipe-routes.js +7 -1
- package/src/lib/server/recipe-routes.js.map +1 -1
- package/src/lib/server/record-timeline.js +26 -2
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/task-reminders-route.js +2 -1
- package/src/lib/server/task-reminders-route.js.map +1 -1
- package/src/lib/server/task-routes.js +4 -2
- package/src/lib/server/task-routes.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.223",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-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/tenant-data-admin": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.223",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.223",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.223",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.223",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.223",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.223",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.223",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.223",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.223",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.223",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.223",
|
|
39
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.223",
|
|
40
40
|
"@dnd-kit/core": "^6.3.1",
|
|
41
41
|
"@dnd-kit/utilities": "^3.2.2",
|
|
42
42
|
"@swc/helpers": "0.5.23",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
-
import { CRM_NEXT_ACTIVITY_FIELD, readNextTaskAtMs } from "@aglyn/aglyn";
|
|
19
18
|
import { Tooltip, Typography } from "@mui/material";
|
|
20
19
|
import { TaskDueText } from "./task-cells.js";
|
|
20
|
+
import { CRM_NEXT_ACTIVITY_FIELD, readNextTaskAtMs } from "../model/crm-next-activity.js";
|
|
21
21
|
/**
|
|
22
22
|
* The "Next activity" column the contacts, companies and deals lists share
|
|
23
23
|
* (AGL-2661): when the record's earliest open task is due, off the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-next-activity-column.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
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-next-activity-column.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { Tooltip, Typography } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { TaskDueText } from './task-cells'\nimport { CRM_NEXT_ACTIVITY_FIELD, readNextTaskAtMs } from '../model/crm-next-activity'\n\n/**\n * The \"Next activity\" column the contacts, companies and deals lists share\n * (AGL-2661): when the record's earliest open task is due, off the\n * `nextTaskAtMs` every server-side task writer keeps on the row.\n *\n * Drawn the way the tasks list draws a due date — overdue in the warning\n * color, today emphasized — because it IS that date, seen from the record.\n * A dash for a record with nothing scheduled, and the same dash for one\n * written before the field existed: the reader's question is \"is anything\n * planned\", and to both the answer is no. Sorting is off for the reason\n * the custom columns' is — the value is on the row, and a sort over the\n * loaded window would look like a sort over the collection. Filtering is\n * the list's to switch on, through `CRM_NEXT_ACTIVITY_FILTER_FIELD`.\n */\nexport function nextActivityColumn(nowMs: number): GridColDef {\n return {\n field: CRM_NEXT_ACTIVITY_FIELD,\n headerName: 'Next activity',\n flex: 0.8,\n minWidth: 140,\n type: 'date',\n sortable: false,\n filterable: false,\n valueGetter: (_value: unknown, row: { nextTaskAtMs?: unknown }) => {\n const at = readNextTaskAtMs(row)\n return at === null ? null : new Date(at)\n },\n renderCell: ({ row }: { row: { nextTaskAtMs?: unknown } }) => {\n const at = readNextTaskAtMs(row)\n if (at === null) {\n return (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n )\n }\n return (\n <Tooltip title={new Date(at).toLocaleString()}>\n <span>\n <TaskDueText task={{ status: 'open', dueAtMs: at }} nowMs={nowMs} variant=\"caption\" />\n </span>\n </Tooltip>\n )\n },\n }\n}\n\n/**\n * \"No next activity\" as a filter of the grid's own panel (AGL-3313): the\n * Next activity column offers \"is empty\", which stores the clause\n * `isNoNextActivityClause` names, so a view saved with the old toggle on\n * reopens filtered the same way. Asked of the query as `nextTaskAtMs ==\n * null` (AGL-3321): every contact, company and deal is created carrying\n * `null`, and the task writers put `null` back when nothing is left\n * scheduled.\n */\nexport const CRM_NEXT_ACTIVITY_FILTER_FIELD: ListFilterField = {\n column: CRM_NEXT_ACTIVITY_FIELD,\n kind: 'date',\n path: CRM_NEXT_ACTIVITY_FIELD,\n presence: 'nullable',\n storedAs: 'millis',\n operators: ['isEmpty'],\n}\n\n/** What the clause reads as on a chip. */\nexport const CRM_NEXT_ACTIVITY_FILTER_HEADER = 'Next activity'\n"],"names":["Tooltip","Typography","TaskDueText","CRM_NEXT_ACTIVITY_FIELD","readNextTaskAtMs","nextActivityColumn","nowMs","field","headerName","flex","minWidth","type","sortable","filterable","valueGetter","_value","row","at","Date","renderCell","variant","color","title","toLocaleString","span","task","status","dueAtMs","CRM_NEXT_ACTIVITY_FILTER_FIELD","column","kind","path","presence","storedAs","operators","CRM_NEXT_ACTIVITY_FILTER_HEADER"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAGA,SAASA,OAAO,EAAEC,UAAU,QAAQ,gBAAe;AAEnD,SAASC,WAAW,QAAQ,kBAAc;AAC1C,SAASC,uBAAuB,EAAEC,gBAAgB,QAAQ,gCAA4B;AAEtF;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,mBAAmBC,KAAa;IAC9C,OAAO;QACLC,OAAOJ;QACPK,YAAY;QACZC,MAAM;QACNC,UAAU;QACVC,MAAM;QACNC,UAAU;QACVC,YAAY;QACZC,aAAa,CAACC,QAAiBC;YAC7B,MAAMC,KAAKb,iBAAiBY;YAC5B,OAAOC,OAAO,OAAO,OAAO,IAAIC,KAAKD;QACvC;QACAE,YAAY,CAAC,EAAEH,GAAG,EAAuC;YACvD,MAAMC,KAAKb,iBAAiBY;YAC5B,IAAIC,OAAO,MAAM;gBACf,qBACE,KAAChB;oBAAWmB,SAAQ;oBAAUC,OAAM;8BACjC;;YAGP;YACA,qBACE,KAACrB;gBAAQsB,OAAO,IAAIJ,KAAKD,IAAIM,cAAc;0BACzC,cAAA,KAACC;8BACC,cAAA,KAACtB;wBAAYuB,MAAM;4BAAEC,QAAQ;4BAAQC,SAASV;wBAAG;wBAAGX,OAAOA;wBAAOc,SAAQ;;;;QAIlF;IACF;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,MAAMQ,iCAAkD;IAC7DC,QAAQ1B;IACR2B,MAAM;IACNC,MAAM5B;IACN6B,UAAU;IACVC,UAAU;IACVC,WAAW;QAAC;KAAU;AACxB,EAAC;AAED,wCAAwC,GACxC,OAAO,MAAMC,kCAAkC,gBAAe"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
18
|
import * as Aglyn from "@aglyn/aglyn";
|
|
19
|
+
import { stuckDeals } from "../../model/crm-next-activity.js";
|
|
19
20
|
import { money } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
|
|
20
21
|
import { AppLink, CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
21
22
|
import { Section } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
|
|
@@ -149,7 +150,7 @@ const TOP_DEALS = 10;
|
|
|
149
150
|
pipelineWindow
|
|
150
151
|
]);
|
|
151
152
|
// Open deals with nothing scheduled (AGL-2661), off the same window.
|
|
152
|
-
const stuck = useMemo(()=>
|
|
153
|
+
const stuck = useMemo(()=>stuckDeals(dealWindow.rows), [
|
|
153
154
|
dealWindow.rows
|
|
154
155
|
]);
|
|
155
156
|
const currency = summary.currency.currency;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/pipeline-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 * as Aglyn from '@aglyn/aglyn'\nimport { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { collectionCeiling } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename, shortDate } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { currencyOfDeals, pipelineTotals } from '../../model/crm-reports'\n\n/**\n * How many open deals the by-stage chart, the forecast and the top-deals\n * table are read from.\n *\n * The count and the face value of the pipeline are server aggregates and\n * never meet this bound. The forecast cannot be: it weights each deal by\n * ITS stage's odds, which is a join the server will not do, so it reads the\n * deals and says so when there were more than this.\n */\nconst OPEN_DEAL_CEILING = 1000\n/** More pipelines than this is a shape nobody has asked for; the probe says if it happens. */\nconst PIPELINE_CEILING = 20\nconst TOP_DEALS = 10\n\n/** The top-deals table's columns, which are also the CSV's — the amount split from its currency there. */\nconst TOP_DEAL_COLUMNS = ['Deal', 'Stage', 'Expected close', 'Amount', 'Currency'] as const\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\ntype PipelineRow = Aglyn.CrmPipeline & { $id: string }\n\nexport interface PipelineCardProps {\n report: CrmReportScope\n}\n\n/**\n * The open pipeline: how many deals, what they add up to, what they are\n * worth at their stage odds, where they sit, and the largest of them\n * (AGL-2604).\n *\n * Not period-scoped, deliberately. A pipeline is a stock, not a flow — what\n * is open NOW — and a \"pipeline over the last 30 days\" would be a report on\n * deals created in a window, which is a different question with a different\n * answer. The period picker governs the cards that measure flows.\n */\nexport function PipelineCard(props: PipelineCardProps) {\n const { report } = props\n const { scope, tokens, routes, nowMs } = report\n const firestore = useFirestore()\n\n const open = useAggregateRead(\n () =>\n getAggregateFromServer(\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n ),\n { count: count(), amountCents: sum('amountCents') },\n ).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:open') },\n )\n\n /**\n * The open deals, most recently updated first — the order the\n * `(visibleTo, status, updatedAt)` index the deals list already uses can\n * serve, so the report adds no index for this read.\n */\n const dealWindow = useWindowRead<DealRow>(\n () =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n orderBy('updatedAt', 'desc'),\n limit(OPEN_DEAL_CEILING + 1),\n ),\n OPEN_DEAL_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:deals') },\n )\n const pipelineWindow = useWindowRead<PipelineRow>(\n () =>\n collectionCeiling(\n query(\n scopedCollection(firestore, scope, 'pipelines'),\n ...visibleToClause(tokens),\n ),\n PIPELINE_CEILING,\n ),\n PIPELINE_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:pipelines') },\n )\n const dealsStatus = dealWindow.status\n\n const summary = useMemo(() => {\n const byPipeline = new Map<string, DealRow[]>()\n for (const deal of dealWindow.rows) {\n const key = deal.pipelineId ?? ''\n const list = byPipeline.get(key) ?? []\n list.push(deal)\n byPipeline.set(key, list)\n }\n // Every active pipeline is charted even with nothing open; an archived\n // one only while a deal still sits open in it — the forecast card's\n // rule, so the two cards name the same pipelines.\n const pipelines = pipelineWindow.rows\n .filter((pipeline) => !Aglyn.isPipelineArchived(pipeline) || byPipeline.has(pipeline.$id))\n .map((pipeline) => ({\n pipeline,\n totals: pipelineTotals(byPipeline.get(pipeline.$id) ?? [], pipeline),\n }))\n const known = new Set(pipelineWindow.rows.map((pipeline) => pipeline.$id))\n const orphans = dealWindow.rows.filter(\n (deal) => !known.has(deal.pipelineId ?? ''),\n )\n const stageName = (deal: DealRow): string =>\n Aglyn.dealStageById(\n pipelineWindow.rows.find((pipeline) => pipeline.$id === deal.pipelineId),\n deal.stageId,\n )?.name ?? deal.stageId\n const top = [...dealWindow.rows]\n .sort(\n (a, b) => Number(b.amountCents ?? 0) - Number(a.amountCents ?? 0),\n )\n .slice(0, TOP_DEALS)\n return {\n pipelines,\n orphanTotals: pipelineTotals(orphans, null),\n weightedCents: pipelines.reduce(\n (total, entry) => total + entry.totals.weightedCents,\n 0,\n ),\n top,\n stageName,\n currency: currencyOfDeals(dealWindow.rows),\n }\n }, [dealWindow, pipelineWindow])\n // Open deals with nothing scheduled (AGL-2661), off the same window.\n const stuck = useMemo(() => Aglyn.stuckDeals(dealWindow.rows), [dealWindow.rows])\n\n const currency = summary.currency.currency\n const dealsRead = dealsStatus === 'success'\n\n return (\n <CardDisplay\n header={'Pipeline'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#pipeline',\n excerpt:\n 'Every open deal: how many, what they add up to, and what they ' +\n 'are worth once each is weighted by the odds of its stage. Not ' +\n 'tied to the period — a pipeline is what is open now.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('top-open-deals')}\n columns={TOP_DEAL_COLUMNS}\n rows={() =>\n summary.top.map((deal) => [\n deal.title || deal.$id,\n summary.stageName(deal),\n typeof deal.expectedCloseAtMs === 'number' && deal.expectedCloseAtMs > 0\n ? new Date(deal.expectedCloseAtMs).toISOString().slice(0, 10)\n : '',\n (Number(deal.amountCents ?? 0) / 100).toFixed(2),\n String(deal.currency || currency).toUpperCase(),\n ])\n }\n disabled={!dealsRead || !summary.top.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open deals'}\n value={open.value ? open.value.count.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Pipeline value'}\n value={open.value ? money(open.value.amountCents, currency) : null}\n note={'face value of every open deal'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Weighted forecast'}\n value={dealsRead ? money(summary.weightedCents, currency) : null}\n note={\n dealWindow.truncated\n ? `from the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated`\n : 'each deal at its stage odds'\n }\n />\n {/*\n Open deals with no open task against them (AGL-2661): the ones\n nobody has a next step for. Painted as a warning above zero,\n because a deal with nothing planned is a deal going nowhere; the\n tile opens the deals list, whose \"No next activity\" chip lists\n them.\n */}\n <ReportStatTile\n label={'Stuck deals'}\n value={dealsRead ? stuck.count.toLocaleString() : null}\n note={'open, with no next activity'}\n riseIsGood={false}\n color={stuck.count > 0 ? 'warning.main' : undefined}\n href={routes.section('deals')}\n />\n </Stack>\n {open.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The deal totals could not be read.'}\n </Alert>\n ) : null}\n {summary.currency.mixed ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Deals are in more than one currency; the totals add them as numbers and show ${currency.toUpperCase()}, the most common.`}\n </Typography>\n ) : null}\n {dealsRead && !summary.pipelines.length && !dealWindow.rows.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No open deals yet.'}\n </Typography>\n ) : null}\n {summary.pipelines.map(({ pipeline, totals }) => (\n <Section key={pipeline.$id} title={pipeline.name || 'Pipeline'}>\n <ReportBreakdown\n rows={[\n ...totals.stages.map((row) => ({\n key: row.stage.id,\n label: row.stage.name,\n value: row.amountCents,\n display: money(row.amountCents, currency),\n note: `${plural(row.count, 'deal')} · ${money(row.weightedCents, currency)} weighted`,\n })),\n ...(totals.unplaced.count\n ? [\n {\n key: '$unplaced',\n label: 'Stage no longer in this pipeline',\n value: totals.unplaced.amountCents,\n display: money(totals.unplaced.amountCents, currency),\n note: `${plural(totals.unplaced.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]\n : []),\n ]}\n emptyText={'This pipeline has no open stages.'}\n />\n </Section>\n ))}\n {summary.orphanTotals.count ? (\n <Section title={'Deals in no pipeline'}>\n <ReportBreakdown\n rows={[\n {\n key: '$orphans',\n label: 'No pipeline',\n value: summary.orphanTotals.amountCents,\n display: money(summary.orphanTotals.amountCents, currency),\n note: `${plural(summary.orphanTotals.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]}\n emptyText={''}\n />\n </Section>\n ) : null}\n {pipelineWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Only the first ${PIPELINE_CEILING} pipelines are charted.`}\n </Typography>\n ) : null}\n {summary.top.length ? (\n <Section title={'Top open deals'}>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Deal'}</TableCell>\n <TableCell>{'Stage'}</TableCell>\n <TableCell>{'Expected close'}</TableCell>\n <TableCell align=\"right\">{'Amount'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {summary.top.map((deal) => (\n <TableRow key={deal.$id}>\n <TableCell>\n <AppLink href={routes.deal(deal.$id)}>\n {deal.title || deal.$id}\n </AppLink>\n </TableCell>\n <TableCell>{summary.stageName(deal)}</TableCell>\n <TableCell>{shortDate(deal.expectedCloseAtMs)}</TableCell>\n <TableCell align=\"right\">\n {money(\n Number(deal.amountCents ?? 0),\n String(deal.currency || currency),\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {/* What the ranking, and so the export, is drawn from when that is not everything. */}\n {dealWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Ranked within the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.`}\n </Typography>\n ) : null}\n </Section>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nPipelineCard.displayName = 'PipelineCard'\n\nexport default PipelineCard\n"],"names":["Aglyn","money","AppLink","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","count","getAggregateFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","collectionCeiling","ReportBreakdown","ReportExport","plural","reportFilename","shortDate","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","currencyOfDeals","pipelineTotals","OPEN_DEAL_CEILING","PIPELINE_CEILING","TOP_DEALS","TOP_DEAL_COLUMNS","PipelineCard","props","report","scope","tokens","routes","nowMs","firestore","open","amountCents","then","snapshot","Number","data","cacheKey","dealWindow","pipelineWindow","dealsStatus","status","summary","byPipeline","Map","deal","rows","key","pipelineId","list","get","push","set","pipelines","filter","pipeline","isPipelineArchived","has","$id","map","totals","known","Set","orphans","stageName","dealStageById","find","stageId","name","top","sort","a","b","slice","orphanTotals","weightedCents","reduce","total","entry","currency","stuck","stuckDeals","dealsRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","title","expectedCloseAtMs","Date","toISOString","toFixed","String","toUpperCase","disabled","length","spacing","direction","sx","flexWrap","label","value","toLocaleString","note","href","section","truncated","riseIsGood","color","undefined","severity","mixed","variant","stages","row","stage","id","display","unplaced","emptyText","size","align","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/pipeline-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 * as Aglyn from '@aglyn/aglyn'\nimport { stuckDeals } from '../../model/crm-next-activity'\nimport { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { collectionCeiling } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename, shortDate } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { currencyOfDeals, pipelineTotals } from '../../model/crm-reports'\n\n/**\n * How many open deals the by-stage chart, the forecast and the top-deals\n * table are read from.\n *\n * The count and the face value of the pipeline are server aggregates and\n * never meet this bound. The forecast cannot be: it weights each deal by\n * ITS stage's odds, which is a join the server will not do, so it reads the\n * deals and says so when there were more than this.\n */\nconst OPEN_DEAL_CEILING = 1000\n/** More pipelines than this is a shape nobody has asked for; the probe says if it happens. */\nconst PIPELINE_CEILING = 20\nconst TOP_DEALS = 10\n\n/** The top-deals table's columns, which are also the CSV's — the amount split from its currency there. */\nconst TOP_DEAL_COLUMNS = ['Deal', 'Stage', 'Expected close', 'Amount', 'Currency'] as const\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\ntype PipelineRow = Aglyn.CrmPipeline & { $id: string }\n\nexport interface PipelineCardProps {\n report: CrmReportScope\n}\n\n/**\n * The open pipeline: how many deals, what they add up to, what they are\n * worth at their stage odds, where they sit, and the largest of them\n * (AGL-2604).\n *\n * Not period-scoped, deliberately. A pipeline is a stock, not a flow — what\n * is open NOW — and a \"pipeline over the last 30 days\" would be a report on\n * deals created in a window, which is a different question with a different\n * answer. The period picker governs the cards that measure flows.\n */\nexport function PipelineCard(props: PipelineCardProps) {\n const { report } = props\n const { scope, tokens, routes, nowMs } = report\n const firestore = useFirestore()\n\n const open = useAggregateRead(\n () =>\n getAggregateFromServer(\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n ),\n { count: count(), amountCents: sum('amountCents') },\n ).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:open') },\n )\n\n /**\n * The open deals, most recently updated first — the order the\n * `(visibleTo, status, updatedAt)` index the deals list already uses can\n * serve, so the report adds no index for this read.\n */\n const dealWindow = useWindowRead<DealRow>(\n () =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n orderBy('updatedAt', 'desc'),\n limit(OPEN_DEAL_CEILING + 1),\n ),\n OPEN_DEAL_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:deals') },\n )\n const pipelineWindow = useWindowRead<PipelineRow>(\n () =>\n collectionCeiling(\n query(\n scopedCollection(firestore, scope, 'pipelines'),\n ...visibleToClause(tokens),\n ),\n PIPELINE_CEILING,\n ),\n PIPELINE_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:pipelines') },\n )\n const dealsStatus = dealWindow.status\n\n const summary = useMemo(() => {\n const byPipeline = new Map<string, DealRow[]>()\n for (const deal of dealWindow.rows) {\n const key = deal.pipelineId ?? ''\n const list = byPipeline.get(key) ?? []\n list.push(deal)\n byPipeline.set(key, list)\n }\n // Every active pipeline is charted even with nothing open; an archived\n // one only while a deal still sits open in it — the forecast card's\n // rule, so the two cards name the same pipelines.\n const pipelines = pipelineWindow.rows\n .filter((pipeline) => !Aglyn.isPipelineArchived(pipeline) || byPipeline.has(pipeline.$id))\n .map((pipeline) => ({\n pipeline,\n totals: pipelineTotals(byPipeline.get(pipeline.$id) ?? [], pipeline),\n }))\n const known = new Set(pipelineWindow.rows.map((pipeline) => pipeline.$id))\n const orphans = dealWindow.rows.filter(\n (deal) => !known.has(deal.pipelineId ?? ''),\n )\n const stageName = (deal: DealRow): string =>\n Aglyn.dealStageById(\n pipelineWindow.rows.find((pipeline) => pipeline.$id === deal.pipelineId),\n deal.stageId,\n )?.name ?? deal.stageId\n const top = [...dealWindow.rows]\n .sort(\n (a, b) => Number(b.amountCents ?? 0) - Number(a.amountCents ?? 0),\n )\n .slice(0, TOP_DEALS)\n return {\n pipelines,\n orphanTotals: pipelineTotals(orphans, null),\n weightedCents: pipelines.reduce(\n (total, entry) => total + entry.totals.weightedCents,\n 0,\n ),\n top,\n stageName,\n currency: currencyOfDeals(dealWindow.rows),\n }\n }, [dealWindow, pipelineWindow])\n // Open deals with nothing scheduled (AGL-2661), off the same window.\n const stuck = useMemo(() => stuckDeals(dealWindow.rows), [dealWindow.rows])\n\n const currency = summary.currency.currency\n const dealsRead = dealsStatus === 'success'\n\n return (\n <CardDisplay\n header={'Pipeline'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#pipeline',\n excerpt:\n 'Every open deal: how many, what they add up to, and what they ' +\n 'are worth once each is weighted by the odds of its stage. Not ' +\n 'tied to the period — a pipeline is what is open now.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('top-open-deals')}\n columns={TOP_DEAL_COLUMNS}\n rows={() =>\n summary.top.map((deal) => [\n deal.title || deal.$id,\n summary.stageName(deal),\n typeof deal.expectedCloseAtMs === 'number' && deal.expectedCloseAtMs > 0\n ? new Date(deal.expectedCloseAtMs).toISOString().slice(0, 10)\n : '',\n (Number(deal.amountCents ?? 0) / 100).toFixed(2),\n String(deal.currency || currency).toUpperCase(),\n ])\n }\n disabled={!dealsRead || !summary.top.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open deals'}\n value={open.value ? open.value.count.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Pipeline value'}\n value={open.value ? money(open.value.amountCents, currency) : null}\n note={'face value of every open deal'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Weighted forecast'}\n value={dealsRead ? money(summary.weightedCents, currency) : null}\n note={\n dealWindow.truncated\n ? `from the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated`\n : 'each deal at its stage odds'\n }\n />\n {/*\n Open deals with no open task against them (AGL-2661): the ones\n nobody has a next step for. Painted as a warning above zero,\n because a deal with nothing planned is a deal going nowhere; the\n tile opens the deals list, whose \"No next activity\" chip lists\n them.\n */}\n <ReportStatTile\n label={'Stuck deals'}\n value={dealsRead ? stuck.count.toLocaleString() : null}\n note={'open, with no next activity'}\n riseIsGood={false}\n color={stuck.count > 0 ? 'warning.main' : undefined}\n href={routes.section('deals')}\n />\n </Stack>\n {open.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The deal totals could not be read.'}\n </Alert>\n ) : null}\n {summary.currency.mixed ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Deals are in more than one currency; the totals add them as numbers and show ${currency.toUpperCase()}, the most common.`}\n </Typography>\n ) : null}\n {dealsRead && !summary.pipelines.length && !dealWindow.rows.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No open deals yet.'}\n </Typography>\n ) : null}\n {summary.pipelines.map(({ pipeline, totals }) => (\n <Section key={pipeline.$id} title={pipeline.name || 'Pipeline'}>\n <ReportBreakdown\n rows={[\n ...totals.stages.map((row) => ({\n key: row.stage.id,\n label: row.stage.name,\n value: row.amountCents,\n display: money(row.amountCents, currency),\n note: `${plural(row.count, 'deal')} · ${money(row.weightedCents, currency)} weighted`,\n })),\n ...(totals.unplaced.count\n ? [\n {\n key: '$unplaced',\n label: 'Stage no longer in this pipeline',\n value: totals.unplaced.amountCents,\n display: money(totals.unplaced.amountCents, currency),\n note: `${plural(totals.unplaced.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]\n : []),\n ]}\n emptyText={'This pipeline has no open stages.'}\n />\n </Section>\n ))}\n {summary.orphanTotals.count ? (\n <Section title={'Deals in no pipeline'}>\n <ReportBreakdown\n rows={[\n {\n key: '$orphans',\n label: 'No pipeline',\n value: summary.orphanTotals.amountCents,\n display: money(summary.orphanTotals.amountCents, currency),\n note: `${plural(summary.orphanTotals.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]}\n emptyText={''}\n />\n </Section>\n ) : null}\n {pipelineWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Only the first ${PIPELINE_CEILING} pipelines are charted.`}\n </Typography>\n ) : null}\n {summary.top.length ? (\n <Section title={'Top open deals'}>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Deal'}</TableCell>\n <TableCell>{'Stage'}</TableCell>\n <TableCell>{'Expected close'}</TableCell>\n <TableCell align=\"right\">{'Amount'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {summary.top.map((deal) => (\n <TableRow key={deal.$id}>\n <TableCell>\n <AppLink href={routes.deal(deal.$id)}>\n {deal.title || deal.$id}\n </AppLink>\n </TableCell>\n <TableCell>{summary.stageName(deal)}</TableCell>\n <TableCell>{shortDate(deal.expectedCloseAtMs)}</TableCell>\n <TableCell align=\"right\">\n {money(\n Number(deal.amountCents ?? 0),\n String(deal.currency || currency),\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {/* What the ranking, and so the export, is drawn from when that is not everything. */}\n {dealWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Ranked within the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.`}\n </Typography>\n ) : null}\n </Section>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nPipelineCard.displayName = 'PipelineCard'\n\nexport default PipelineCard\n"],"names":["Aglyn","stuckDeals","money","AppLink","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","count","getAggregateFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","collectionCeiling","ReportBreakdown","ReportExport","plural","reportFilename","shortDate","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","currencyOfDeals","pipelineTotals","OPEN_DEAL_CEILING","PIPELINE_CEILING","TOP_DEALS","TOP_DEAL_COLUMNS","PipelineCard","props","report","scope","tokens","routes","nowMs","firestore","open","amountCents","then","snapshot","Number","data","cacheKey","dealWindow","pipelineWindow","dealsStatus","status","summary","byPipeline","Map","deal","rows","key","pipelineId","list","get","push","set","pipelines","filter","pipeline","isPipelineArchived","has","$id","map","totals","known","Set","orphans","stageName","dealStageById","find","stageId","name","top","sort","a","b","slice","orphanTotals","weightedCents","reduce","total","entry","currency","stuck","dealsRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","title","expectedCloseAtMs","Date","toISOString","toFixed","String","toUpperCase","disabled","length","spacing","direction","sx","flexWrap","label","value","toLocaleString","note","href","section","truncated","riseIsGood","color","undefined","severity","mixed","variant","stages","row","stage","id","display","unplaced","emptyText","size","align","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,YAAYA,WAAW,eAAc;AACrC,SAASC,UAAU,QAAQ,mCAA+B;AAC1D,SAASC,KAAK,QAAQ,6DAA4D;AAClF,SAASC,OAAO,EAAEC,WAAW,QAAQ,uBAAsB;AAC3D,SAASC,OAAO,QAAQ,6DAA4D;AACpF,SAASC,WAAW,QAAQ,yDAAwD;AACpF,SACEC,KAAK,EACLC,KAAK,EACLC,SAAS,EACTC,SAAS,EACTC,SAAS,EACTC,QAAQ,EACRC,UAAU,QACL,gBAAe;AACtB,SACEC,KAAK,EACLC,sBAAsB,EACtBC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,GAAG,EACHC,KAAK,QACA,qBAAoB;AAC3B,SAASC,OAAO,QAAQ,QAAO;AAC/B,SAASC,YAAY,QAAQ,iCAAgC;AAC7D,SAASC,iBAAiB,QAAQ,+DAA8D;AAChG,SAASC,eAAe,QAAQ,wBAAoB;AACpD,SAASC,YAAY,QAAQ,qBAAiB;AAC9C,SAASC,MAAM,EAAEC,cAAc,EAAEC,SAAS,QAAQ,qBAAiB;AACnE,SAEEC,cAAc,EACdC,gBAAgB,EAChBC,eAAe,QACV,oBAAgB;AACvB,SAASC,cAAc,QAAQ,wBAAoB;AACnD,SAASC,gBAAgB,EAAEC,aAAa,QAAQ,0BAAsB;AACtE,SAASC,eAAe,EAAEC,cAAc,QAAQ,6BAAyB;AAEzE;;;;;;;;CAQC,GACD,MAAMC,oBAAoB;AAC1B,4FAA4F,GAC5F,MAAMC,mBAAmB;AACzB,MAAMC,YAAY;AAElB,wGAAwG,GACxG,MAAMC,mBAAmB;IAAC;IAAQ;IAAS;IAAkB;IAAU;CAAW;AASlF;;;;;;;;;CASC,GACD,OAAO,SAASC,aAAaC,KAAwB;IACnD,MAAM,EAAEC,MAAM,EAAE,GAAGD;IACnB,MAAM,EAAEE,KAAK,EAAEC,MAAM,EAAEC,MAAM,EAAEC,KAAK,EAAE,GAAGJ;IACzC,MAAMK,YAAY1B;IAElB,MAAM2B,OAAOhB,iBACX,IACElB,uBACEG,MACEY,iBAAiBkB,WAAWJ,OAAO,aAChCb,gBAAgBc,SACnBzB,MAAM,UAAU,MAAM,UAExB;YAAEN,OAAOA;YAASoC,aAAa/B,IAAI;QAAe,GAClDgC,IAAI,CAAC,CAACC;gBACQA,sBACMA;mBAFA;gBACpBtC,OAAOuC,QAAOD,uBAAAA,SAASE,IAAI,GAAGxC,KAAK,YAArBsC,uBAAyB;gBACvCF,aAAaG,QAAOD,6BAAAA,SAASE,IAAI,GAAGJ,WAAW,YAA3BE,6BAA+B;YACrD;YACF;QAACJ;QAAWJ;QAAOC;QAAQE;KAAM,EACjC;QAAEQ,UAAU1B,eAAec,QAAQ;IAAiB;IAGtD;;;;GAIC,GACD,MAAMa,aAAatB,cACjB,IACEhB,MACEY,iBAAiBkB,WAAWJ,OAAO,aAChCb,gBAAgBc,SACnBzB,MAAM,UAAU,MAAM,SACtBH,QAAQ,aAAa,SACrBD,MAAMqB,oBAAoB,KAE9BA,mBACA;QAACW;QAAWJ;QAAOC;QAAQE;KAAM,EACjC;QAAEQ,UAAU1B,eAAec,QAAQ;IAAkB;IAEvD,MAAMc,iBAAiBvB,cACrB,IACEX,kBACEL,MACEY,iBAAiBkB,WAAWJ,OAAO,iBAChCb,gBAAgBc,UAErBP,mBAEJA,kBACA;QAACU;QAAWJ;QAAOC;QAAQE;KAAM,EACjC;QAAEQ,UAAU1B,eAAec,QAAQ;IAAsB;IAE3D,MAAMe,cAAcF,WAAWG,MAAM;IAErC,MAAMC,UAAUvC,QAAQ;QACtB,MAAMwC,aAAa,IAAIC;QACvB,KAAK,MAAMC,QAAQP,WAAWQ,IAAI,CAAE;gBACtBD,kBACCF;YADb,MAAMI,OAAMF,mBAAAA,KAAKG,UAAU,YAAfH,mBAAmB;YAC/B,MAAMI,QAAON,kBAAAA,WAAWO,GAAG,CAACH,gBAAfJ,kBAAuB,EAAE;YACtCM,KAAKE,IAAI,CAACN;YACVF,WAAWS,GAAG,CAACL,KAAKE;QACtB;QACA,uEAAuE;QACvE,oEAAoE;QACpE,kDAAkD;QAClD,MAAMI,YAAYd,eAAeO,IAAI,CAClCQ,MAAM,CAAC,CAACC,WAAa,CAACzE,MAAM0E,kBAAkB,CAACD,aAAaZ,WAAWc,GAAG,CAACF,SAASG,GAAG,GACvFC,GAAG,CAAC,CAACJ;gBAEmBZ;mBAFL;gBAClBY;gBACAK,QAAQ1C,gBAAeyB,kBAAAA,WAAWO,GAAG,CAACK,SAASG,GAAG,aAA3Bf,kBAAgC,EAAE,EAAEY;YAC7D;;QACF,MAAMM,QAAQ,IAAIC,IAAIvB,eAAeO,IAAI,CAACa,GAAG,CAAC,CAACJ,WAAaA,SAASG,GAAG;QACxE,MAAMK,UAAUzB,WAAWQ,IAAI,CAACQ,MAAM,CACpC,CAACT;gBAAoBA;mBAAX,CAACgB,MAAMJ,GAAG,EAACZ,mBAAAA,KAAKG,UAAU,YAAfH,mBAAmB;;QAE1C,MAAMmB,YAAY,CAACnB;;gBACjB/D;4BAAAA,uBAAAA,MAAMmF,aAAa,CACjB1B,eAAeO,IAAI,CAACoB,IAAI,CAAC,CAACX,WAAaA,SAASG,GAAG,KAAKb,KAAKG,UAAU,GACvEH,KAAKsB,OAAO,sBAFdrF,qBAGGsF,IAAI,mBAAIvB,KAAKsB,OAAO;;QACzB,MAAME,MAAM;eAAI/B,WAAWQ,IAAI;SAAC,CAC7BwB,IAAI,CACH,CAACC,GAAGC;gBAAaA,gBAA6BD;mBAApCpC,QAAOqC,iBAAAA,EAAExC,WAAW,YAAbwC,iBAAiB,KAAKrC,QAAOoC,iBAAAA,EAAEvC,WAAW,YAAbuC,iBAAiB;WAEhEE,KAAK,CAAC,GAAGpD;QACZ,OAAO;YACLgC;YACAqB,cAAcxD,eAAe6C,SAAS;YACtCY,eAAetB,UAAUuB,MAAM,CAC7B,CAACC,OAAOC,QAAUD,QAAQC,MAAMlB,MAAM,CAACe,aAAa,EACpD;YAEFN;YACAL;YACAe,UAAU9D,gBAAgBqB,WAAWQ,IAAI;QAC3C;IACF,GAAG;QAACR;QAAYC;KAAe;IAC/B,qEAAqE;IACrE,MAAMyC,QAAQ7E,QAAQ,IAAMpB,WAAWuD,WAAWQ,IAAI,GAAG;QAACR,WAAWQ,IAAI;KAAC;IAE1E,MAAMiC,WAAWrC,QAAQqC,QAAQ,CAACA,QAAQ;IAC1C,MAAME,YAAYzC,gBAAgB;IAElC,qBACE,KAACtD;QACCgG,QAAQ;QACRC,MAAMrG,MAAMsG,cAAc,CAAC,cAAc;YACvCC,QAAQ;YACRC,SACE,mEACA,mEACA;QACJ;QACAC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,sBACE,KAACnF;gBACCoF,UAAUlF,eAAe;gBACzBmF,SAAStE;gBACTwB,MAAM,IACJJ,QAAQ2B,GAAG,CAACV,GAAG,CAAC,CAACd;4BAMPA;+BANgB;4BACxBA,KAAKgD,KAAK,IAAIhD,KAAKa,GAAG;4BACtBhB,QAAQsB,SAAS,CAACnB;4BAClB,OAAOA,KAAKiD,iBAAiB,KAAK,YAAYjD,KAAKiD,iBAAiB,GAAG,IACnE,IAAIC,KAAKlD,KAAKiD,iBAAiB,EAAEE,WAAW,GAAGvB,KAAK,CAAC,GAAG,MACxD;4BACHtC,CAAAA,QAAOU,oBAAAA,KAAKb,WAAW,YAAhBa,oBAAoB,KAAK,GAAE,EAAGoD,OAAO,CAAC;4BAC9CC,OAAOrD,KAAKkC,QAAQ,IAAIA,UAAUoB,WAAW;yBAC9C;;gBAEHC,UAAU,CAACnB,aAAa,CAACvC,QAAQ2B,GAAG,CAACgC,MAAM;;QAGjD;kBAEA,cAAA,MAAC/G;YAAMgH,SAAS;;8BACd,MAAChH;oBAAMiH,WAAU;oBAAMD,SAAS;oBAAGE,IAAI;wBAAEC,UAAU;oBAAO;;sCACxD,KAAC3F;4BACC4F,OAAO;4BACPC,OAAO5E,KAAK4E,KAAK,GAAG5E,KAAK4E,KAAK,CAAC/G,KAAK,CAACgH,cAAc,KAAK;4BACxDC,MAAM;4BACNC,MAAMlF,OAAOmF,OAAO,CAAC;;sCAEvB,KAACjG;4BACC4F,OAAO;4BACPC,OAAO5E,KAAK4E,KAAK,GAAG3H,MAAM+C,KAAK4E,KAAK,CAAC3E,WAAW,EAAE+C,YAAY;4BAC9D8B,MAAM;4BACNC,MAAMlF,OAAOmF,OAAO,CAAC;;sCAEvB,KAACjG;4BACC4F,OAAO;4BACPC,OAAO1B,YAAYjG,MAAM0D,QAAQiC,aAAa,EAAEI,YAAY;4BAC5D8B,MACEvE,WAAW0E,SAAS,GAChB,CAAC,SAAS,EAAE7F,kBAAkByF,cAAc,GAAG,sBAAsB,CAAC,GACtE;;sCAUR,KAAC9F;4BACC4F,OAAO;4BACPC,OAAO1B,YAAYD,MAAMpF,KAAK,CAACgH,cAAc,KAAK;4BAClDC,MAAM;4BACNI,YAAY;4BACZC,OAAOlC,MAAMpF,KAAK,GAAG,IAAI,iBAAiBuH;4BAC1CL,MAAMlF,OAAOmF,OAAO,CAAC;;;;gBAGxBhF,KAAKU,MAAM,KAAK,wBACf,KAACpD;oBAAM+H,UAAS;8BACb;qBAED;gBACH1E,QAAQqC,QAAQ,CAACsC,KAAK,iBACrB,KAAC1H;oBAAW2H,SAAQ;oBAAUJ,OAAM;8BACjC,CAAC,6EAA6E,EAAEnC,SAASoB,WAAW,GAAG,kBAAkB,CAAC;qBAE3H;gBACHlB,aAAa,CAACvC,QAAQW,SAAS,CAACgD,MAAM,IAAI,CAAC/D,WAAWQ,IAAI,CAACuD,MAAM,iBAChE,KAAC1G;oBAAW2H,SAAQ;oBAAQJ,OAAM;8BAC/B;qBAED;gBACHxE,QAAQW,SAAS,CAACM,GAAG,CAAC,CAAC,EAAEJ,QAAQ,EAAEK,MAAM,EAAE,iBAC1C,KAACzE;wBAA2B0G,OAAOtC,SAASa,IAAI,IAAI;kCAClD,cAAA,KAAC9D;4BACCwC,MAAM;mCACDc,OAAO2D,MAAM,CAAC5D,GAAG,CAAC,CAAC6D,MAAS,CAAA;wCAC7BzE,KAAKyE,IAAIC,KAAK,CAACC,EAAE;wCACjBhB,OAAOc,IAAIC,KAAK,CAACrD,IAAI;wCACrBuC,OAAOa,IAAIxF,WAAW;wCACtB2F,SAAS3I,MAAMwI,IAAIxF,WAAW,EAAE+C;wCAChC8B,MAAM,GAAGrG,OAAOgH,IAAI5H,KAAK,EAAE,QAAQ,GAAG,EAAEZ,MAAMwI,IAAI7C,aAAa,EAAEI,UAAU,SAAS,CAAC;oCACvF,CAAA;mCACInB,OAAOgE,QAAQ,CAAChI,KAAK,GACrB;oCACE;wCACEmD,KAAK;wCACL2D,OAAO;wCACPC,OAAO/C,OAAOgE,QAAQ,CAAC5F,WAAW;wCAClC2F,SAAS3I,MAAM4E,OAAOgE,QAAQ,CAAC5F,WAAW,EAAE+C;wCAC5C8B,MAAM,GAAGrG,OAAOoD,OAAOgE,QAAQ,CAAChI,KAAK,EAAE,QAAQ,eAAe,CAAC;wCAC/DsH,OAAO;oCACT;iCACD,GACD,EAAE;6BACP;4BACDW,WAAW;;uBAvBDtE,SAASG,GAAG;gBA2B3BhB,QAAQgC,YAAY,CAAC9E,KAAK,iBACzB,KAACT;oBAAQ0G,OAAO;8BACd,cAAA,KAACvF;wBACCwC,MAAM;4BACJ;gCACEC,KAAK;gCACL2D,OAAO;gCACPC,OAAOjE,QAAQgC,YAAY,CAAC1C,WAAW;gCACvC2F,SAAS3I,MAAM0D,QAAQgC,YAAY,CAAC1C,WAAW,EAAE+C;gCACjD8B,MAAM,GAAGrG,OAAOkC,QAAQgC,YAAY,CAAC9E,KAAK,EAAE,QAAQ,eAAe,CAAC;gCACpEsH,OAAO;4BACT;yBACD;wBACDW,WAAW;;qBAGb;gBACHtF,eAAeyE,SAAS,iBACvB,KAACrH;oBAAW2H,SAAQ;oBAAUJ,OAAM;8BACjC,CAAC,eAAe,EAAE9F,iBAAiB,uBAAuB,CAAC;qBAE5D;gBACHsB,QAAQ2B,GAAG,CAACgC,MAAM,iBACjB,MAAClH;oBAAQ0G,OAAO;;sCACd,MAACzG;4BAAY0I,MAAK;;8CAChB,KAACrI;8CACC,cAAA,MAACC;;0DACC,KAACF;0DAAW;;0DACZ,KAACA;0DAAW;;0DACZ,KAACA;0DAAW;;0DACZ,KAACA;gDAAUuI,OAAM;0DAAS;;;;;8CAG9B,KAACxI;8CACEmD,QAAQ2B,GAAG,CAACV,GAAG,CAAC,CAACd;4CAWHA;6DAVb,MAACnD;;8DACC,KAACF;8DACC,cAAA,KAACP;wDAAQ6H,MAAMlF,OAAOiB,IAAI,CAACA,KAAKa,GAAG;kEAChCb,KAAKgD,KAAK,IAAIhD,KAAKa,GAAG;;;8DAG3B,KAAClE;8DAAWkD,QAAQsB,SAAS,CAACnB;;8DAC9B,KAACrD;8DAAWkB,UAAUmC,KAAKiD,iBAAiB;;8DAC5C,KAACtG;oDAAUuI,OAAM;8DACd/I,MACCmD,QAAOU,oBAAAA,KAAKb,WAAW,YAAhBa,oBAAoB,IAC3BqD,OAAOrD,KAAKkC,QAAQ,IAAIA;;;2CAXflC,KAAKa,GAAG;;;;;wBAmB5BpB,WAAW0E,SAAS,iBACnB,KAACrH;4BAAW2H,SAAQ;4BAAUJ,OAAM;sCACjC,CAAC,kBAAkB,EAAE/F,kBAAkByF,cAAc,GAAG,kCAAkC,CAAC;6BAE5F;;qBAEJ;;;;AAIZ;AACArF,aAAayG,WAAW,GAAG;AAE3B,eAAezG,aAAY"}
|
|
@@ -116,6 +116,11 @@ import { summarizeConsentGroupChange } from "./model/consent-group-summary.js";
|
|
|
116
116
|
const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } = await import("./server/record-timeline.js");
|
|
117
117
|
return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).recordEmailDelivery(request);
|
|
118
118
|
},
|
|
119
|
+
// A delivery event for a message the CRM tagged (AGL-2615, AGL-3080).
|
|
120
|
+
async recordTaggedDelivery (request) {
|
|
121
|
+
const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } = await import("./server/record-timeline.js");
|
|
122
|
+
return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).recordTaggedDelivery(request);
|
|
123
|
+
},
|
|
119
124
|
// An automation's email to the person its event is about, filed on that
|
|
120
125
|
// person's timeline with its delivery state (AGL-2615, AGL-3080).
|
|
121
126
|
async prepareEmail (request) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/declarations.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 contract from its own module, not the plugin-manager barrel: boot needs\n// the registry and nothing else, and the barrel reaches the client contexts.\nimport { registerPluginConsentGroupParticipant } from '@aglyn/aglyn/plugin-manager/plugin-consent-group-change'\nimport {\n registerPluginContactCaptureWriter,\n type PluginContactCaptureWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport { registerPluginLeadConversionListener } from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { registerPluginPersonMatcher } from '@aglyn/aglyn/plugin-manager/plugin-person-matches'\nimport {\n registerPluginPersonRecords,\n type PluginPersonRecords,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-records'\nimport {\n registerPluginRecordIndex,\n type PluginRecordIndex,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { registerPluginRecordEmailStateWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\nimport { registerPluginRecordOriginWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-origin'\nimport {\n registerPluginRecordTimelineWriter,\n type PluginRecordTimelineWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { registerPluginRecordWrittenListener } from '@aglyn/aglyn/plugin-manager/plugin-record-written'\nimport { registerServerStepExecutor } from '@aglyn/aglyn/plugin-manager/plugin-server-steps'\nimport { registerPluginUsageMeter } from '@aglyn/aglyn/plugin-manager/plugin-usage-meters'\nimport { BUNDLE_ID, CRM_RECORDS_METER_ID, CRM_STEP_TYPES } from './constants/bundle-common'\nimport { summarizeConsentGroupChange } from './model/consent-group-summary'\n\n/**\n * The CRM as the plugin that keeps people.\n *\n * Light by construction, the way the workflows listener is: the capture\n * itself is imported when the first one arrives, not when the process\n * starts, so a process that never captures anybody pays for this object and\n * nothing else. That matters because this registers in EVERY server process,\n * including ones that will never touch the CRM.\n */\nexport const crmContactCaptureWriter: PluginContactCaptureWriter = {\n async capture(request) {\n const { captureContactForCrm } = await import('./server/capture-contact')\n return await captureContactForCrm(request)\n },\n}\n\n/**\n * The CRM as the plugin other plugins ask for people (AGL-3080), deferred\n * like the capture: `server/person-records.ts` and the Admin SDK it brings\n * load with the first question, not when the process starts.\n */\nexport const crmPersonRecordsService: PluginPersonRecords = {\n async find(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.find(request)\n },\n async read(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.read(request)\n },\n async fileUnder(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.fileUnder(request)\n },\n async recordRefund(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.recordRefund(request)\n },\n async peopleInView(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.peopleInView(request)\n },\n async wroteIn(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.wroteIn(request)\n },\n}\n\n/**\n * The `pipeline` record index, its reads loaded with the first one — see\n * `server/pipeline-record-index.ts` for what it answers.\n */\nexport const crmPipelineRecordIndex: PluginRecordIndex = {\n async list(request) {\n const { pipelineRecordIndex } = await import('./server/pipeline-record-index')\n return pipelineRecordIndex.list(request)\n },\n async get(request) {\n const { pipelineRecordIndex } = await import('./server/pipeline-record-index')\n return pipelineRecordIndex.get(request)\n },\n}\n\n/** One of `server/crm-record-indexes.ts`'s indexes, its reads loaded with the first one. */\nfunction lazyCrmRecordIndex(kind: 'company' | 'messageTemplate'): PluginRecordIndex {\n return {\n async list(request) {\n const { crmRecordIndexes } = await import('./server/crm-record-indexes')\n return crmRecordIndexes()[kind].list(request)\n },\n async get(request) {\n const { crmRecordIndexes } = await import('./server/crm-record-indexes')\n return crmRecordIndexes()[kind].get(request)\n },\n }\n}\n\n/** The companies a person works for, as another plugin reads them (AGL-3080). */\nexport const crmCompanyRecordIndex = lazyCrmRecordIndex('company')\n\n/** The email templates and snippets reps write, as another plugin reads them (AGL-3080). */\nexport const crmMessageTemplateRecordIndex = lazyCrmRecordIndex('messageTemplate')\n\n/**\n * The CRM's writer on the record-timeline seam (AGL-2981), deferred: the\n * writer and the Admin SDK it brings load with the first entry filed.\n */\nexport const crmRecordTimelineWriter: PluginRecordTimelineWriter = {\n async logActivity(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).logActivity(request)\n },\n async createTask(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).createTask(request)\n },\n async recordEmailDelivery(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).recordEmailDelivery!(request)\n },\n // An automation's email to the person its event is about, filed on that\n // person's timeline with its delivery state (AGL-2615, AGL-3080).\n async prepareEmail(request) {\n const { prepareCrmRecordEmail } = await import('./server/automation-steps')\n return prepareCrmRecordEmail(request)\n },\n}\n\n/**\n * The plugin's SERVER declarations: what the server must know at boot, before\n * any door of the CRM has been called.\n *\n * ## Why a declaration and not a `tenantApi` registration\n *\n * The doors that meet a person mostly never load this plugin. A form\n * submission is a core route; an order and a booking are captured inside\n * STRIPE WEBHOOKS. None of them has a reason to load the CRM, and a registry\n * filled by a surface that did not load answers `null` — which\n * `capturePluginContact` defines as \"this workspace has no record system\", a\n * sentence that would be false and silent. Orders would stop creating\n * contacts with nothing red anywhere, which is the AGL-3025 shape.\n *\n * Running from both apps' generated server-declarations manifest at boot is\n * what makes the answer true in every process. It is also called from the\n * plugin's own API register functions, so a process whose boot did not run it\n * still registers the writer the first time a CRM door loads. Registering\n * twice replaces in place.\n *\n * ⚠️ The registry holds ONE writer: a workspace keeps one set of people. A\n * second plugin's is refused naming both, and the incumbent keeps serving.\n */\nexport function registerCrmServerDeclarations(): void {\n registerPluginContactCaptureWriter(crmContactCaptureWriter, {\n pluginId: BUNDLE_ID,\n })\n // The other half of keeping people (AGL-3080): another plugin that holds\n // an address, or a record the CRM handed it, asks here for the person —\n // a flow email's consent read, a refund, an automation filing somebody\n // under a campaign — and never opens the CRM's collections itself.\n registerPluginPersonRecords(crmPersonRecordsService, { pluginId: BUNDLE_ID })\n // The CRM's own share of a lead conversion (AGL-3254): the lead's\n // campaigns go onto the contact's facet. Through the seam every door\n // that converts a lead reaches, and deferred like the capture: the\n // module that writes is loaded when the first conversion arrives, and\n // it brings the Admin SDK with it — this file defers the plugin's OWN\n // module only. A library imported statically across the plugin cannot\n // also be lazy-loaded here: `enforce-module-boundaries` refuses every\n // static import of a library the project lazy-loads anywhere.\n registerPluginLeadConversionListener(\n async (request) => {\n const { carryLeadCampaignsOnConversion } =\n await import('./server/lead-campaign-carry')\n return carryLeadCampaignsOnConversion(request)\n },\n { pluginId: BUNDLE_ID },\n )\n // \"Did this workspace already know this person?\" (AGL-3289), asked by the\n // staff console's acquisition card. Deferred like the rest: the reads load\n // when a staff member first opens a card, not when the process starts.\n registerPluginPersonMatcher(\n async (request) => {\n const { matchCrmPeople } = await import('./server/person-matches')\n return matchCrmPeople(request)\n },\n { pluginId: BUNDLE_ID },\n )\n // The CRM's share of a consent group change (AGL-3320): a contact's\n // per-site refusal carried across a separation, and the records keyed by a\n // group re-homed after the flip. Registered whether or not a workspace has\n // the CRM switched on, because the contacts — and the refusals on them —\n // outlive the switch. Deferred like the rest; the summary is plain words\n // and answers synchronously.\n // The record email-state writer (AGL-3245), here as well as in the console\n // API so the tenant's doors reach it too: a form submission's capture is\n // checked for deliverability after its response (AGL-3328), and the \"Would\n // bounce\" that check finds is written through this seam. Deferred like the\n // rest; the console's own registration replaces this one with the same\n // writer, loaded eagerly.\n registerPluginRecordEmailStateWriter(\n {\n async stamp(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).stamp(request)\n },\n // A send's first opens and clicks, stamped on the contacts they name\n // (AGL-2616) — the campaign webhook's courtesy, deferred like the rest.\n async engaged(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).engaged!(request)\n },\n // A delivered campaign's leads, moved to Nurturing (AGL-3446).\n async reached(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).reached!(request)\n },\n // A lead that wrote back, moved to Working (AGL-3080).\n async replied(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).replied!(request)\n },\n },\n { pluginId: BUNDLE_ID },\n )\n // The record timeline writer (AGL-2981), here as well as in the API\n // surfaces so a door in either app reaches it: a booking confirmed on the\n // tenant, or by the payment webhook, files its meeting and follow-up\n // through it (AGL-2660). Deferred like the rest; an API registration\n // replaces this one with the same writer, loaded eagerly.\n registerPluginRecordTimelineWriter(crmRecordTimelineWriter, { pluginId: BUNDLE_ID })\n // Where a person came from (AGL-3519): a door the CRM does not run — the\n // platform's account sign-up, a sequence enrolling someone — names its\n // origin, and the CRM stamps the built-in Lead source on a record holding\n // none. Deferred like the rest.\n registerPluginRecordOriginWriter(\n {\n async stamp(request) {\n const { crmRecordOriginWriter } = await import('./server/record-origin')\n return crmRecordOriginWriter.stamp(request)\n },\n },\n { pluginId: BUNDLE_ID },\n )\n // Sharing rules (AGL-3336), re-evaluated after every server write of a\n // lead, a contact, a company or a deal: the core tells its record-written\n // listeners from the list-field restamp every such writer ends with, in\n // both apps — a form's capture on the tenant, an edit in the console.\n // Deferred like the rest: an org with no rules pays one cached read.\n registerPluginRecordWrittenListener(\n async (event) => {\n const { crmSharingRecordWritten } = await import('./server/crm-sharing')\n await crmSharingRecordWritten(event)\n },\n { pluginId: BUNDLE_ID, key: 'sharing' },\n )\n // The organization's pipelines and their stages, as another plugin reads\n // them (AGL-3080): an automation the AI drafts names the stage a deal\n // moves to. Deferred like the rest: the reads load with the first one.\n registerPluginRecordIndex('pipeline', crmPipelineRecordIndex, { pluginId: BUNDLE_ID })\n // The company a person works for, and the templates reps write, as a sales\n // sequence reads them (AGL-3080). Deferred like the rest.\n registerPluginRecordIndex('company', crmCompanyRecordIndex, { pluginId: BUNDLE_ID })\n registerPluginRecordIndex('messageTemplate', crmMessageTemplateRecordIndex, { pluginId: BUNDLE_ID })\n // The automation steps that write the CRM (AGL-2605, AGL-3080), run for the\n // workflows engine through the server-step seam and declared under\n // `serverSteps`. Deferred like the rest: the writes load with the first step.\n registerServerStepExecutor(\n CRM_STEP_TYPES,\n async (request) => (await import('./server/automation-steps')).runCrmAutomationStep(request),\n { pluginId: BUNDLE_ID },\n )\n registerPluginConsentGroupParticipant(\n {\n async preview(request) {\n const { consentGroupParticipant } = await import('./server/consent-group-participant')\n return consentGroupParticipant.preview(request)\n },\n async run(request) {\n const { consentGroupParticipant } = await import('./server/consent-group-participant')\n return consentGroupParticipant.run(request)\n },\n summarize: summarizeConsentGroupChange,\n },\n { pluginId: BUNDLE_ID },\n )\n // The records band in the monthly usage sweep, a core cron that never\n // loads a CRM door; declared in `usageAxes` so the sweep refuses to bill a\n // month without it. Deferred like the rest: the counts load with the\n // first sweep.\n registerPluginUsageMeter({\n pluginId: BUNDLE_ID,\n id: CRM_RECORDS_METER_ID,\n measure: async (context) => {\n const { measureCrmRecords } = await import('./server/crm-records-meter')\n return measureCrmRecords(context)\n },\n })\n}\n\n"],"names":["registerPluginConsentGroupParticipant","registerPluginContactCaptureWriter","registerPluginLeadConversionListener","registerPluginPersonMatcher","registerPluginPersonRecords","registerPluginRecordIndex","registerPluginRecordEmailStateWriter","registerPluginRecordOriginWriter","registerPluginRecordTimelineWriter","registerPluginRecordWrittenListener","registerServerStepExecutor","registerPluginUsageMeter","BUNDLE_ID","CRM_RECORDS_METER_ID","CRM_STEP_TYPES","summarizeConsentGroupChange","crmContactCaptureWriter","capture","request","captureContactForCrm","crmPersonRecordsService","find","crmPersonRecords","read","fileUnder","recordRefund","peopleInView","wroteIn","crmPipelineRecordIndex","list","pipelineRecordIndex","get","lazyCrmRecordIndex","kind","crmRecordIndexes","crmCompanyRecordIndex","crmMessageTemplateRecordIndex","crmRecordTimelineWriter","logActivity","createCrmRecordTimelineWriter","defaultCrmRecordTimelineDeps","createTask","recordEmailDelivery","prepareEmail","prepareCrmRecordEmail","registerCrmServerDeclarations","pluginId","carryLeadCampaignsOnConversion","matchCrmPeople","stamp","createCrmRecordEmailStateWriter","defaultCrmRecordEmailStateDeps","engaged","reached","replied","crmRecordOriginWriter","event","crmSharingRecordWritten","key","runCrmAutomationStep","preview","consentGroupParticipant","run","summarize","id","measure","context","measureCrmRecords"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/declarations.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 contract from its own module, not the plugin-manager barrel: boot needs\n// the registry and nothing else, and the barrel reaches the client contexts.\nimport { registerPluginConsentGroupParticipant } from '@aglyn/aglyn/plugin-manager/plugin-consent-group-change'\nimport {\n registerPluginContactCaptureWriter,\n type PluginContactCaptureWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport { registerPluginLeadConversionListener } from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { registerPluginPersonMatcher } from '@aglyn/aglyn/plugin-manager/plugin-person-matches'\nimport {\n registerPluginPersonRecords,\n type PluginPersonRecords,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-records'\nimport {\n registerPluginRecordIndex,\n type PluginRecordIndex,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { registerPluginRecordEmailStateWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\nimport { registerPluginRecordOriginWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-origin'\nimport {\n registerPluginRecordTimelineWriter,\n type PluginRecordTimelineWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport { registerPluginRecordWrittenListener } from '@aglyn/aglyn/plugin-manager/plugin-record-written'\nimport { registerServerStepExecutor } from '@aglyn/aglyn/plugin-manager/plugin-server-steps'\nimport { registerPluginUsageMeter } from '@aglyn/aglyn/plugin-manager/plugin-usage-meters'\nimport { BUNDLE_ID, CRM_RECORDS_METER_ID, CRM_STEP_TYPES } from './constants/bundle-common'\nimport { summarizeConsentGroupChange } from './model/consent-group-summary'\n\n/**\n * The CRM as the plugin that keeps people.\n *\n * Light by construction, the way the workflows listener is: the capture\n * itself is imported when the first one arrives, not when the process\n * starts, so a process that never captures anybody pays for this object and\n * nothing else. That matters because this registers in EVERY server process,\n * including ones that will never touch the CRM.\n */\nexport const crmContactCaptureWriter: PluginContactCaptureWriter = {\n async capture(request) {\n const { captureContactForCrm } = await import('./server/capture-contact')\n return await captureContactForCrm(request)\n },\n}\n\n/**\n * The CRM as the plugin other plugins ask for people (AGL-3080), deferred\n * like the capture: `server/person-records.ts` and the Admin SDK it brings\n * load with the first question, not when the process starts.\n */\nexport const crmPersonRecordsService: PluginPersonRecords = {\n async find(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.find(request)\n },\n async read(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.read(request)\n },\n async fileUnder(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.fileUnder(request)\n },\n async recordRefund(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.recordRefund(request)\n },\n async peopleInView(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.peopleInView(request)\n },\n async wroteIn(request) {\n const { crmPersonRecords } = await import('./server/person-records')\n return crmPersonRecords.wroteIn(request)\n },\n}\n\n/**\n * The `pipeline` record index, its reads loaded with the first one — see\n * `server/pipeline-record-index.ts` for what it answers.\n */\nexport const crmPipelineRecordIndex: PluginRecordIndex = {\n async list(request) {\n const { pipelineRecordIndex } = await import('./server/pipeline-record-index')\n return pipelineRecordIndex.list(request)\n },\n async get(request) {\n const { pipelineRecordIndex } = await import('./server/pipeline-record-index')\n return pipelineRecordIndex.get(request)\n },\n}\n\n/** One of `server/crm-record-indexes.ts`'s indexes, its reads loaded with the first one. */\nfunction lazyCrmRecordIndex(kind: 'company' | 'messageTemplate'): PluginRecordIndex {\n return {\n async list(request) {\n const { crmRecordIndexes } = await import('./server/crm-record-indexes')\n return crmRecordIndexes()[kind].list(request)\n },\n async get(request) {\n const { crmRecordIndexes } = await import('./server/crm-record-indexes')\n return crmRecordIndexes()[kind].get(request)\n },\n }\n}\n\n/** The companies a person works for, as another plugin reads them (AGL-3080). */\nexport const crmCompanyRecordIndex = lazyCrmRecordIndex('company')\n\n/** The email templates and snippets reps write, as another plugin reads them (AGL-3080). */\nexport const crmMessageTemplateRecordIndex = lazyCrmRecordIndex('messageTemplate')\n\n/**\n * The CRM's writer on the record-timeline seam (AGL-2981), deferred: the\n * writer and the Admin SDK it brings load with the first entry filed.\n */\nexport const crmRecordTimelineWriter: PluginRecordTimelineWriter = {\n async logActivity(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).logActivity(request)\n },\n async createTask(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).createTask(request)\n },\n async recordEmailDelivery(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).recordEmailDelivery!(request)\n },\n // A delivery event for a message the CRM tagged (AGL-2615, AGL-3080).\n async recordTaggedDelivery(request) {\n const { createCrmRecordTimelineWriter, defaultCrmRecordTimelineDeps } =\n await import('./server/record-timeline')\n return createCrmRecordTimelineWriter(defaultCrmRecordTimelineDeps()).recordTaggedDelivery!(request)\n },\n // An automation's email to the person its event is about, filed on that\n // person's timeline with its delivery state (AGL-2615, AGL-3080).\n async prepareEmail(request) {\n const { prepareCrmRecordEmail } = await import('./server/automation-steps')\n return prepareCrmRecordEmail(request)\n },\n}\n\n/**\n * The plugin's SERVER declarations: what the server must know at boot, before\n * any door of the CRM has been called.\n *\n * ## Why a declaration and not a `tenantApi` registration\n *\n * The doors that meet a person mostly never load this plugin. A form\n * submission is a core route; an order and a booking are captured inside\n * STRIPE WEBHOOKS. None of them has a reason to load the CRM, and a registry\n * filled by a surface that did not load answers `null` — which\n * `capturePluginContact` defines as \"this workspace has no record system\", a\n * sentence that would be false and silent. Orders would stop creating\n * contacts with nothing red anywhere, which is the AGL-3025 shape.\n *\n * Running from both apps' generated server-declarations manifest at boot is\n * what makes the answer true in every process. It is also called from the\n * plugin's own API register functions, so a process whose boot did not run it\n * still registers the writer the first time a CRM door loads. Registering\n * twice replaces in place.\n *\n * ⚠️ The registry holds ONE writer: a workspace keeps one set of people. A\n * second plugin's is refused naming both, and the incumbent keeps serving.\n */\nexport function registerCrmServerDeclarations(): void {\n registerPluginContactCaptureWriter(crmContactCaptureWriter, {\n pluginId: BUNDLE_ID,\n })\n // The other half of keeping people (AGL-3080): another plugin that holds\n // an address, or a record the CRM handed it, asks here for the person —\n // a flow email's consent read, a refund, an automation filing somebody\n // under a campaign — and never opens the CRM's collections itself.\n registerPluginPersonRecords(crmPersonRecordsService, { pluginId: BUNDLE_ID })\n // The CRM's own share of a lead conversion (AGL-3254): the lead's\n // campaigns go onto the contact's facet. Through the seam every door\n // that converts a lead reaches, and deferred like the capture: the\n // module that writes is loaded when the first conversion arrives, and\n // it brings the Admin SDK with it — this file defers the plugin's OWN\n // module only. A library imported statically across the plugin cannot\n // also be lazy-loaded here: `enforce-module-boundaries` refuses every\n // static import of a library the project lazy-loads anywhere.\n registerPluginLeadConversionListener(\n async (request) => {\n const { carryLeadCampaignsOnConversion } =\n await import('./server/lead-campaign-carry')\n return carryLeadCampaignsOnConversion(request)\n },\n { pluginId: BUNDLE_ID },\n )\n // \"Did this workspace already know this person?\" (AGL-3289), asked by the\n // staff console's acquisition card. Deferred like the rest: the reads load\n // when a staff member first opens a card, not when the process starts.\n registerPluginPersonMatcher(\n async (request) => {\n const { matchCrmPeople } = await import('./server/person-matches')\n return matchCrmPeople(request)\n },\n { pluginId: BUNDLE_ID },\n )\n // The CRM's share of a consent group change (AGL-3320): a contact's\n // per-site refusal carried across a separation, and the records keyed by a\n // group re-homed after the flip. Registered whether or not a workspace has\n // the CRM switched on, because the contacts — and the refusals on them —\n // outlive the switch. Deferred like the rest; the summary is plain words\n // and answers synchronously.\n // The record email-state writer (AGL-3245), here as well as in the console\n // API so the tenant's doors reach it too: a form submission's capture is\n // checked for deliverability after its response (AGL-3328), and the \"Would\n // bounce\" that check finds is written through this seam. Deferred like the\n // rest; the console's own registration replaces this one with the same\n // writer, loaded eagerly.\n registerPluginRecordEmailStateWriter(\n {\n async stamp(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).stamp(request)\n },\n // A send's first opens and clicks, stamped on the contacts they name\n // (AGL-2616) — the campaign webhook's courtesy, deferred like the rest.\n async engaged(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).engaged!(request)\n },\n // A delivered campaign's leads, moved to Nurturing (AGL-3446).\n async reached(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).reached!(request)\n },\n // A lead that wrote back, moved to Working (AGL-3080).\n async replied(request) {\n const { createCrmRecordEmailStateWriter, defaultCrmRecordEmailStateDeps } =\n await import('./server/record-email-state')\n return createCrmRecordEmailStateWriter(defaultCrmRecordEmailStateDeps()).replied!(request)\n },\n },\n { pluginId: BUNDLE_ID },\n )\n // The record timeline writer (AGL-2981), here as well as in the API\n // surfaces so a door in either app reaches it: a booking confirmed on the\n // tenant, or by the payment webhook, files its meeting and follow-up\n // through it (AGL-2660). Deferred like the rest; an API registration\n // replaces this one with the same writer, loaded eagerly.\n registerPluginRecordTimelineWriter(crmRecordTimelineWriter, { pluginId: BUNDLE_ID })\n // Where a person came from (AGL-3519): a door the CRM does not run — the\n // platform's account sign-up, a sequence enrolling someone — names its\n // origin, and the CRM stamps the built-in Lead source on a record holding\n // none. Deferred like the rest.\n registerPluginRecordOriginWriter(\n {\n async stamp(request) {\n const { crmRecordOriginWriter } = await import('./server/record-origin')\n return crmRecordOriginWriter.stamp(request)\n },\n },\n { pluginId: BUNDLE_ID },\n )\n // Sharing rules (AGL-3336), re-evaluated after every server write of a\n // lead, a contact, a company or a deal: the core tells its record-written\n // listeners from the list-field restamp every such writer ends with, in\n // both apps — a form's capture on the tenant, an edit in the console.\n // Deferred like the rest: an org with no rules pays one cached read.\n registerPluginRecordWrittenListener(\n async (event) => {\n const { crmSharingRecordWritten } = await import('./server/crm-sharing')\n await crmSharingRecordWritten(event)\n },\n { pluginId: BUNDLE_ID, key: 'sharing' },\n )\n // The organization's pipelines and their stages, as another plugin reads\n // them (AGL-3080): an automation the AI drafts names the stage a deal\n // moves to. Deferred like the rest: the reads load with the first one.\n registerPluginRecordIndex('pipeline', crmPipelineRecordIndex, { pluginId: BUNDLE_ID })\n // The company a person works for, and the templates reps write, as a sales\n // sequence reads them (AGL-3080). Deferred like the rest.\n registerPluginRecordIndex('company', crmCompanyRecordIndex, { pluginId: BUNDLE_ID })\n registerPluginRecordIndex('messageTemplate', crmMessageTemplateRecordIndex, { pluginId: BUNDLE_ID })\n // The automation steps that write the CRM (AGL-2605, AGL-3080), run for the\n // workflows engine through the server-step seam and declared under\n // `serverSteps`. Deferred like the rest: the writes load with the first step.\n registerServerStepExecutor(\n CRM_STEP_TYPES,\n async (request) => (await import('./server/automation-steps')).runCrmAutomationStep(request),\n { pluginId: BUNDLE_ID },\n )\n registerPluginConsentGroupParticipant(\n {\n async preview(request) {\n const { consentGroupParticipant } = await import('./server/consent-group-participant')\n return consentGroupParticipant.preview(request)\n },\n async run(request) {\n const { consentGroupParticipant } = await import('./server/consent-group-participant')\n return consentGroupParticipant.run(request)\n },\n summarize: summarizeConsentGroupChange,\n },\n { pluginId: BUNDLE_ID },\n )\n // The records band in the monthly usage sweep, a core cron that never\n // loads a CRM door; declared in `usageAxes` so the sweep refuses to bill a\n // month without it. Deferred like the rest: the counts load with the\n // first sweep.\n registerPluginUsageMeter({\n pluginId: BUNDLE_ID,\n id: CRM_RECORDS_METER_ID,\n measure: async (context) => {\n const { measureCrmRecords } = await import('./server/crm-records-meter')\n return measureCrmRecords(context)\n },\n })\n}\n\n"],"names":["registerPluginConsentGroupParticipant","registerPluginContactCaptureWriter","registerPluginLeadConversionListener","registerPluginPersonMatcher","registerPluginPersonRecords","registerPluginRecordIndex","registerPluginRecordEmailStateWriter","registerPluginRecordOriginWriter","registerPluginRecordTimelineWriter","registerPluginRecordWrittenListener","registerServerStepExecutor","registerPluginUsageMeter","BUNDLE_ID","CRM_RECORDS_METER_ID","CRM_STEP_TYPES","summarizeConsentGroupChange","crmContactCaptureWriter","capture","request","captureContactForCrm","crmPersonRecordsService","find","crmPersonRecords","read","fileUnder","recordRefund","peopleInView","wroteIn","crmPipelineRecordIndex","list","pipelineRecordIndex","get","lazyCrmRecordIndex","kind","crmRecordIndexes","crmCompanyRecordIndex","crmMessageTemplateRecordIndex","crmRecordTimelineWriter","logActivity","createCrmRecordTimelineWriter","defaultCrmRecordTimelineDeps","createTask","recordEmailDelivery","recordTaggedDelivery","prepareEmail","prepareCrmRecordEmail","registerCrmServerDeclarations","pluginId","carryLeadCampaignsOnConversion","matchCrmPeople","stamp","createCrmRecordEmailStateWriter","defaultCrmRecordEmailStateDeps","engaged","reached","replied","crmRecordOriginWriter","event","crmSharingRecordWritten","key","runCrmAutomationStep","preview","consentGroupParticipant","run","summarize","id","measure","context","measureCrmRecords"],"mappings":"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"}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import * as Aglyn from "@aglyn/aglyn";
|
|
17
|
+
import { readNextTaskAtMs } from "./crm-next-activity.js";
|
|
17
18
|
import { crmShareChipFor } from "./crm-sharing.js";
|
|
18
19
|
/*
|
|
19
20
|
* Which holder a cross-site reader sees one contact through (AGL-2630) —
|
|
@@ -66,7 +67,7 @@ import { crmShareChipFor } from "./crm-sharing.js";
|
|
|
66
67
|
ownerUid: (_facet_ownerUid = facet.ownerUid) != null ? _facet_ownerUid : '',
|
|
67
68
|
lifecycleStage: Aglyn.isContactLifecycleStage(facet.lifecycleStage) ? facet.lifecycleStage : '',
|
|
68
69
|
lastEmailEngagementAtMs: typeof facet.lastEmailEngagementAtMs === 'number' && Number.isFinite(facet.lastEmailEngagementAtMs) && facet.lastEmailEngagementAtMs > 0 ? facet.lastEmailEngagementAtMs : null,
|
|
69
|
-
nextTaskAtMs:
|
|
70
|
+
nextTaskAtMs: readNextTaskAtMs(row),
|
|
70
71
|
emailState: Aglyn.readEmailState(row),
|
|
71
72
|
shareChip: crmShareChipFor(row, group.hostIds),
|
|
72
73
|
createdAt: row['createdAt'],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-record.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 { type CrmShareChip, crmShareChipFor } from './crm-sharing'\nimport type {\n AglynPostalAddress,\n ConsentGroup,\n ContactInteraction,\n ContactLifecycleStage,\n ContactSource,\n} from '@aglyn/aglyn'\n\n/**\n * One contact, as the VIEWING GROUP may see it (AGL-2596).\n *\n * A contact document is shared by every site in the org, and almost nothing\n * on it is legitimately shared: the notes, tags, timeline, profile and\n * commercial figures are the holder's own records and live in that holder's\n * facet. This is the row after the facet read — flat, so the list, the\n * record page and the export can render fields by name without any of them\n * reaching into `facets.{groupId}` for itself. One projection rather than a\n * facet read at every field is what keeps a surface from ever showing\n * another holder's records: there is no field on this shape that came from\n * the top of the document except the two that ARE shared, the address and\n * the canonical name.\n */\nexport interface ContactRecord {\n $id: string\n /**\n * The facet this row was flattened through — the viewing group under a\n * site, the person's primary holder at the org level (AGL-2630). A write\n * that edits the row goes back to the same facet, so a surface that has\n * only the flat record can still address the holder's own fields.\n */\n groupId: string\n /**\n * A site of that holder's group — the viewing site under a site, and at\n * the organization level the site the primary holder was resolved from —\n * which is where a stage move for this row is made and announced. `''` for\n * a row no site holds.\n */\n holderHostId: string\n /** Every site that has captured this person, sorted — the org-level \"Known by\". */\n capturedByHostIds: string[]\n /** The shared identity and the dedupe key. */\n email: string\n /** The other addresses a merge folded into this record (AGL-2625). */\n alternateEmails: string[]\n /** What THIS holder sees — their own override, or the canonical name. */\n name: string\n /** The canonical name: the identity of last resort, shared by every holder. */\n canonicalName: string\n /** This holder's own name for the person, when they have set one. */\n nameOverride: string\n sources: Partial<Record<ContactSource, true>>\n /** This holder's timeline, narrowed to the sites the group covers. */\n interactions: ContactInteraction[]\n tags: string[]\n notes: string\n campaignIds: string[]\n ltvCents: number\n ordersCount: number\n phone: string\n jobTitle: string\n /** This holder's lead source label (AGL-3298), `''` for none. */\n leadSource: string\n /*\n * Salesforce's standard contact fields (AGL-3515), this holder's, each\n * `''` (or `null`, `false`) for none — see `ContactFacet`.\n */\n salutation: string\n firstName: string\n lastName: string\n department: string\n mobilePhone: string\n homePhone: string\n otherPhone: string\n fax: string\n /** `YYYY-MM-DD`. */\n birthdate: string\n assistantName: string\n assistantPhone: string\n /** The contact this person reports to, by id. */\n reportsToContactId: string\n otherAddress: AglynPostalAddress | null\n doNotCall: boolean\n companyName: string\n companyId: string\n /**\n * What linking this person to a company has to know — this holder's link,\n * the shared mirror and the other holders' ids — so the properties card\n * and the bulk bar can plan a link from the row without the document. Not\n * a display field: the mirror is an index every reader of the document\n * already holds, and nothing renders it.\n */\n companyLink: Aglyn.ContactCompanyLinkState\n /** The mailing address. */\n address: AglynPostalAddress | null\n /** This holder's custom field values, keyed by definition key (AGL-2601). */\n custom?: Record<string, Aglyn.ContactCustomValue>\n /** This holder's attached org-library files, by media id (AGL-2662). */\n mediaIds?: string[]\n ownerUid: string\n /** Empty when the holder has not placed the person in the funnel. */\n lifecycleStage: ContactLifecycleStage | ''\n /**\n * When the person last opened or clicked one of this holder's campaigns\n * (AGL-2616), or `null` when they never have since the stamp shipped.\n */\n lastEmailEngagementAtMs: number | null\n /**\n * When the earliest open task against this person is due (AGL-2661), or\n * `null` when nothing is scheduled. SHARED, not the holder's: a task is\n * filed against the contact document, and every holder reading the row\n * sees the same next step.\n */\n nextTaskAtMs: number | null\n /**\n * The last verdict on the address (AGL-3245) — bounced, blocked,\n * unsubscribed, do-not-contact — or `null` when nothing is known. SHARED\n * like the address it is about.\n */\n emailState: Aglyn.EmailState | null\n /**\n * Why the viewing group sees this person when the answer is a share\n * (AGL-3336) — `null` when it holds them, and at the organization level.\n */\n shareChip?: CrmShareChip | null\n createdAt?: unknown\n updatedAt?: unknown\n}\n\n/*\n * Which holder a cross-site reader sees one contact through (AGL-2630) —\n * `contact-holder.ts`, shared with the server's whole-collection export so\n * the file and the organization-level list flatten a contact one way.\n */\nexport { contactPrimaryGroup, NO_HOLDER_GROUP } from './contact-holder'\n\n/** A document off the wire, flattened through one group's facet. */\nexport function contactRecordFromDoc(\n row: Record<string, any>,\n group: ConsentGroup,\n): ContactRecord {\n const facet = Aglyn.readContactFacet(row, group.groupId)\n return {\n $id: String(row['$id'] ?? ''),\n groupId: group.groupId,\n holderHostId: group.hostId,\n capturedByHostIds: Aglyn.contactCaptureHostIds(row),\n email: typeof row['email'] === 'string' ? row['email'] : '',\n alternateEmails: Array.isArray(row[Aglyn.CONTACT_ALTERNATE_EMAILS_FIELD])\n ? (row[Aglyn.CONTACT_ALTERNATE_EMAILS_FIELD] as unknown[]).filter(\n (email): email is string => typeof email === 'string',\n )\n : [],\n name: Aglyn.contactDisplayName(row, group.groupId),\n canonicalName: typeof row['name'] === 'string' ? row['name'] : '',\n nameOverride: facet.name ?? '',\n sources: facet.sources,\n interactions: Aglyn.interactionsForGroup(facet.interactions, group.hostIds),\n tags: facet.tags ?? [],\n notes: facet.notes ?? '',\n campaignIds: Aglyn.readContactContainerIds(row, group.groupId, 'campaign'),\n ltvCents: facet.ltvCents ?? 0,\n ordersCount: facet.ordersCount ?? 0,\n phone: facet.phone ?? '',\n jobTitle: facet.jobTitle ?? '',\n leadSource: facet.leadSource ?? '',\n salutation: facet.salutation ?? '',\n firstName: facet.firstName ?? '',\n lastName: facet.lastName ?? '',\n department: facet.department ?? '',\n mobilePhone: facet.mobilePhone ?? '',\n homePhone: facet.homePhone ?? '',\n otherPhone: facet.otherPhone ?? '',\n fax: facet.fax ?? '',\n birthdate: facet.birthdate ?? '',\n assistantName: facet.assistantName ?? '',\n assistantPhone: facet.assistantPhone ?? '',\n reportsToContactId: facet.reportsToContactId ?? '',\n otherAddress: facet.otherAddress ?? null,\n doNotCall: facet.doNotCall === true,\n companyName: facet.companyName ?? '',\n companyId: facet.companyId ?? '',\n companyLink: Aglyn.readContactCompanyLink(row, group.groupId),\n address: facet.address ?? null,\n custom: facet.custom ?? {},\n mediaIds: facet.mediaIds ?? [],\n ownerUid: facet.ownerUid ?? '',\n lifecycleStage: Aglyn.isContactLifecycleStage(facet.lifecycleStage)\n ? facet.lifecycleStage\n : '',\n lastEmailEngagementAtMs:\n typeof facet.lastEmailEngagementAtMs === 'number' &&\n Number.isFinite(facet.lastEmailEngagementAtMs) &&\n facet.lastEmailEngagementAtMs > 0\n ? facet.lastEmailEngagementAtMs\n : null,\n nextTaskAtMs: Aglyn.readNextTaskAtMs(row as { nextTaskAtMs?: unknown }),\n emailState: Aglyn.readEmailState(row),\n shareChip: crmShareChipFor(row, group.hostIds),\n createdAt: row['createdAt'],\n updatedAt: row['updatedAt'],\n }\n}\n\n/**\n * Tags as typed — comma-separated — into the shape the facet stores:\n * lower-cased, trimmed, deduplicated and capped, so a tag typed on the\n * record page and one typed in the create drawer are the same tag to the\n * segment filter that matches on them.\n */\nexport function parseContactTags(input: string): string[] {\n return [\n ...new Set(\n input\n .split(',')\n .map((tag) => tag.trim().toLowerCase().slice(0, 40))\n .filter(Boolean)\n .slice(0, 20),\n ),\n ]\n}\n"],"names":["Aglyn","crmShareChipFor","contactPrimaryGroup","NO_HOLDER_GROUP","contactRecordFromDoc","row","group","facet","readContactFacet","groupId","$id","String","holderHostId","hostId","capturedByHostIds","contactCaptureHostIds","email","alternateEmails","Array","isArray","CONTACT_ALTERNATE_EMAILS_FIELD","filter","name","contactDisplayName","canonicalName","nameOverride","sources","interactions","interactionsForGroup","hostIds","tags","notes","campaignIds","readContactContainerIds","ltvCents","ordersCount","phone","jobTitle","leadSource","salutation","firstName","lastName","department","mobilePhone","homePhone","otherPhone","fax","birthdate","assistantName","assistantPhone","reportsToContactId","otherAddress","doNotCall","companyName","companyId","companyLink","readContactCompanyLink","address","custom","mediaIds","ownerUid","lifecycleStage","isContactLifecycleStage","lastEmailEngagementAtMs","Number","isFinite","nextTaskAtMs","readNextTaskAtMs","emailState","readEmailState","shareChip","createdAt","updatedAt","parseContactTags","input","Set","split","map","tag","trim","toLowerCase","slice","Boolean"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-record.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 { readNextTaskAtMs } from './crm-next-activity'\nimport { type CrmShareChip, crmShareChipFor } from './crm-sharing'\nimport type {\n AglynPostalAddress,\n ConsentGroup,\n ContactInteraction,\n ContactLifecycleStage,\n ContactSource,\n} from '@aglyn/aglyn'\n\n/**\n * One contact, as the VIEWING GROUP may see it (AGL-2596).\n *\n * A contact document is shared by every site in the org, and almost nothing\n * on it is legitimately shared: the notes, tags, timeline, profile and\n * commercial figures are the holder's own records and live in that holder's\n * facet. This is the row after the facet read — flat, so the list, the\n * record page and the export can render fields by name without any of them\n * reaching into `facets.{groupId}` for itself. One projection rather than a\n * facet read at every field is what keeps a surface from ever showing\n * another holder's records: there is no field on this shape that came from\n * the top of the document except the two that ARE shared, the address and\n * the canonical name.\n */\nexport interface ContactRecord {\n $id: string\n /**\n * The facet this row was flattened through — the viewing group under a\n * site, the person's primary holder at the org level (AGL-2630). A write\n * that edits the row goes back to the same facet, so a surface that has\n * only the flat record can still address the holder's own fields.\n */\n groupId: string\n /**\n * A site of that holder's group — the viewing site under a site, and at\n * the organization level the site the primary holder was resolved from —\n * which is where a stage move for this row is made and announced. `''` for\n * a row no site holds.\n */\n holderHostId: string\n /** Every site that has captured this person, sorted — the org-level \"Known by\". */\n capturedByHostIds: string[]\n /** The shared identity and the dedupe key. */\n email: string\n /** The other addresses a merge folded into this record (AGL-2625). */\n alternateEmails: string[]\n /** What THIS holder sees — their own override, or the canonical name. */\n name: string\n /** The canonical name: the identity of last resort, shared by every holder. */\n canonicalName: string\n /** This holder's own name for the person, when they have set one. */\n nameOverride: string\n sources: Partial<Record<ContactSource, true>>\n /** This holder's timeline, narrowed to the sites the group covers. */\n interactions: ContactInteraction[]\n tags: string[]\n notes: string\n campaignIds: string[]\n ltvCents: number\n ordersCount: number\n phone: string\n jobTitle: string\n /** This holder's lead source label (AGL-3298), `''` for none. */\n leadSource: string\n /*\n * Salesforce's standard contact fields (AGL-3515), this holder's, each\n * `''` (or `null`, `false`) for none — see `ContactFacet`.\n */\n salutation: string\n firstName: string\n lastName: string\n department: string\n mobilePhone: string\n homePhone: string\n otherPhone: string\n fax: string\n /** `YYYY-MM-DD`. */\n birthdate: string\n assistantName: string\n assistantPhone: string\n /** The contact this person reports to, by id. */\n reportsToContactId: string\n otherAddress: AglynPostalAddress | null\n doNotCall: boolean\n companyName: string\n companyId: string\n /**\n * What linking this person to a company has to know — this holder's link,\n * the shared mirror and the other holders' ids — so the properties card\n * and the bulk bar can plan a link from the row without the document. Not\n * a display field: the mirror is an index every reader of the document\n * already holds, and nothing renders it.\n */\n companyLink: Aglyn.ContactCompanyLinkState\n /** The mailing address. */\n address: AglynPostalAddress | null\n /** This holder's custom field values, keyed by definition key (AGL-2601). */\n custom?: Record<string, Aglyn.ContactCustomValue>\n /** This holder's attached org-library files, by media id (AGL-2662). */\n mediaIds?: string[]\n ownerUid: string\n /** Empty when the holder has not placed the person in the funnel. */\n lifecycleStage: ContactLifecycleStage | ''\n /**\n * When the person last opened or clicked one of this holder's campaigns\n * (AGL-2616), or `null` when they never have since the stamp shipped.\n */\n lastEmailEngagementAtMs: number | null\n /**\n * When the earliest open task against this person is due (AGL-2661), or\n * `null` when nothing is scheduled. SHARED, not the holder's: a task is\n * filed against the contact document, and every holder reading the row\n * sees the same next step.\n */\n nextTaskAtMs: number | null\n /**\n * The last verdict on the address (AGL-3245) — bounced, blocked,\n * unsubscribed, do-not-contact — or `null` when nothing is known. SHARED\n * like the address it is about.\n */\n emailState: Aglyn.EmailState | null\n /**\n * Why the viewing group sees this person when the answer is a share\n * (AGL-3336) — `null` when it holds them, and at the organization level.\n */\n shareChip?: CrmShareChip | null\n createdAt?: unknown\n updatedAt?: unknown\n}\n\n/*\n * Which holder a cross-site reader sees one contact through (AGL-2630) —\n * `contact-holder.ts`, shared with the server's whole-collection export so\n * the file and the organization-level list flatten a contact one way.\n */\nexport { contactPrimaryGroup, NO_HOLDER_GROUP } from './contact-holder'\n\n/** A document off the wire, flattened through one group's facet. */\nexport function contactRecordFromDoc(\n row: Record<string, any>,\n group: ConsentGroup,\n): ContactRecord {\n const facet = Aglyn.readContactFacet(row, group.groupId)\n return {\n $id: String(row['$id'] ?? ''),\n groupId: group.groupId,\n holderHostId: group.hostId,\n capturedByHostIds: Aglyn.contactCaptureHostIds(row),\n email: typeof row['email'] === 'string' ? row['email'] : '',\n alternateEmails: Array.isArray(row[Aglyn.CONTACT_ALTERNATE_EMAILS_FIELD])\n ? (row[Aglyn.CONTACT_ALTERNATE_EMAILS_FIELD] as unknown[]).filter(\n (email): email is string => typeof email === 'string',\n )\n : [],\n name: Aglyn.contactDisplayName(row, group.groupId),\n canonicalName: typeof row['name'] === 'string' ? row['name'] : '',\n nameOverride: facet.name ?? '',\n sources: facet.sources,\n interactions: Aglyn.interactionsForGroup(facet.interactions, group.hostIds),\n tags: facet.tags ?? [],\n notes: facet.notes ?? '',\n campaignIds: Aglyn.readContactContainerIds(row, group.groupId, 'campaign'),\n ltvCents: facet.ltvCents ?? 0,\n ordersCount: facet.ordersCount ?? 0,\n phone: facet.phone ?? '',\n jobTitle: facet.jobTitle ?? '',\n leadSource: facet.leadSource ?? '',\n salutation: facet.salutation ?? '',\n firstName: facet.firstName ?? '',\n lastName: facet.lastName ?? '',\n department: facet.department ?? '',\n mobilePhone: facet.mobilePhone ?? '',\n homePhone: facet.homePhone ?? '',\n otherPhone: facet.otherPhone ?? '',\n fax: facet.fax ?? '',\n birthdate: facet.birthdate ?? '',\n assistantName: facet.assistantName ?? '',\n assistantPhone: facet.assistantPhone ?? '',\n reportsToContactId: facet.reportsToContactId ?? '',\n otherAddress: facet.otherAddress ?? null,\n doNotCall: facet.doNotCall === true,\n companyName: facet.companyName ?? '',\n companyId: facet.companyId ?? '',\n companyLink: Aglyn.readContactCompanyLink(row, group.groupId),\n address: facet.address ?? null,\n custom: facet.custom ?? {},\n mediaIds: facet.mediaIds ?? [],\n ownerUid: facet.ownerUid ?? '',\n lifecycleStage: Aglyn.isContactLifecycleStage(facet.lifecycleStage)\n ? facet.lifecycleStage\n : '',\n lastEmailEngagementAtMs:\n typeof facet.lastEmailEngagementAtMs === 'number' &&\n Number.isFinite(facet.lastEmailEngagementAtMs) &&\n facet.lastEmailEngagementAtMs > 0\n ? facet.lastEmailEngagementAtMs\n : null,\n nextTaskAtMs: readNextTaskAtMs(row as { nextTaskAtMs?: unknown }),\n emailState: Aglyn.readEmailState(row),\n shareChip: crmShareChipFor(row, group.hostIds),\n createdAt: row['createdAt'],\n updatedAt: row['updatedAt'],\n }\n}\n\n/**\n * Tags as typed — comma-separated — into the shape the facet stores:\n * lower-cased, trimmed, deduplicated and capped, so a tag typed on the\n * record page and one typed in the create drawer are the same tag to the\n * segment filter that matches on them.\n */\nexport function parseContactTags(input: string): string[] {\n return [\n ...new Set(\n input\n .split(',')\n .map((tag) => tag.trim().toLowerCase().slice(0, 40))\n .filter(Boolean)\n .slice(0, 20),\n ),\n ]\n}\n"],"names":["Aglyn","readNextTaskAtMs","crmShareChipFor","contactPrimaryGroup","NO_HOLDER_GROUP","contactRecordFromDoc","row","group","facet","readContactFacet","groupId","$id","String","holderHostId","hostId","capturedByHostIds","contactCaptureHostIds","email","alternateEmails","Array","isArray","CONTACT_ALTERNATE_EMAILS_FIELD","filter","name","contactDisplayName","canonicalName","nameOverride","sources","interactions","interactionsForGroup","hostIds","tags","notes","campaignIds","readContactContainerIds","ltvCents","ordersCount","phone","jobTitle","leadSource","salutation","firstName","lastName","department","mobilePhone","homePhone","otherPhone","fax","birthdate","assistantName","assistantPhone","reportsToContactId","otherAddress","doNotCall","companyName","companyId","companyLink","readContactCompanyLink","address","custom","mediaIds","ownerUid","lifecycleStage","isContactLifecycleStage","lastEmailEngagementAtMs","Number","isFinite","nextTaskAtMs","emailState","readEmailState","shareChip","createdAt","updatedAt","parseContactTags","input","Set","split","map","tag","trim","toLowerCase","slice","Boolean"],"mappings":"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"}
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* NEXT ACTIVITY (AGL-2661): when the earliest open task against a record
|
|
19
|
+
* is due, denormalized onto the record as `nextTaskAtMs`.
|
|
20
|
+
*
|
|
21
|
+
* The question a sales manager asks of a LIST — "which deals have nothing
|
|
22
|
+
* scheduled?" — cannot be answered by a task query per row, so every
|
|
23
|
+
* contact, company and deal carries the answer. This module is the pure
|
|
24
|
+
* half: what the value IS given a record's open tasks, how a stored one is
|
|
25
|
+
* read back, the view clause a list filters on, and the report figure. The
|
|
26
|
+
* admin library's `recomputeCrmNextTaskAt` is the writer; the console's
|
|
27
|
+
* route is its door for client-direct task writes.
|
|
28
|
+
*/
|
|
29
|
+
import type { CrmDeal, CrmTask, CrmViewFilterClause } from '@aglyn/aglyn/app-utils/crm';
|
|
30
|
+
/** The field on a contact, a company and a deal — the one name every list reads. */
|
|
31
|
+
export declare const CRM_NEXT_ACTIVITY_FIELD = "nextTaskAtMs";
|
|
32
|
+
/**
|
|
33
|
+
* The records one task can be filed against — the ids a recompute reads
|
|
34
|
+
* off a task, and the three collections it writes to.
|
|
35
|
+
*/
|
|
36
|
+
export interface CrmNextActivityLink {
|
|
37
|
+
contactId?: string;
|
|
38
|
+
companyId?: string;
|
|
39
|
+
dealId?: string;
|
|
40
|
+
}
|
|
41
|
+
/** How the collections are keyed on a task, in the order a recompute walks them. */
|
|
42
|
+
export declare const CRM_NEXT_ACTIVITY_LINK_FIELDS: readonly ["contactId", "companyId", "dealId"];
|
|
43
|
+
export type CrmNextActivityLinkField = (typeof CRM_NEXT_ACTIVITY_LINK_FIELDS)[number];
|
|
44
|
+
/**
|
|
45
|
+
* When the earliest OPEN task in the list is due, or `null`.
|
|
46
|
+
*
|
|
47
|
+
* Only an open task with a finite due time counts: a done task is history,
|
|
48
|
+
* and an open one with no date is a to-do with no "next" about it — the
|
|
49
|
+
* record's next activity is unknown rather than immediate, and `null`
|
|
50
|
+
* says so. A pre-change writer never produced a time outside the finite
|
|
51
|
+
* range, but a stored `dueAtMs` is trusted no further than that.
|
|
52
|
+
*/
|
|
53
|
+
export declare function nextTaskAtMsOf(tasks: readonly Pick<CrmTask, 'status' | 'dueAtMs'>[]): number | null;
|
|
54
|
+
/**
|
|
55
|
+
* A stored `nextTaskAtMs`, read back: a finite positive number, else
|
|
56
|
+
* `null`. Absent — a record written before the field existed — reads as
|
|
57
|
+
* `null` too, because to every reader "not yet computed" and "nothing
|
|
58
|
+
* scheduled" draw the same dash; the Fields section's recompute is what
|
|
59
|
+
* closes the gap for a filter that asks the store rather than the row.
|
|
60
|
+
*/
|
|
61
|
+
export declare function readNextTaskAtMs(record: {
|
|
62
|
+
nextTaskAtMs?: unknown;
|
|
63
|
+
} | null | undefined): number | null;
|
|
64
|
+
/** Whether nothing is scheduled against the record — the "stuck" test. */
|
|
65
|
+
export declare function hasNoNextActivity(record: {
|
|
66
|
+
nextTaskAtMs?: unknown;
|
|
67
|
+
} | null | undefined): boolean;
|
|
68
|
+
/** The links one task carries, with the blanks left out. */
|
|
69
|
+
export declare function nextActivityLinksOfTask(task: Partial<Pick<CrmTask, CrmNextActivityLinkField>> | null | undefined): CrmNextActivityLink;
|
|
70
|
+
/**
|
|
71
|
+
* Several links as one per record, so a bulk action over forty tasks on
|
|
72
|
+
* one deal recomputes that deal once. Order is first-seen.
|
|
73
|
+
*/
|
|
74
|
+
export declare function mergeNextActivityLinks(links: readonly (CrmNextActivityLink | null | undefined)[]): Array<{
|
|
75
|
+
field: CrmNextActivityLinkField;
|
|
76
|
+
id: string;
|
|
77
|
+
}>;
|
|
78
|
+
/**
|
|
79
|
+
* The saved-view clause "No next activity" is stored as — one clause,
|
|
80
|
+
* spelled once, so the toggle a list draws and the matcher it runs agree
|
|
81
|
+
* on the name. `isEmpty` is honest here: a recompute writes `null` for a
|
|
82
|
+
* record with nothing scheduled, and a record never written reads blank
|
|
83
|
+
* too (see `readNextTaskAtMs`).
|
|
84
|
+
*/
|
|
85
|
+
export declare const CRM_NO_NEXT_ACTIVITY_CLAUSE: Readonly<CrmViewFilterClause>;
|
|
86
|
+
export declare function isNoNextActivityClause(clause: Pick<CrmViewFilterClause, 'field' | 'op'>): boolean;
|
|
87
|
+
/** A view's filters with the clause switched on or off, the rest untouched. */
|
|
88
|
+
export declare function withNoNextActivity(filters: readonly CrmViewFilterClause[], on: boolean): CrmViewFilterClause[];
|
|
89
|
+
/**
|
|
90
|
+
* The rows a "No next activity" clause keeps: every one when the view does
|
|
91
|
+
* not carry it, else those with nothing scheduled. What a list that
|
|
92
|
+
* filters through a query — companies, deals — runs over the page it
|
|
93
|
+
* loaded, the way the contacts list runs its window-only clauses.
|
|
94
|
+
*/
|
|
95
|
+
export declare function filterByNextActivity<T extends {
|
|
96
|
+
nextTaskAtMs?: unknown;
|
|
97
|
+
}>(rows: readonly T[], filters: readonly CrmViewFilterClause[]): T[];
|
|
98
|
+
/**
|
|
99
|
+
* The report figure: how many OPEN deals have nothing scheduled, and which
|
|
100
|
+
* (AGL-2661). A won deal needs no next step and a lost one has none, so
|
|
101
|
+
* only open deals are stuck; the caller hands in the window it already
|
|
102
|
+
* reads for the pipeline card, so this costs no read of its own.
|
|
103
|
+
*/
|
|
104
|
+
export declare function stuckDeals<T extends Pick<CrmDeal, 'status' | 'nextTaskAtMs'>>(deals: readonly T[]): {
|
|
105
|
+
count: number;
|
|
106
|
+
deals: T[];
|
|
107
|
+
};
|