@aglyn/plugins-crm 1.0.0-beta.262 → 1.0.0-beta.263

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-crm",
3
- "version": "1.0.0-beta.262",
3
+ "version": "1.0.0-beta.263",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,19 +25,19 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.262",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.262",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.262",
31
- "@aglyn/shared-ui-jsx": "1.0.0-beta.262",
32
- "@aglyn/shared-ui-next": "1.0.0-beta.262",
33
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.262",
34
- "@aglyn/shared-util-email": "1.0.0-beta.262",
35
- "@aglyn/shared-util-http": "1.0.0-beta.262",
36
- "@aglyn/shared-util-timestamp": "1.0.0-beta.262",
37
- "@aglyn/shared-util-tools": "1.0.0-beta.262",
38
- "@aglyn/tenant-data-admin": "1.0.0-beta.262",
39
- "@aglyn/tenant-feature-instance": "1.0.0-beta.262",
40
- "@aglyn/tenant-runtime": "1.0.0-beta.262",
28
+ "@aglyn/aglyn": "1.0.0-beta.263",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.263",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.263",
31
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.263",
32
+ "@aglyn/shared-ui-next": "1.0.0-beta.263",
33
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.263",
34
+ "@aglyn/shared-util-email": "1.0.0-beta.263",
35
+ "@aglyn/shared-util-http": "1.0.0-beta.263",
36
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.263",
37
+ "@aglyn/shared-util-tools": "1.0.0-beta.263",
38
+ "@aglyn/tenant-data-admin": "1.0.0-beta.263",
39
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.263",
40
+ "@aglyn/tenant-runtime": "1.0.0-beta.263",
41
41
  "@dnd-kit/core": "^6.3.1",
42
42
  "@dnd-kit/utilities": "^3.2.2",
43
43
  "@swc/helpers": "0.5.23",
@@ -14,8 +14,8 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { isPipelineArchived } from "@aglyn/aglyn/app-utils/crm";
17
- import { PLUGIN_FIGURE_MAX_ROWS, pluginFigureCellText, pluginFigureChange, pluginFigureWindows, registerPluginFigureReader } from "@aglyn/aglyn/plugin-manager/plugin-figures";
18
- import { figureCount, figureHasColumn, figurePlural, figureRow } from "@aglyn/aglyn/app-utils/weekly-summary";
17
+ import { PLUGIN_FIGURE_MAX_ROWS, pluginFigureChange, pluginFigureWindows, registerPluginFigureReader } from "@aglyn/aglyn/plugin-manager/plugin-figures";
18
+ import { figureCount, figureHasColumn, figureRow, figureStat } from "@aglyn/aglyn/app-utils/weekly-summary";
19
19
  import { scopedToHost } from "@aglyn/tenant-data-admin/server/organizations";
20
20
  import { BUNDLE_ID } from "../constants/bundle-common.js";
21
21
  import { currencyOfDeals, pipelineTotals } from "../model/crm-reports.js";
@@ -64,14 +64,17 @@ export function crmPipelineFigureReader(firestore) {
64
64
  feature: 'crm',
65
65
  weeklySummary: {
66
66
  order: 20,
67
- lines: (table)=>{
68
- var _all_value;
67
+ summarize: (table)=>{
69
68
  const all = figureRow(table, 'pipeline', 'All pipelines');
70
- const deals = figureCount(all == null ? void 0 : all['deals']);
71
- if (!all || !deals) return [];
72
- return [
73
- `Open pipeline: ${figurePlural(deals, 'deal', 'deals')} worth ${pluginFigureCellText(table, 'value', (_all_value = all['value']) != null ? _all_value : 0)}`
74
- ];
69
+ if (!all || !figureCount(all['deals'])) return null;
70
+ return {
71
+ title: 'Open pipeline',
72
+ stats: [
73
+ figureStat(table, all, 'deals', 'Open deals'),
74
+ figureStat(table, all, 'value', 'Value'),
75
+ figureStat(table, all, 'weighted', 'Weighted value')
76
+ ]
77
+ };
75
78
  }
76
79
  },
77
80
  read: async (request)=>{
@@ -193,16 +196,26 @@ export function crmClosedDealsFigureReader(firestore) {
193
196
  feature: 'crm',
194
197
  weeklySummary: {
195
198
  order: 10,
196
- lines: (table)=>{
197
- var _row_wonValue;
199
+ summarize: (table)=>{
198
200
  const row = table.rows[0];
199
- if (!row || !figureHasColumn(table, 'won')) return [];
200
- const won = figureCount(row['won']);
201
- const lost = figureCount(row['lost']);
202
- if (!won && !lost) return [];
203
- return [
204
- `Deals: ${won.toLocaleString('en-US')} won` + (won ? ` (${pluginFigureCellText(table, 'wonValue', (_row_wonValue = row['wonValue']) != null ? _row_wonValue : 0)})` : '') + `, ${lost.toLocaleString('en-US')} lost`
205
- ];
201
+ if (!row || !figureHasColumn(table, 'won')) return null;
202
+ if (!figureCount(row['won']) && !figureCount(row['lost'])) return null;
203
+ return {
204
+ title: 'Deals closed',
205
+ stats: [
206
+ figureStat(table, row, 'won', 'Won', {
207
+ change: 'change',
208
+ total: {
209
+ one: 'deal won',
210
+ many: 'deals won'
211
+ }
212
+ }),
213
+ figureStat(table, row, 'wonValue', 'Value won'),
214
+ ...figureCount(row['lost']) ? [
215
+ figureStat(table, row, 'lost', 'Lost')
216
+ ] : []
217
+ ]
218
+ };
206
219
  }
207
220
  },
208
221
  read: async (request)=>{
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/deal-figures.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 { isPipelineArchived, type CrmDeal, type CrmPipeline } from '@aglyn/aglyn/app-utils/crm'\nimport {\n PLUGIN_FIGURE_MAX_ROWS,\n pluginFigureCellText,\n pluginFigureChange,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n type PluginFigureRow,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { figureCount, figureHasColumn, figurePlural, figureRow } from '@aglyn/aglyn/app-utils/weekly-summary'\nimport { scopedToHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { currencyOfDeals, pipelineTotals } from '../model/crm-reports'\n\n/**\n * The organization's deals as figure tables another plugin reads by id\n * (AGL-3603) — the AI plugin's insights answer \"how much is in the pipeline?\"\n * and \"what did we win this month?\" from them.\n *\n * - `crm.pipeline`: the open pipeline as it stands, by pipeline and stage —\n * how many deals, what they are worth, and what they are worth at their\n * stage's odds — with `pipelineTotals`, the Reports page's own arithmetic.\n * - `crm.closed`: deals won and lost over the window, what was won, the win\n * rate, and the change in won deals from the window before.\n *\n * Organization readers: with a site, only the deals shared with that site —\n * `scopedToHost`, the predicate the rules evaluate — and with none, the\n * workspace's. Each read is a Firestore query on `status` (and `closedAtMs`\n * for the window), served by the `deals` composites the index file already\n * carries, projected to the fields counted and capped; a deal's title, its\n * contact and its company never reach a table.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** Open deals the pipeline reads; a larger pipeline is told its figures read low. */\nexport const DEAL_FIGURES_OPEN_CEILING = 1_000\n\n/** Deals one window reads of each outcome. */\nexport const DEAL_FIGURES_CLOSED_CEILING = 1_000\n\n/** Pipelines read. */\nexport const DEAL_FIGURES_PIPELINE_CEILING = 20\n\nconst WINDOWS: readonly number[] = [7, 14, 30, 90]\n\ntype DealRow = Pick<CrmDeal, 'status' | 'stageId' | 'pipelineId' | 'amountCents' | 'currency' | 'probability'>\n\nconst major = (cents: number): number => Math.round(cents) / 100\n\nfunction deals(firestore: Firestore, orgId: string, hostId: string | null): FirebaseFirestore.Query {\n const collection = firestore.collection('orgs').doc(orgId).collection('deals')\n return hostId ? scopedToHost(collection, hostId) : collection\n}\n\nasync function readOpenDeals(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n): Promise<{ rows: DealRow[]; truncated: boolean }> {\n const snapshot = await deals(firestore, orgId, hostId)\n .where('status', '==', 'open')\n .orderBy('updatedAt', 'desc')\n .select('status', 'stageId', 'pipelineId', 'amountCents', 'currency', 'probability')\n .limit(DEAL_FIGURES_OPEN_CEILING + 1)\n .get()\n return {\n rows: snapshot.docs.slice(0, DEAL_FIGURES_OPEN_CEILING).map((doc) => (doc.data() ?? {}) as DealRow),\n truncated: snapshot.docs.length > DEAL_FIGURES_OPEN_CEILING,\n }\n}\n\nasync function readPipelines(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n): Promise<Array<{ id: string; pipeline: CrmPipeline }>> {\n const collection = firestore.collection('orgs').doc(orgId).collection('pipelines')\n const snapshot = await (hostId ? scopedToHost(collection, hostId) : collection)\n .limit(DEAL_FIGURES_PIPELINE_CEILING)\n .get()\n return snapshot.docs\n .map((doc) => ({ id: doc.id, pipeline: (doc.data() ?? {}) as CrmPipeline }))\n .filter(({ pipeline }) => !isPipelineArchived(pipeline))\n}\n\nexport function crmPipelineFigureReader(firestore: () => Firestore): PluginFigureReader {\n return {\n id: 'crm.pipeline',\n label: 'Deal pipeline',\n description:\n 'The open deals as they stand, by pipeline and stage: how many, what they are worth, and what they are worth at each stage’s odds. Current totals, with no window.',\n scope: 'org',\n windows: [],\n feature: 'crm',\n weeklySummary: {\n order: 20,\n lines: (table) => {\n const all = figureRow(table, 'pipeline', 'All pipelines')\n const deals = figureCount(all?.['deals'])\n if (!all || !deals) return []\n return [`Open pipeline: ${figurePlural(deals, 'deal', 'deals')} worth ${pluginFigureCellText(table, 'value', all['value'] ?? 0)}`]\n },\n },\n read: async (request) => {\n const db = firestore()\n const [open, pipelines] = await Promise.all([\n readOpenDeals(db, request.orgId, request.hostId),\n readPipelines(db, request.orgId, request.hostId),\n ])\n const { currency, mixed } = currencyOfDeals(open.rows)\n const code = currency.toUpperCase()\n const rows: PluginFigureRow[] = []\n let count = 0\n let amount = 0\n let weighted = 0\n let placed = 0\n for (const { id, pipeline } of pipelines) {\n const totals = pipelineTotals(\n open.rows.filter((deal) => deal.pipelineId === id),\n pipeline,\n )\n count += totals.count\n amount += totals.amountCents\n weighted += totals.weightedCents\n for (const stage of totals.stages) {\n placed += 1\n rows.push({\n pipeline: pipeline.name || 'Untitled pipeline',\n stage: stage.stage.name || 'Untitled stage',\n deals: stage.count,\n value: major(stage.amountCents),\n weighted: major(stage.weightedCents),\n })\n }\n }\n const kept = rows.slice(0, PLUGIN_FIGURE_MAX_ROWS - 1)\n return {\n ok: true,\n table: {\n title: 'Deal pipeline',\n source: { label: 'CRM reports', path: 'crm/reports' },\n period: null,\n columns: [\n { key: 'pipeline', label: 'Pipeline', kind: 'text' },\n { key: 'stage', label: 'Stage', kind: 'text' },\n { key: 'deals', label: 'Open deals', kind: 'count' },\n { key: 'value', label: 'Value', kind: 'money', currency: code },\n { key: 'weighted', label: 'Weighted value', kind: 'money', currency: code },\n ],\n rows: [\n { pipeline: 'All pipelines', stage: 'All open stages', deals: count, value: major(amount), weighted: major(weighted) },\n ...kept,\n ],\n omitted: Math.max(0, placed - kept.length),\n notes: [\n 'Only open deals: a won deal is revenue and a lost one is history.',\n ...(mixed ? [`Deals are in more than one currency; every amount is added as ${code}.`] : []),\n ...(open.truncated\n ? [`More than ${DEAL_FIGURES_OPEN_CEILING.toLocaleString('en-US')} deals are open, so the figures count the most recently updated ${DEAL_FIGURES_OPEN_CEILING.toLocaleString('en-US')} and read low.`]\n : []),\n ].slice(0, 3),\n },\n }\n },\n }\n}\n\ninterface ClosedRead {\n count: number\n amountCents: number\n currencies: Array<Pick<CrmDeal, 'currency'>>\n truncated: boolean\n}\n\nasync function readClosed(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n status: 'won' | 'lost',\n startMs: number,\n endMs: number,\n): Promise<ClosedRead> {\n const snapshot = await deals(firestore, orgId, hostId)\n .where('status', '==', status)\n .where('closedAtMs', '>=', startMs)\n .where('closedAtMs', '<', endMs)\n .orderBy('closedAtMs', 'asc')\n .select('amountCents', 'currency')\n .limit(DEAL_FIGURES_CLOSED_CEILING + 1)\n .get()\n const docs = snapshot.docs.slice(0, DEAL_FIGURES_CLOSED_CEILING).map((doc) => (doc.data() ?? {}) as DealRow)\n return {\n count: docs.length,\n amountCents: docs.reduce((sum, deal) => sum + Math.max(0, Math.round(Number(deal.amountCents ?? 0) || 0)), 0),\n currencies: docs,\n truncated: snapshot.docs.length > DEAL_FIGURES_CLOSED_CEILING,\n }\n}\n\nexport function crmClosedDealsFigureReader(firestore: () => Firestore): PluginFigureReader {\n return {\n id: 'crm.closed',\n label: 'Deals won and lost',\n description:\n 'Deals closed over the window: how many were won and lost, what was won, the win rate, and the change in deals won from the window before.',\n scope: 'org',\n windows: WINDOWS,\n feature: 'crm',\n weeklySummary: {\n order: 10,\n lines: (table) => {\n const row = table.rows[0]\n if (!row || !figureHasColumn(table, 'won')) return []\n const won = figureCount(row['won'])\n const lost = figureCount(row['lost'])\n if (!won && !lost) return []\n return [\n `Deals: ${won.toLocaleString('en-US')} won` +\n (won ? ` (${pluginFigureCellText(table, 'wonValue', row['wonValue'] ?? 0)})` : '') +\n `, ${lost.toLocaleString('en-US')} lost`,\n ]\n },\n },\n read: async (request) => {\n if (!WINDOWS.includes(request.days)) {\n return { ok: false, status: 400, error: 'That window is not one closed deals are read over' }\n }\n const db = firestore()\n const { current, previous } = pluginFigureWindows(request.now, request.days)\n const [won, lost, wonBefore] = await Promise.all([\n readClosed(db, request.orgId, request.hostId, 'won', current.startMs, current.endMs),\n readClosed(db, request.orgId, request.hostId, 'lost', current.startMs, current.endMs),\n readClosed(db, request.orgId, request.hostId, 'won', previous.startMs, previous.endMs),\n ])\n const { currency, mixed } = currencyOfDeals(won.currencies)\n const code = currency.toUpperCase()\n const closed = won.count + lost.count\n return {\n ok: true,\n table: {\n title: 'Deals won and lost',\n source: { label: 'CRM reports', path: 'crm/reports' },\n period: { from: current.from, to: current.to, days: request.days },\n columns: [\n { key: 'won', label: 'Won', kind: 'count' },\n { key: 'lost', label: 'Lost', kind: 'count' },\n { key: 'wonValue', label: 'Value won', kind: 'money', currency: code },\n { key: 'winRate', label: 'Win rate', kind: 'percent' },\n { key: 'change', label: 'Change in deals won', kind: 'change' },\n ],\n rows: [\n {\n won: won.count,\n lost: lost.count,\n wonValue: major(won.amountCents),\n winRate: closed > 0 ? Math.round((won.count / closed) * 1_000) / 10 : null,\n change: pluginFigureChange(won.count, wonBefore.count),\n },\n ],\n omitted: 0,\n notes: [\n 'A deal is counted on the day it closed.',\n ...(mixed ? [`Deals won are in more than one currency; every amount is added as ${code}.`] : []),\n ...(won.truncated || lost.truncated || wonBefore.truncated\n ? [`More than ${DEAL_FIGURES_CLOSED_CEILING.toLocaleString('en-US')} deals closed one way in a window, so the figures count ${DEAL_FIGURES_CLOSED_CEILING.toLocaleString('en-US')} and read low.`]\n : []),\n ].slice(0, 3),\n },\n }\n },\n }\n}\n\n/** Registers the deal readers from the console surface, where insight jobs run. */\nexport function registerDealFigureReaders(firestore: () => Firestore): void {\n for (const reader of [crmPipelineFigureReader(firestore), crmClosedDealsFigureReader(firestore)]) {\n registerPluginFigureReader(reader, { pluginId: BUNDLE_ID })\n }\n}\n"],"names":["isPipelineArchived","PLUGIN_FIGURE_MAX_ROWS","pluginFigureCellText","pluginFigureChange","pluginFigureWindows","registerPluginFigureReader","figureCount","figureHasColumn","figurePlural","figureRow","scopedToHost","BUNDLE_ID","currencyOfDeals","pipelineTotals","DEAL_FIGURES_OPEN_CEILING","DEAL_FIGURES_CLOSED_CEILING","DEAL_FIGURES_PIPELINE_CEILING","WINDOWS","major","cents","Math","round","deals","firestore","orgId","hostId","collection","doc","readOpenDeals","snapshot","where","orderBy","select","limit","get","rows","docs","slice","map","data","truncated","length","readPipelines","id","pipeline","filter","crmPipelineFigureReader","label","description","scope","windows","feature","weeklySummary","order","lines","table","all","read","request","db","open","pipelines","Promise","currency","mixed","code","toUpperCase","count","amount","weighted","placed","totals","deal","pipelineId","amountCents","weightedCents","stage","stages","push","name","value","kept","ok","title","source","path","period","columns","key","kind","omitted","max","notes","toLocaleString","readClosed","status","startMs","endMs","reduce","sum","Number","currencies","crmClosedDealsFigureReader","row","won","lost","includes","days","error","current","previous","now","wonBefore","closed","from","to","wonValue","winRate","change","registerDealFigureReaders","reader","pluginId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/deal-figures.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 { isPipelineArchived, type CrmDeal, type CrmPipeline } from '@aglyn/aglyn/app-utils/crm'\nimport {\n PLUGIN_FIGURE_MAX_ROWS,\n pluginFigureChange,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n type PluginFigureRow,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { figureCount, figureHasColumn, figureRow, figureStat } from '@aglyn/aglyn/app-utils/weekly-summary'\nimport { scopedToHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { currencyOfDeals, pipelineTotals } from '../model/crm-reports'\n\n/**\n * The organization's deals as figure tables another plugin reads by id\n * (AGL-3603) — the AI plugin's insights answer \"how much is in the pipeline?\"\n * and \"what did we win this month?\" from them.\n *\n * - `crm.pipeline`: the open pipeline as it stands, by pipeline and stage —\n * how many deals, what they are worth, and what they are worth at their\n * stage's odds — with `pipelineTotals`, the Reports page's own arithmetic.\n * - `crm.closed`: deals won and lost over the window, what was won, the win\n * rate, and the change in won deals from the window before.\n *\n * Organization readers: with a site, only the deals shared with that site —\n * `scopedToHost`, the predicate the rules evaluate — and with none, the\n * workspace's. Each read is a Firestore query on `status` (and `closedAtMs`\n * for the window), served by the `deals` composites the index file already\n * carries, projected to the fields counted and capped; a deal's title, its\n * contact and its company never reach a table.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** Open deals the pipeline reads; a larger pipeline is told its figures read low. */\nexport const DEAL_FIGURES_OPEN_CEILING = 1_000\n\n/** Deals one window reads of each outcome. */\nexport const DEAL_FIGURES_CLOSED_CEILING = 1_000\n\n/** Pipelines read. */\nexport const DEAL_FIGURES_PIPELINE_CEILING = 20\n\nconst WINDOWS: readonly number[] = [7, 14, 30, 90]\n\ntype DealRow = Pick<CrmDeal, 'status' | 'stageId' | 'pipelineId' | 'amountCents' | 'currency' | 'probability'>\n\nconst major = (cents: number): number => Math.round(cents) / 100\n\nfunction deals(firestore: Firestore, orgId: string, hostId: string | null): FirebaseFirestore.Query {\n const collection = firestore.collection('orgs').doc(orgId).collection('deals')\n return hostId ? scopedToHost(collection, hostId) : collection\n}\n\nasync function readOpenDeals(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n): Promise<{ rows: DealRow[]; truncated: boolean }> {\n const snapshot = await deals(firestore, orgId, hostId)\n .where('status', '==', 'open')\n .orderBy('updatedAt', 'desc')\n .select('status', 'stageId', 'pipelineId', 'amountCents', 'currency', 'probability')\n .limit(DEAL_FIGURES_OPEN_CEILING + 1)\n .get()\n return {\n rows: snapshot.docs.slice(0, DEAL_FIGURES_OPEN_CEILING).map((doc) => (doc.data() ?? {}) as DealRow),\n truncated: snapshot.docs.length > DEAL_FIGURES_OPEN_CEILING,\n }\n}\n\nasync function readPipelines(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n): Promise<Array<{ id: string; pipeline: CrmPipeline }>> {\n const collection = firestore.collection('orgs').doc(orgId).collection('pipelines')\n const snapshot = await (hostId ? scopedToHost(collection, hostId) : collection)\n .limit(DEAL_FIGURES_PIPELINE_CEILING)\n .get()\n return snapshot.docs\n .map((doc) => ({ id: doc.id, pipeline: (doc.data() ?? {}) as CrmPipeline }))\n .filter(({ pipeline }) => !isPipelineArchived(pipeline))\n}\n\nexport function crmPipelineFigureReader(firestore: () => Firestore): PluginFigureReader {\n return {\n id: 'crm.pipeline',\n label: 'Deal pipeline',\n description:\n 'The open deals as they stand, by pipeline and stage: how many, what they are worth, and what they are worth at each stage’s odds. Current totals, with no window.',\n scope: 'org',\n windows: [],\n feature: 'crm',\n weeklySummary: {\n order: 20,\n summarize: (table) => {\n const all = figureRow(table, 'pipeline', 'All pipelines')\n if (!all || !figureCount(all['deals'])) return null\n return {\n title: 'Open pipeline',\n stats: [figureStat(table, all, 'deals', 'Open deals'), figureStat(table, all, 'value', 'Value'), figureStat(table, all, 'weighted', 'Weighted value')],\n }\n },\n },\n read: async (request) => {\n const db = firestore()\n const [open, pipelines] = await Promise.all([\n readOpenDeals(db, request.orgId, request.hostId),\n readPipelines(db, request.orgId, request.hostId),\n ])\n const { currency, mixed } = currencyOfDeals(open.rows)\n const code = currency.toUpperCase()\n const rows: PluginFigureRow[] = []\n let count = 0\n let amount = 0\n let weighted = 0\n let placed = 0\n for (const { id, pipeline } of pipelines) {\n const totals = pipelineTotals(\n open.rows.filter((deal) => deal.pipelineId === id),\n pipeline,\n )\n count += totals.count\n amount += totals.amountCents\n weighted += totals.weightedCents\n for (const stage of totals.stages) {\n placed += 1\n rows.push({\n pipeline: pipeline.name || 'Untitled pipeline',\n stage: stage.stage.name || 'Untitled stage',\n deals: stage.count,\n value: major(stage.amountCents),\n weighted: major(stage.weightedCents),\n })\n }\n }\n const kept = rows.slice(0, PLUGIN_FIGURE_MAX_ROWS - 1)\n return {\n ok: true,\n table: {\n title: 'Deal pipeline',\n source: { label: 'CRM reports', path: 'crm/reports' },\n period: null,\n columns: [\n { key: 'pipeline', label: 'Pipeline', kind: 'text' },\n { key: 'stage', label: 'Stage', kind: 'text' },\n { key: 'deals', label: 'Open deals', kind: 'count' },\n { key: 'value', label: 'Value', kind: 'money', currency: code },\n { key: 'weighted', label: 'Weighted value', kind: 'money', currency: code },\n ],\n rows: [\n { pipeline: 'All pipelines', stage: 'All open stages', deals: count, value: major(amount), weighted: major(weighted) },\n ...kept,\n ],\n omitted: Math.max(0, placed - kept.length),\n notes: [\n 'Only open deals: a won deal is revenue and a lost one is history.',\n ...(mixed ? [`Deals are in more than one currency; every amount is added as ${code}.`] : []),\n ...(open.truncated\n ? [`More than ${DEAL_FIGURES_OPEN_CEILING.toLocaleString('en-US')} deals are open, so the figures count the most recently updated ${DEAL_FIGURES_OPEN_CEILING.toLocaleString('en-US')} and read low.`]\n : []),\n ].slice(0, 3),\n },\n }\n },\n }\n}\n\ninterface ClosedRead {\n count: number\n amountCents: number\n currencies: Array<Pick<CrmDeal, 'currency'>>\n truncated: boolean\n}\n\nasync function readClosed(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n status: 'won' | 'lost',\n startMs: number,\n endMs: number,\n): Promise<ClosedRead> {\n const snapshot = await deals(firestore, orgId, hostId)\n .where('status', '==', status)\n .where('closedAtMs', '>=', startMs)\n .where('closedAtMs', '<', endMs)\n .orderBy('closedAtMs', 'asc')\n .select('amountCents', 'currency')\n .limit(DEAL_FIGURES_CLOSED_CEILING + 1)\n .get()\n const docs = snapshot.docs.slice(0, DEAL_FIGURES_CLOSED_CEILING).map((doc) => (doc.data() ?? {}) as DealRow)\n return {\n count: docs.length,\n amountCents: docs.reduce((sum, deal) => sum + Math.max(0, Math.round(Number(deal.amountCents ?? 0) || 0)), 0),\n currencies: docs,\n truncated: snapshot.docs.length > DEAL_FIGURES_CLOSED_CEILING,\n }\n}\n\nexport function crmClosedDealsFigureReader(firestore: () => Firestore): PluginFigureReader {\n return {\n id: 'crm.closed',\n label: 'Deals won and lost',\n description:\n 'Deals closed over the window: how many were won and lost, what was won, the win rate, and the change in deals won from the window before.',\n scope: 'org',\n windows: WINDOWS,\n feature: 'crm',\n weeklySummary: {\n order: 10,\n summarize: (table) => {\n const row = table.rows[0]\n if (!row || !figureHasColumn(table, 'won')) return null\n if (!figureCount(row['won']) && !figureCount(row['lost'])) return null\n return {\n title: 'Deals closed',\n stats: [\n figureStat(table, row, 'won', 'Won', { change: 'change', total: { one: 'deal won', many: 'deals won' } }),\n figureStat(table, row, 'wonValue', 'Value won'),\n ...(figureCount(row['lost']) ? [figureStat(table, row, 'lost', 'Lost')] : []),\n ],\n }\n },\n },\n read: async (request) => {\n if (!WINDOWS.includes(request.days)) {\n return { ok: false, status: 400, error: 'That window is not one closed deals are read over' }\n }\n const db = firestore()\n const { current, previous } = pluginFigureWindows(request.now, request.days)\n const [won, lost, wonBefore] = await Promise.all([\n readClosed(db, request.orgId, request.hostId, 'won', current.startMs, current.endMs),\n readClosed(db, request.orgId, request.hostId, 'lost', current.startMs, current.endMs),\n readClosed(db, request.orgId, request.hostId, 'won', previous.startMs, previous.endMs),\n ])\n const { currency, mixed } = currencyOfDeals(won.currencies)\n const code = currency.toUpperCase()\n const closed = won.count + lost.count\n return {\n ok: true,\n table: {\n title: 'Deals won and lost',\n source: { label: 'CRM reports', path: 'crm/reports' },\n period: { from: current.from, to: current.to, days: request.days },\n columns: [\n { key: 'won', label: 'Won', kind: 'count' },\n { key: 'lost', label: 'Lost', kind: 'count' },\n { key: 'wonValue', label: 'Value won', kind: 'money', currency: code },\n { key: 'winRate', label: 'Win rate', kind: 'percent' },\n { key: 'change', label: 'Change in deals won', kind: 'change' },\n ],\n rows: [\n {\n won: won.count,\n lost: lost.count,\n wonValue: major(won.amountCents),\n winRate: closed > 0 ? Math.round((won.count / closed) * 1_000) / 10 : null,\n change: pluginFigureChange(won.count, wonBefore.count),\n },\n ],\n omitted: 0,\n notes: [\n 'A deal is counted on the day it closed.',\n ...(mixed ? [`Deals won are in more than one currency; every amount is added as ${code}.`] : []),\n ...(won.truncated || lost.truncated || wonBefore.truncated\n ? [`More than ${DEAL_FIGURES_CLOSED_CEILING.toLocaleString('en-US')} deals closed one way in a window, so the figures count ${DEAL_FIGURES_CLOSED_CEILING.toLocaleString('en-US')} and read low.`]\n : []),\n ].slice(0, 3),\n },\n }\n },\n }\n}\n\n/** Registers the deal readers from the console surface, where insight jobs run. */\nexport function registerDealFigureReaders(firestore: () => Firestore): void {\n for (const reader of [crmPipelineFigureReader(firestore), crmClosedDealsFigureReader(firestore)]) {\n registerPluginFigureReader(reader, { pluginId: BUNDLE_ID })\n }\n}\n"],"names":["isPipelineArchived","PLUGIN_FIGURE_MAX_ROWS","pluginFigureChange","pluginFigureWindows","registerPluginFigureReader","figureCount","figureHasColumn","figureRow","figureStat","scopedToHost","BUNDLE_ID","currencyOfDeals","pipelineTotals","DEAL_FIGURES_OPEN_CEILING","DEAL_FIGURES_CLOSED_CEILING","DEAL_FIGURES_PIPELINE_CEILING","WINDOWS","major","cents","Math","round","deals","firestore","orgId","hostId","collection","doc","readOpenDeals","snapshot","where","orderBy","select","limit","get","rows","docs","slice","map","data","truncated","length","readPipelines","id","pipeline","filter","crmPipelineFigureReader","label","description","scope","windows","feature","weeklySummary","order","summarize","table","all","title","stats","read","request","db","open","pipelines","Promise","currency","mixed","code","toUpperCase","count","amount","weighted","placed","totals","deal","pipelineId","amountCents","weightedCents","stage","stages","push","name","value","kept","ok","source","path","period","columns","key","kind","omitted","max","notes","toLocaleString","readClosed","status","startMs","endMs","reduce","sum","Number","currencies","crmClosedDealsFigureReader","row","change","total","one","many","includes","days","error","current","previous","now","won","lost","wonBefore","closed","from","to","wonValue","winRate","registerDealFigureReaders","reader","pluginId"],"mappings":"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"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { pluginFigureChange, pluginFigureWindows, registerPluginFigureReader } from "@aglyn/aglyn/plugin-manager/plugin-figures";
17
- import { figureCount, figureRow, figureWithChange } from "@aglyn/aglyn/app-utils/weekly-summary";
17
+ import { figureCount, figureRow, figureStat } from "@aglyn/aglyn/app-utils/weekly-summary";
18
18
  import { scopedToHost } from "@aglyn/tenant-data-admin/server/organizations";
19
19
  import { Timestamp } from "firebase-admin/firestore";
20
20
  import { BUNDLE_ID } from "../constants/bundle-common.js";
@@ -45,29 +45,33 @@ export function crmPeopleFigureReader(firestore) {
45
45
  scope: 'org',
46
46
  windows: WINDOWS,
47
47
  feature: 'crm',
48
- // Under each site: the leads a site captured are that site's news.
48
+ // In each site's email: the leads a site captured are that site's news.
49
49
  weeklySummary: {
50
50
  per: 'site',
51
51
  order: 50,
52
- lines: (table)=>{
52
+ summarize: (table)=>{
53
53
  const leads = figureRow(table, 'figure', 'New leads');
54
54
  const contacts = figureRow(table, 'figure', 'New contacts');
55
- const parts = [
56
- leads && figureCount(leads['current']) ? `New leads: ${figureWithChange(table, leads, 'current', 'change')}` : null,
57
- contacts && figureCount(contacts['current']) ? `New contacts: ${figureWithChange(table, contacts, 'current', 'change')}` : null
58
- ].filter(Boolean);
59
- return parts.length ? [
60
- parts.join(' · ')
61
- ] : [];
62
- },
63
- headline: (table)=>{
64
- var _figureRow;
65
- const count = figureCount((_figureRow = figureRow(table, 'figure', 'New leads')) == null ? void 0 : _figureRow['current']);
66
- return count ? {
67
- count,
68
- one: 'new lead',
69
- many: 'new leads'
70
- } : null;
55
+ if (!figureCount(leads == null ? void 0 : leads['current']) && !figureCount(contacts == null ? void 0 : contacts['current'])) return null;
56
+ return {
57
+ title: 'New people',
58
+ stats: [
59
+ ...leads ? [
60
+ figureStat(table, leads, 'current', 'New leads', {
61
+ change: 'change',
62
+ total: {
63
+ one: 'new lead',
64
+ many: 'new leads'
65
+ }
66
+ })
67
+ ] : [],
68
+ ...contacts ? [
69
+ figureStat(table, contacts, 'current', 'New contacts', {
70
+ change: 'change'
71
+ })
72
+ ] : []
73
+ ]
74
+ };
71
75
  }
72
76
  },
73
77
  read: async (request)=>{
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/people-figures.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n pluginFigureChange,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { figureCount, figureRow, figureWithChange } from '@aglyn/aglyn/app-utils/weekly-summary'\nimport { scopedToHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { Timestamp } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * The people the CRM gained over a window, as a figure table another plugin\n * or the platform reads by id (AGL-3867): the weekly summary's \"new leads\"\n * and \"new contacts\" lines, and an insight's answer to \"are we getting more\n * leads?\".\n *\n * An organization reader, like the deal readers: with a site, only the\n * records shared with that site (`scopedToHost`, the predicate the rules\n * evaluate); with none, the workspace's. Each figure is one `count()`\n * aggregation over the window, so a busy CRM costs the same as a quiet one,\n * and no record — let alone a name or an address — ever reaches the table.\n *\n * A lead is counted on the day it was first seen (`firstSeenAtMs`), a contact\n * on the day its record was created (`createdAt`), which are the fields the\n * Leads and Contacts lists sort their newest by. Both are served by the\n * `visibleTo` composites the index file already carries.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nconst WINDOWS: readonly number[] = [7, 14, 30, 90]\n\nfunction scoped(collection: FirebaseFirestore.CollectionReference, hostId: string | null) {\n return hostId ? scopedToHost(collection, hostId) : collection\n}\n\nasync function countLeads(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n startMs: number,\n endMs: number,\n): Promise<number> {\n const leads = firestore.collection('orgs').doc(orgId).collection('leads')\n const snapshot = await scoped(leads, hostId)\n .where('firstSeenAtMs', '>=', startMs)\n .where('firstSeenAtMs', '<', endMs)\n .count()\n .get()\n return snapshot.data().count\n}\n\nasync function countContacts(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n startMs: number,\n endMs: number,\n): Promise<number> {\n const contacts = firestore.collection('orgs').doc(orgId).collection('contacts')\n const snapshot = await scoped(contacts, hostId)\n .where('createdAt', '>=', Timestamp.fromMillis(startMs))\n .where('createdAt', '<', Timestamp.fromMillis(endMs))\n .count()\n .get()\n return snapshot.data().count\n}\n\nexport function crmPeopleFigureReader(firestore: () => Firestore): PluginFigureReader {\n return {\n id: 'crm.people',\n label: 'New leads and contacts',\n description:\n 'How many leads were first seen and how many contacts were added over the window, against the window before.',\n scope: 'org',\n windows: WINDOWS,\n feature: 'crm',\n // Under each site: the leads a site captured are that site's news.\n weeklySummary: {\n per: 'site',\n order: 50,\n lines: (table) => {\n const leads = figureRow(table, 'figure', 'New leads')\n const contacts = figureRow(table, 'figure', 'New contacts')\n const parts = [\n leads && figureCount(leads['current']) ? `New leads: ${figureWithChange(table, leads, 'current', 'change')}` : null,\n contacts && figureCount(contacts['current'])\n ? `New contacts: ${figureWithChange(table, contacts, 'current', 'change')}`\n : null,\n ].filter(Boolean)\n return parts.length ? [parts.join(' · ')] : []\n },\n headline: (table) => {\n const count = figureCount(figureRow(table, 'figure', 'New leads')?.['current'])\n return count ? { count, one: 'new lead', many: 'new leads' } : null\n },\n },\n read: async (request) => {\n if (!WINDOWS.includes(request.days)) {\n return { ok: false, status: 400, error: 'That window is not one new leads and contacts are read over' }\n }\n const db = firestore()\n const { current, previous } = pluginFigureWindows(request.now, request.days)\n const [leads, leadsBefore, contacts, contactsBefore] = await Promise.all([\n countLeads(db, request.orgId, request.hostId, current.startMs, current.endMs),\n countLeads(db, request.orgId, request.hostId, previous.startMs, previous.endMs),\n countContacts(db, request.orgId, request.hostId, current.startMs, current.endMs),\n countContacts(db, request.orgId, request.hostId, previous.startMs, previous.endMs),\n ])\n return {\n ok: true,\n table: {\n title: 'New leads and contacts',\n source: { label: 'CRM', path: 'crm' },\n period: { from: current.from, to: current.to, days: request.days },\n columns: [\n { key: 'figure', label: 'Figure', kind: 'text' },\n { key: 'current', label: 'This window', kind: 'count' },\n { key: 'previous', label: 'Window before', kind: 'count' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows: [\n { figure: 'New leads', current: leads, previous: leadsBefore, change: pluginFigureChange(leads, leadsBefore) },\n {\n figure: 'New contacts',\n current: contacts,\n previous: contactsBefore,\n change: pluginFigureChange(contacts, contactsBefore),\n },\n ],\n omitted: 0,\n notes: ['A lead is counted on the day it was first seen, a contact on the day it was added.'],\n },\n }\n },\n }\n}\n\n/** Registers the reader from the console surface, where insight jobs and the weekly summary run. */\nexport function registerPeopleFigureReader(firestore: () => Firestore): void {\n registerPluginFigureReader(crmPeopleFigureReader(firestore), { pluginId: BUNDLE_ID })\n}\n"],"names":["pluginFigureChange","pluginFigureWindows","registerPluginFigureReader","figureCount","figureRow","figureWithChange","scopedToHost","Timestamp","BUNDLE_ID","WINDOWS","scoped","collection","hostId","countLeads","firestore","orgId","startMs","endMs","leads","doc","snapshot","where","count","get","data","countContacts","contacts","fromMillis","crmPeopleFigureReader","id","label","description","scope","windows","feature","weeklySummary","per","order","lines","table","parts","filter","Boolean","length","join","headline","one","many","read","request","includes","days","ok","status","error","db","current","previous","now","leadsBefore","contactsBefore","Promise","all","title","source","path","period","from","to","columns","key","kind","rows","figure","change","omitted","notes","registerPeopleFigureReader","pluginId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/people-figures.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n pluginFigureChange,\n pluginFigureWindows,\n registerPluginFigureReader,\n type PluginFigureReader,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport { figureCount, figureRow, figureStat } from '@aglyn/aglyn/app-utils/weekly-summary'\nimport { scopedToHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { Timestamp } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * The people the CRM gained over a window, as a figure table another plugin\n * or the platform reads by id (AGL-3867): the weekly summary's \"new leads\"\n * and \"new contacts\" lines, and an insight's answer to \"are we getting more\n * leads?\".\n *\n * An organization reader, like the deal readers: with a site, only the\n * records shared with that site (`scopedToHost`, the predicate the rules\n * evaluate); with none, the workspace's. Each figure is one `count()`\n * aggregation over the window, so a busy CRM costs the same as a quiet one,\n * and no record — let alone a name or an address — ever reaches the table.\n *\n * A lead is counted on the day it was first seen (`firstSeenAtMs`), a contact\n * on the day its record was created (`createdAt`), which are the fields the\n * Leads and Contacts lists sort their newest by. Both are served by the\n * `visibleTo` composites the index file already carries.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nconst WINDOWS: readonly number[] = [7, 14, 30, 90]\n\nfunction scoped(collection: FirebaseFirestore.CollectionReference, hostId: string | null) {\n return hostId ? scopedToHost(collection, hostId) : collection\n}\n\nasync function countLeads(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n startMs: number,\n endMs: number,\n): Promise<number> {\n const leads = firestore.collection('orgs').doc(orgId).collection('leads')\n const snapshot = await scoped(leads, hostId)\n .where('firstSeenAtMs', '>=', startMs)\n .where('firstSeenAtMs', '<', endMs)\n .count()\n .get()\n return snapshot.data().count\n}\n\nasync function countContacts(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n startMs: number,\n endMs: number,\n): Promise<number> {\n const contacts = firestore.collection('orgs').doc(orgId).collection('contacts')\n const snapshot = await scoped(contacts, hostId)\n .where('createdAt', '>=', Timestamp.fromMillis(startMs))\n .where('createdAt', '<', Timestamp.fromMillis(endMs))\n .count()\n .get()\n return snapshot.data().count\n}\n\nexport function crmPeopleFigureReader(firestore: () => Firestore): PluginFigureReader {\n return {\n id: 'crm.people',\n label: 'New leads and contacts',\n description:\n 'How many leads were first seen and how many contacts were added over the window, against the window before.',\n scope: 'org',\n windows: WINDOWS,\n feature: 'crm',\n // In each site's email: the leads a site captured are that site's news.\n weeklySummary: {\n per: 'site',\n order: 50,\n summarize: (table) => {\n const leads = figureRow(table, 'figure', 'New leads')\n const contacts = figureRow(table, 'figure', 'New contacts')\n if (!figureCount(leads?.['current']) && !figureCount(contacts?.['current'])) return null\n return {\n title: 'New people',\n stats: [\n ...(leads\n ? [figureStat(table, leads, 'current', 'New leads', { change: 'change', total: { one: 'new lead', many: 'new leads' } })]\n : []),\n ...(contacts ? [figureStat(table, contacts, 'current', 'New contacts', { change: 'change' })] : []),\n ],\n }\n },\n },\n read: async (request) => {\n if (!WINDOWS.includes(request.days)) {\n return { ok: false, status: 400, error: 'That window is not one new leads and contacts are read over' }\n }\n const db = firestore()\n const { current, previous } = pluginFigureWindows(request.now, request.days)\n const [leads, leadsBefore, contacts, contactsBefore] = await Promise.all([\n countLeads(db, request.orgId, request.hostId, current.startMs, current.endMs),\n countLeads(db, request.orgId, request.hostId, previous.startMs, previous.endMs),\n countContacts(db, request.orgId, request.hostId, current.startMs, current.endMs),\n countContacts(db, request.orgId, request.hostId, previous.startMs, previous.endMs),\n ])\n return {\n ok: true,\n table: {\n title: 'New leads and contacts',\n source: { label: 'CRM', path: 'crm' },\n period: { from: current.from, to: current.to, days: request.days },\n columns: [\n { key: 'figure', label: 'Figure', kind: 'text' },\n { key: 'current', label: 'This window', kind: 'count' },\n { key: 'previous', label: 'Window before', kind: 'count' },\n { key: 'change', label: 'Change', kind: 'change' },\n ],\n rows: [\n { figure: 'New leads', current: leads, previous: leadsBefore, change: pluginFigureChange(leads, leadsBefore) },\n {\n figure: 'New contacts',\n current: contacts,\n previous: contactsBefore,\n change: pluginFigureChange(contacts, contactsBefore),\n },\n ],\n omitted: 0,\n notes: ['A lead is counted on the day it was first seen, a contact on the day it was added.'],\n },\n }\n },\n }\n}\n\n/** Registers the reader from the console surface, where insight jobs and the weekly summary run. */\nexport function registerPeopleFigureReader(firestore: () => Firestore): void {\n registerPluginFigureReader(crmPeopleFigureReader(firestore), { pluginId: BUNDLE_ID })\n}\n"],"names":["pluginFigureChange","pluginFigureWindows","registerPluginFigureReader","figureCount","figureRow","figureStat","scopedToHost","Timestamp","BUNDLE_ID","WINDOWS","scoped","collection","hostId","countLeads","firestore","orgId","startMs","endMs","leads","doc","snapshot","where","count","get","data","countContacts","contacts","fromMillis","crmPeopleFigureReader","id","label","description","scope","windows","feature","weeklySummary","per","order","summarize","table","title","stats","change","total","one","many","read","request","includes","days","ok","status","error","db","current","previous","now","leadsBefore","contactsBefore","Promise","all","source","path","period","from","to","columns","key","kind","rows","figure","omitted","notes","registerPeopleFigureReader","pluginId"],"mappings":"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"}