@aglyn/plugins-crm 1.0.0-beta.145 → 1.0.0-beta.147

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.145",
3
+ "version": "1.0.0-beta.147",
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.145",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.145",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.145",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.145",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.145",
33
- "@aglyn/shared-ui-next": "1.0.0-beta.145",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.145",
35
- "@aglyn/shared-util-email": "1.0.0-beta.145",
36
- "@aglyn/shared-util-http": "1.0.0-beta.145",
37
- "@aglyn/tenant-data-admin": "1.0.0-beta.145",
38
- "@aglyn/tenant-feature-instance": "1.0.0-beta.145",
39
- "@aglyn/tenant-runtime": "1.0.0-beta.145",
28
+ "@aglyn/aglyn": "1.0.0-beta.147",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.147",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.147",
31
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.147",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.147",
33
+ "@aglyn/shared-ui-next": "1.0.0-beta.147",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.147",
35
+ "@aglyn/shared-util-email": "1.0.0-beta.147",
36
+ "@aglyn/shared-util-http": "1.0.0-beta.147",
37
+ "@aglyn/tenant-data-admin": "1.0.0-beta.147",
38
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.147",
39
+ "@aglyn/tenant-runtime": "1.0.0-beta.147",
40
40
  "@dnd-kit/core": "^6.3.1",
41
41
  "@dnd-kit/utilities": "^3.2.2",
42
42
  "@swc/helpers": "0.5.23",
@@ -17,7 +17,7 @@
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
19
  import * as Aglyn from "@aglyn/aglyn";
20
- import { money } from "@aglyn/shared-ui-email-campaigns/components/report-figures";
20
+ import { money } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
21
21
  import { AppLink, CardDisplay } from "@aglyn/shared-ui-jsx";
22
22
  import { Button, Stack, Typography } from "@mui/material";
23
23
  import { getAggregateFromServer, getCountFromServer, query, sum, Timestamp, where } from "firebase/firestore";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-glance-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-email-campaigns/components/report-figures'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Button, Stack, Typography } from '@mui/material'\nimport {\n getAggregateFromServer,\n getCountFromServer,\n query,\n sum,\n Timestamp,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useOrgDataScope,\n useOrgPlan,\n} from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { crmRoutes } from '../model/crm-routes'\nimport { openLeadsAcrossSites } from './reports/lead-counts'\nimport { scopedCollection, visibleToClause } from './reports/report-scope'\nimport { ReportStatTile } from './reports/report-stat-tile'\nimport { useAggregateRead } from './reports/use-aggregate-read'\n\nconst WEEK_MS = 7 * 24 * 60 * 60 * 1000\n\ninterface GlanceFigures {\n contacts: number\n newThisWeek: number\n pipelineCents: number\n tasksDue: number\n /** Open leads — new or being worked — on this site, or across the org's sites. */\n leadsToWork: number\n}\n\nexport interface CrmGlanceCardProps {\n /**\n * The site the card is on the dashboard of, or `null` beneath the org\n * hub's mount (AGL-2634), where the figures total every site: the\n * org-wide reads carry no scope clause, and the lead figure fans out\n * over the mount's site list.\n */\n hostId: string | null\n /** The org hub's own path, for the links when there is no site to derive them from. */\n basePath?: string\n}\n\n/**\n * CRM at a glance (AGL-2604): a `hostDashboard` widget with five numbers —\n * contacts, new this week, open pipeline value, tasks due today or overdue,\n * and leads still to work (AGL-2624) — each a link into the section that\n * explains it.\n *\n * Server aggregates and nothing else: a dashboard card mounts on every\n * visit to the site's front page, so it is the one surface on which a\n * bounded read of a thousand rows would be a cost paid for nothing. Each\n * number is a count or a sum the server takes over the same `visibleTo`\n * predicate the hub's sections query with, so the card never shows a figure\n * the section behind the link would not.\n *\n * The lead figure is two counts and a subtraction — every lead on the site,\n * less the closed ones — because a lead nobody has touched carries no status\n * field and Firestore cannot select on a field's absence; see\n * `openLeadsFromCounts`. Leads are host-scoped by path rather than by\n * `visibleTo`, so those two counts run on `hosts/{hostId}/leads` under the\n * site's own rules, the way the Leads section reads it.\n *\n * The widget is handed a host doc id and nothing else, so the org — needed\n * for the consent group whose tokens scope the reads — comes from\n * `useOrgPlan`, and the link base from `useConsoleHostRoute`, the way the\n * commerce glance card builds its store link. Nothing is read until the org\n * has settled: a first round of aggregates against the solo group, replaced\n * by a second against the real one, would be two reads for one answer.\n *\n * ## The organization level (AGL-2634)\n *\n * Mounted with no site beneath the org hub's mount, the card totals the\n * organization: the reads carry no scope clause — the rules admit an\n * org-wide member to every row — and the lead figure is the same two\n * counts per site, over every site the mount lists, summed. Nothing is\n * read until that list has settled, for the reason nothing is read under\n * a site until the org has. The org's dashboard has no widget slot yet, so\n * the mount is a capability rather than a page; what it needs from the\n * shell is a slot and the hub's `basePath`.\n */\nexport function CrmGlanceCard(props: CrmGlanceCardProps) {\n const { hostId, basePath } = props\n const mount = useCrmOrgMount()\n const consoleRoute = useConsoleHostRoute(hostId)\n const { org, ready: planReady } = useOrgPlan(hostId ?? undefined)\n const { scope } = useOrgDataScope(hostId ? { hostId } : { orgId: mount?.orgId })\n const firestore = useFirestore()\n // Under a site, the site's group's tokens once the org has settled; at\n // the org level `null` — no clause — once the site list has.\n const tokens = useMemo(() => {\n if (!hostId) return null\n if (!planReady) return undefined\n const group = Aglyn.consentGroupForHost(org ?? null, hostId)\n return [\n Aglyn.ORG_SCOPE_TOKEN,\n ...group.hostIds.map((id) => Aglyn.hostScopeToken(id)),\n ].slice(0, Aglyn.MAX_SCOPE_HOSTS)\n }, [org, planReady, hostId])\n const leadSites = useMemo(\n () =>\n hostId\n ? [hostId]\n : mount?.hostsReady\n ? mount.hosts.map((host) => host.id)\n : null,\n [hostId, mount],\n )\n const leadSitesKey = leadSites?.join('\\n') ?? ''\n // Anchored once per mount so the queries are stable across renders.\n const nowMs = useMemo(() => Date.now(), [])\n\n const figures = useAggregateRead<GlanceFigures>(() => {\n if (!scope || tokens === undefined || !leadSites) return null\n const contacts = scopedCollection(firestore, scope, 'contacts')\n const deals = scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.deals)\n const tasks = scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks)\n const day = Aglyn.localDayBounds(nowMs)\n const countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n return Promise.all([\n countOf(query(contacts, ...visibleToClause(tokens))),\n countOf(\n query(\n contacts,\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(nowMs - WEEK_MS)),\n ),\n ),\n getAggregateFromServer(\n query(deals, ...visibleToClause(tokens), where('status', '==', 'open')),\n { amountCents: sum('amountCents') },\n ).then((snapshot) => Number(snapshot.data().amountCents ?? 0)),\n // Due today OR overdue: everything open that is due before the day ends.\n countOf(\n query(\n tasks,\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n where('dueAtMs', '<', day.end),\n ),\n ),\n openLeadsAcrossSites(firestore, leadSites),\n ]).then(([contacts, newThisWeek, pipelineCents, tasksDue, leadsToWork]) => ({\n contacts,\n newThisWeek,\n pipelineCents,\n tasksDue,\n leadsToWork,\n }))\n }, [firestore, scope, tokens, nowMs, leadSitesKey])\n\n const routes = useMemo(\n () =>\n hostId\n ? consoleRoute.base\n ? crmRoutes(`${consoleRoute.base}/crm`)\n : null\n : basePath\n ? crmRoutes(basePath)\n : null,\n [hostId, consoleRoute.base, basePath],\n )\n const hubHref = hostId\n ? consoleRoute.base\n ? `${consoleRoute.base}/crm`\n : undefined\n : basePath\n const value = figures.value\n\n return (\n <CardDisplay\n header={'CRM at a glance'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#crm-at-a-glance',\n excerpt:\n 'Contacts, new contacts this week, the value of every open deal, ' +\n 'the tasks due today or overdue, and the leads still to work — ' +\n 'each counted on the server and each a link into the CRM.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={hubHref}\n size=\"small\"\n color=\"primary\"\n >\n {'Open CRM'}\n </Button>\n ),\n }}\n >\n <Stack spacing={1.5}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Contacts'}\n value={value ? value.contacts.toLocaleString() : null}\n href={routes?.section('contacts')}\n />\n <ReportStatTile\n label={'New this week'}\n value={value ? value.newThisWeek.toLocaleString() : null}\n href={routes?.section('contacts')}\n />\n <ReportStatTile\n label={'Open pipeline'}\n value={value ? money(value.pipelineCents, 'usd') : null}\n href={routes?.section('deals')}\n />\n <ReportStatTile\n label={'Tasks due'}\n value={value ? value.tasksDue.toLocaleString() : null}\n note={'today or overdue'}\n color={value?.tasksDue ? 'warning.main' : undefined}\n href={routes?.section('tasks')}\n />\n <ReportStatTile\n label={'Leads to work'}\n value={value ? value.leadsToWork.toLocaleString() : null}\n note={hostId ? 'open on this site' : 'open across every site'}\n href={routes?.section('leads')}\n />\n </Stack>\n {figures.status === 'error' ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The CRM figures could not be read. Open the CRM for the full picture.'}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nCrmGlanceCard.displayName = 'CrmGlanceCard'\n\nexport default CrmGlanceCard\n"],"names":["Aglyn","money","AppLink","CardDisplay","Button","Stack","Typography","getAggregateFromServer","getCountFromServer","query","sum","Timestamp","where","useMemo","useConsoleHostRoute","useFirestore","useOrgDataScope","useOrgPlan","useCrmOrgMount","crmRoutes","openLeadsAcrossSites","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","WEEK_MS","CrmGlanceCard","props","hostId","basePath","mount","consoleRoute","org","ready","planReady","undefined","scope","orgId","firestore","tokens","group","consentGroupForHost","ORG_SCOPE_TOKEN","hostIds","map","id","hostScopeToken","slice","MAX_SCOPE_HOSTS","leadSites","hostsReady","hosts","host","leadSitesKey","join","nowMs","Date","now","figures","contacts","deals","CRM_COLLECTIONS","tasks","day","localDayBounds","countOf","target","then","snapshot","data","count","Promise","all","fromMillis","amountCents","Number","end","newThisWeek","pipelineCents","tasksDue","leadsToWork","routes","base","hubHref","value","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","component","componentVariant","nativeButton","href","size","color","spacing","direction","sx","flexWrap","label","toLocaleString","section","note","status","variant","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-glance-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 { Button, Stack, Typography } from '@mui/material'\nimport {\n getAggregateFromServer,\n getCountFromServer,\n query,\n sum,\n Timestamp,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useOrgDataScope,\n useOrgPlan,\n} from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { crmRoutes } from '../model/crm-routes'\nimport { openLeadsAcrossSites } from './reports/lead-counts'\nimport { scopedCollection, visibleToClause } from './reports/report-scope'\nimport { ReportStatTile } from './reports/report-stat-tile'\nimport { useAggregateRead } from './reports/use-aggregate-read'\n\nconst WEEK_MS = 7 * 24 * 60 * 60 * 1000\n\ninterface GlanceFigures {\n contacts: number\n newThisWeek: number\n pipelineCents: number\n tasksDue: number\n /** Open leads — new or being worked — on this site, or across the org's sites. */\n leadsToWork: number\n}\n\nexport interface CrmGlanceCardProps {\n /**\n * The site the card is on the dashboard of, or `null` beneath the org\n * hub's mount (AGL-2634), where the figures total every site: the\n * org-wide reads carry no scope clause, and the lead figure fans out\n * over the mount's site list.\n */\n hostId: string | null\n /** The org hub's own path, for the links when there is no site to derive them from. */\n basePath?: string\n}\n\n/**\n * CRM at a glance (AGL-2604): a `hostDashboard` widget with five numbers —\n * contacts, new this week, open pipeline value, tasks due today or overdue,\n * and leads still to work (AGL-2624) — each a link into the section that\n * explains it.\n *\n * Server aggregates and nothing else: a dashboard card mounts on every\n * visit to the site's front page, so it is the one surface on which a\n * bounded read of a thousand rows would be a cost paid for nothing. Each\n * number is a count or a sum the server takes over the same `visibleTo`\n * predicate the hub's sections query with, so the card never shows a figure\n * the section behind the link would not.\n *\n * The lead figure is two counts and a subtraction — every lead on the site,\n * less the closed ones — because a lead nobody has touched carries no status\n * field and Firestore cannot select on a field's absence; see\n * `openLeadsFromCounts`. Leads are host-scoped by path rather than by\n * `visibleTo`, so those two counts run on `hosts/{hostId}/leads` under the\n * site's own rules, the way the Leads section reads it.\n *\n * The widget is handed a host doc id and nothing else, so the org — needed\n * for the consent group whose tokens scope the reads — comes from\n * `useOrgPlan`, and the link base from `useConsoleHostRoute`, the way the\n * commerce glance card builds its store link. Nothing is read until the org\n * has settled: a first round of aggregates against the solo group, replaced\n * by a second against the real one, would be two reads for one answer.\n *\n * ## The organization level (AGL-2634)\n *\n * Mounted with no site beneath the org hub's mount, the card totals the\n * organization: the reads carry no scope clause — the rules admit an\n * org-wide member to every row — and the lead figure is the same two\n * counts per site, over every site the mount lists, summed. Nothing is\n * read until that list has settled, for the reason nothing is read under\n * a site until the org has. The org's dashboard has no widget slot yet, so\n * the mount is a capability rather than a page; what it needs from the\n * shell is a slot and the hub's `basePath`.\n */\nexport function CrmGlanceCard(props: CrmGlanceCardProps) {\n const { hostId, basePath } = props\n const mount = useCrmOrgMount()\n const consoleRoute = useConsoleHostRoute(hostId)\n const { org, ready: planReady } = useOrgPlan(hostId ?? undefined)\n const { scope } = useOrgDataScope(hostId ? { hostId } : { orgId: mount?.orgId })\n const firestore = useFirestore()\n // Under a site, the site's group's tokens once the org has settled; at\n // the org level `null` — no clause — once the site list has.\n const tokens = useMemo(() => {\n if (!hostId) return null\n if (!planReady) return undefined\n const group = Aglyn.consentGroupForHost(org ?? null, hostId)\n return [\n Aglyn.ORG_SCOPE_TOKEN,\n ...group.hostIds.map((id) => Aglyn.hostScopeToken(id)),\n ].slice(0, Aglyn.MAX_SCOPE_HOSTS)\n }, [org, planReady, hostId])\n const leadSites = useMemo(\n () =>\n hostId\n ? [hostId]\n : mount?.hostsReady\n ? mount.hosts.map((host) => host.id)\n : null,\n [hostId, mount],\n )\n const leadSitesKey = leadSites?.join('\\n') ?? ''\n // Anchored once per mount so the queries are stable across renders.\n const nowMs = useMemo(() => Date.now(), [])\n\n const figures = useAggregateRead<GlanceFigures>(() => {\n if (!scope || tokens === undefined || !leadSites) return null\n const contacts = scopedCollection(firestore, scope, 'contacts')\n const deals = scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.deals)\n const tasks = scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks)\n const day = Aglyn.localDayBounds(nowMs)\n const countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n return Promise.all([\n countOf(query(contacts, ...visibleToClause(tokens))),\n countOf(\n query(\n contacts,\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(nowMs - WEEK_MS)),\n ),\n ),\n getAggregateFromServer(\n query(deals, ...visibleToClause(tokens), where('status', '==', 'open')),\n { amountCents: sum('amountCents') },\n ).then((snapshot) => Number(snapshot.data().amountCents ?? 0)),\n // Due today OR overdue: everything open that is due before the day ends.\n countOf(\n query(\n tasks,\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n where('dueAtMs', '<', day.end),\n ),\n ),\n openLeadsAcrossSites(firestore, leadSites),\n ]).then(([contacts, newThisWeek, pipelineCents, tasksDue, leadsToWork]) => ({\n contacts,\n newThisWeek,\n pipelineCents,\n tasksDue,\n leadsToWork,\n }))\n }, [firestore, scope, tokens, nowMs, leadSitesKey])\n\n const routes = useMemo(\n () =>\n hostId\n ? consoleRoute.base\n ? crmRoutes(`${consoleRoute.base}/crm`)\n : null\n : basePath\n ? crmRoutes(basePath)\n : null,\n [hostId, consoleRoute.base, basePath],\n )\n const hubHref = hostId\n ? consoleRoute.base\n ? `${consoleRoute.base}/crm`\n : undefined\n : basePath\n const value = figures.value\n\n return (\n <CardDisplay\n header={'CRM at a glance'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#crm-at-a-glance',\n excerpt:\n 'Contacts, new contacts this week, the value of every open deal, ' +\n 'the tasks due today or overdue, and the leads still to work — ' +\n 'each counted on the server and each a link into the CRM.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={hubHref}\n size=\"small\"\n color=\"primary\"\n >\n {'Open CRM'}\n </Button>\n ),\n }}\n >\n <Stack spacing={1.5}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Contacts'}\n value={value ? value.contacts.toLocaleString() : null}\n href={routes?.section('contacts')}\n />\n <ReportStatTile\n label={'New this week'}\n value={value ? value.newThisWeek.toLocaleString() : null}\n href={routes?.section('contacts')}\n />\n <ReportStatTile\n label={'Open pipeline'}\n value={value ? money(value.pipelineCents, 'usd') : null}\n href={routes?.section('deals')}\n />\n <ReportStatTile\n label={'Tasks due'}\n value={value ? value.tasksDue.toLocaleString() : null}\n note={'today or overdue'}\n color={value?.tasksDue ? 'warning.main' : undefined}\n href={routes?.section('tasks')}\n />\n <ReportStatTile\n label={'Leads to work'}\n value={value ? value.leadsToWork.toLocaleString() : null}\n note={hostId ? 'open on this site' : 'open across every site'}\n href={routes?.section('leads')}\n />\n </Stack>\n {figures.status === 'error' ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'The CRM figures could not be read. Open the CRM for the full picture.'}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nCrmGlanceCard.displayName = 'CrmGlanceCard'\n\nexport default CrmGlanceCard\n"],"names":["Aglyn","money","AppLink","CardDisplay","Button","Stack","Typography","getAggregateFromServer","getCountFromServer","query","sum","Timestamp","where","useMemo","useConsoleHostRoute","useFirestore","useOrgDataScope","useOrgPlan","useCrmOrgMount","crmRoutes","openLeadsAcrossSites","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","WEEK_MS","CrmGlanceCard","props","hostId","basePath","mount","consoleRoute","org","ready","planReady","undefined","scope","orgId","firestore","tokens","group","consentGroupForHost","ORG_SCOPE_TOKEN","hostIds","map","id","hostScopeToken","slice","MAX_SCOPE_HOSTS","leadSites","hostsReady","hosts","host","leadSitesKey","join","nowMs","Date","now","figures","contacts","deals","CRM_COLLECTIONS","tasks","day","localDayBounds","countOf","target","then","snapshot","data","count","Promise","all","fromMillis","amountCents","Number","end","newThisWeek","pipelineCents","tasksDue","leadsToWork","routes","base","hubHref","value","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","component","componentVariant","nativeButton","href","size","color","spacing","direction","sx","flexWrap","label","toLocaleString","section","note","status","variant","displayName"],"mappings":"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"}
@@ -16,7 +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 { money } from "@aglyn/shared-ui-email-campaigns/components/report-figures";
19
+ import { money } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
20
20
  import { CardDisplay } from "@aglyn/shared-ui-jsx";
21
21
  import { measuredRate, percent, Section } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
22
22
  import { Alert, Stack, Typography } from "@mui/material";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/closed-deals-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-email-campaigns/components/report-figures'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n getCountFromServer,\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 { ReportBarChart } from './report-bar-chart'\nimport { weekLabel } 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'\n\n/**\n * How many closed deals of each outcome the weekly chart reads.\n *\n * The tiles are server aggregates and never meet this bound; the chart\n * needs each deal's `closedAtMs` to place it in a week, so it reads them,\n * most recent first, and says when the period held more.\n */\nconst CLOSED_DEAL_CEILING = 500\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\n\nexport interface ClosedDealsCardProps {\n report: CrmReportScope\n}\n\n/**\n * Deals won and lost in the period, and the win rate over what closed\n * (AGL-2604).\n *\n * Won and lost are TWO queries rather than one over `status in [won,\n * lost]`: each is an equality the `(visibleTo, status, closedAtMs)` index\n * serves directly, and a card that drew one series from a merged read\n * would have to split it again by hand. The range is on `closedAtMs`, the\n * moment the deal was closed — not `updatedAt`, which a note added last\n * week would move.\n */\nexport function ClosedDealsCard(props: ClosedDealsCardProps) {\n const { report } = props\n const { scope, tokens, range, routes } = report\n const firestore = useFirestore()\n\n const closedInPeriod = (status: Aglyn.CrmDealStatus) =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', status),\n where('closedAtMs', '>=', range.from),\n where('closedAtMs', '<', range.to),\n )\n\n const closed = useAggregateRead(\n () =>\n Promise.all([\n getAggregateFromServer(closedInPeriod('won'), {\n count: count(),\n amountCents: sum('amountCents'),\n }).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n getCountFromServer(closedInPeriod('lost')).then(\n (snapshot) => snapshot.data().count,\n ),\n ]).then(([won, lost]) => ({ won, lost })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:totals') },\n )\n\n const wonWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('won'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:won') },\n )\n const lostWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('lost'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:lost') },\n )\n const wonStatus = wonWindow.status\n const lostStatus = lostWindow.status\n\n const chart = useMemo(() => {\n const wonWeeks = Aglyn.bucketByWeek(\n wonWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n const lostWeeks = Aglyn.bucketByWeek(\n lostWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n return {\n labels: wonWeeks.map((week) => weekLabel(week.start)),\n won: wonWeeks.map((week) => week.items.length),\n wonCents: wonWeeks.map((week) =>\n week.items.reduce(\n (total, deal) => total + Math.max(0, Number(deal.amountCents ?? 0)),\n 0,\n ),\n ),\n lost: lostWeeks.map((week) => week.items.length),\n currency: Aglyn.currencyOfDeals(wonWindow.rows).currency,\n }\n }, [wonWindow, lostWindow, range])\n\n const figures = closed.value\n const winRate = figures\n ? measuredRate(figures.won.count, figures.won.count + figures.lost, 'closed')\n : null\n const chartRead = wonStatus === 'success' && lostStatus === 'success'\n\n return (\n <CardDisplay\n header={'Won and lost'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#won-and-lost',\n excerpt:\n 'Deals closed in the period, by outcome and by week, with the ' +\n 'value of what was won and the win rate over everything that ' +\n 'closed. Placed by the moment each deal was closed.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Won'}\n value={figures ? figures.won.count.toLocaleString() : null}\n note={\n figures\n ? `${money(figures.won.amountCents, chart.currency)} closed won`\n : undefined\n }\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Lost'}\n value={figures ? figures.lost.toLocaleString() : null}\n note={'in the period'}\n riseIsGood={false}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Win rate'}\n value={winRate ? percent(winRate.value) : null}\n note={\n winRate\n ? `${winRate.numerator.toLocaleString()} of ${winRate.denominator.toLocaleString()} ${winRate.denominatorLabel}`\n : figures\n ? 'nothing closed in the period'\n : undefined\n }\n />\n </Stack>\n {closed.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The closed-deal totals could not be read.'}\n </Alert>\n ) : null}\n <Section title={'Won vs lost per week'}>\n <ReportBarChart\n labels={chart.labels}\n series={[\n { name: 'Won', color: 'success.main', values: chart.won },\n { name: 'Lost', color: 'error.main', values: chart.lost },\n ]}\n format={(value, seriesIndex, barIndex) =>\n seriesIndex === 0\n ? `Won: ${value.toLocaleString()} · ${money(chart.wonCents[barIndex] ?? 0, chart.currency)}`\n : `Lost: ${value.toLocaleString()}`\n }\n emptyText={chartRead ? 'Nothing closed in this period.' : 'Reading…'}\n />\n {wonWindow.truncated || lostWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`The chart places the ${CLOSED_DEAL_CEILING} most recently closed of each outcome; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nClosedDealsCard.displayName = 'ClosedDealsCard'\n\nexport default ClosedDealsCard\n"],"names":["Aglyn","money","CardDisplay","measuredRate","percent","Section","Alert","Stack","Typography","count","getAggregateFromServer","getCountFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","ReportBarChart","weekLabel","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","CLOSED_DEAL_CEILING","ClosedDealsCard","props","report","scope","tokens","range","routes","firestore","closedInPeriod","status","from","to","closed","Promise","all","amountCents","then","snapshot","Number","data","won","lost","cacheKey","wonWindow","lostWindow","wonStatus","lostStatus","chart","wonWeeks","bucketByWeek","rows","deal","closedAtMs","lostWeeks","labels","map","week","start","items","length","wonCents","reduce","total","Math","max","currency","currencyOfDeals","figures","value","winRate","chartRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","direction","sx","flexWrap","label","toLocaleString","note","undefined","href","section","riseIsGood","numerator","denominator","denominatorLabel","severity","title","series","name","color","values","format","seriesIndex","barIndex","emptyText","truncated","variant","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/closed-deals-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 { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n getCountFromServer,\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 { ReportBarChart } from './report-bar-chart'\nimport { weekLabel } 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'\n\n/**\n * How many closed deals of each outcome the weekly chart reads.\n *\n * The tiles are server aggregates and never meet this bound; the chart\n * needs each deal's `closedAtMs` to place it in a week, so it reads them,\n * most recent first, and says when the period held more.\n */\nconst CLOSED_DEAL_CEILING = 500\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\n\nexport interface ClosedDealsCardProps {\n report: CrmReportScope\n}\n\n/**\n * Deals won and lost in the period, and the win rate over what closed\n * (AGL-2604).\n *\n * Won and lost are TWO queries rather than one over `status in [won,\n * lost]`: each is an equality the `(visibleTo, status, closedAtMs)` index\n * serves directly, and a card that drew one series from a merged read\n * would have to split it again by hand. The range is on `closedAtMs`, the\n * moment the deal was closed — not `updatedAt`, which a note added last\n * week would move.\n */\nexport function ClosedDealsCard(props: ClosedDealsCardProps) {\n const { report } = props\n const { scope, tokens, range, routes } = report\n const firestore = useFirestore()\n\n const closedInPeriod = (status: Aglyn.CrmDealStatus) =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', status),\n where('closedAtMs', '>=', range.from),\n where('closedAtMs', '<', range.to),\n )\n\n const closed = useAggregateRead(\n () =>\n Promise.all([\n getAggregateFromServer(closedInPeriod('won'), {\n count: count(),\n amountCents: sum('amountCents'),\n }).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n getCountFromServer(closedInPeriod('lost')).then(\n (snapshot) => snapshot.data().count,\n ),\n ]).then(([won, lost]) => ({ won, lost })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:totals') },\n )\n\n const wonWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('won'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:won') },\n )\n const lostWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('lost'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:lost') },\n )\n const wonStatus = wonWindow.status\n const lostStatus = lostWindow.status\n\n const chart = useMemo(() => {\n const wonWeeks = Aglyn.bucketByWeek(\n wonWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n const lostWeeks = Aglyn.bucketByWeek(\n lostWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n return {\n labels: wonWeeks.map((week) => weekLabel(week.start)),\n won: wonWeeks.map((week) => week.items.length),\n wonCents: wonWeeks.map((week) =>\n week.items.reduce(\n (total, deal) => total + Math.max(0, Number(deal.amountCents ?? 0)),\n 0,\n ),\n ),\n lost: lostWeeks.map((week) => week.items.length),\n currency: Aglyn.currencyOfDeals(wonWindow.rows).currency,\n }\n }, [wonWindow, lostWindow, range])\n\n const figures = closed.value\n const winRate = figures\n ? measuredRate(figures.won.count, figures.won.count + figures.lost, 'closed')\n : null\n const chartRead = wonStatus === 'success' && lostStatus === 'success'\n\n return (\n <CardDisplay\n header={'Won and lost'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#won-and-lost',\n excerpt:\n 'Deals closed in the period, by outcome and by week, with the ' +\n 'value of what was won and the win rate over everything that ' +\n 'closed. Placed by the moment each deal was closed.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Won'}\n value={figures ? figures.won.count.toLocaleString() : null}\n note={\n figures\n ? `${money(figures.won.amountCents, chart.currency)} closed won`\n : undefined\n }\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Lost'}\n value={figures ? figures.lost.toLocaleString() : null}\n note={'in the period'}\n riseIsGood={false}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Win rate'}\n value={winRate ? percent(winRate.value) : null}\n note={\n winRate\n ? `${winRate.numerator.toLocaleString()} of ${winRate.denominator.toLocaleString()} ${winRate.denominatorLabel}`\n : figures\n ? 'nothing closed in the period'\n : undefined\n }\n />\n </Stack>\n {closed.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The closed-deal totals could not be read.'}\n </Alert>\n ) : null}\n <Section title={'Won vs lost per week'}>\n <ReportBarChart\n labels={chart.labels}\n series={[\n { name: 'Won', color: 'success.main', values: chart.won },\n { name: 'Lost', color: 'error.main', values: chart.lost },\n ]}\n format={(value, seriesIndex, barIndex) =>\n seriesIndex === 0\n ? `Won: ${value.toLocaleString()} · ${money(chart.wonCents[barIndex] ?? 0, chart.currency)}`\n : `Lost: ${value.toLocaleString()}`\n }\n emptyText={chartRead ? 'Nothing closed in this period.' : 'Reading…'}\n />\n {wonWindow.truncated || lostWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`The chart places the ${CLOSED_DEAL_CEILING} most recently closed of each outcome; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nClosedDealsCard.displayName = 'ClosedDealsCard'\n\nexport default ClosedDealsCard\n"],"names":["Aglyn","money","CardDisplay","measuredRate","percent","Section","Alert","Stack","Typography","count","getAggregateFromServer","getCountFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","ReportBarChart","weekLabel","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","CLOSED_DEAL_CEILING","ClosedDealsCard","props","report","scope","tokens","range","routes","firestore","closedInPeriod","status","from","to","closed","Promise","all","amountCents","then","snapshot","Number","data","won","lost","cacheKey","wonWindow","lostWindow","wonStatus","lostStatus","chart","wonWeeks","bucketByWeek","rows","deal","closedAtMs","lostWeeks","labels","map","week","start","items","length","wonCents","reduce","total","Math","max","currency","currencyOfDeals","figures","value","winRate","chartRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","direction","sx","flexWrap","label","toLocaleString","note","undefined","href","section","riseIsGood","numerator","denominator","denominatorLabel","severity","title","series","name","color","values","format","seriesIndex","barIndex","emptyText","truncated","variant","displayName"],"mappings":"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"}
@@ -16,7 +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 { money } from "@aglyn/shared-ui-email-campaigns/components/report-figures";
19
+ import { money } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
20
20
  import { CardDisplay } from "@aglyn/shared-ui-jsx";
21
21
  import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
22
22
  import { Box, Stack, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/forecast-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-email-campaigns/components/report-figures'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Box,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport { limit, orderBy, query, where } 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 { plural } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { useWindowRead } from './use-aggregate-read'\n\n/**\n * The same window the pipeline card reads, under the SAME cache key and\n * the same dependencies, so the two cards cost one read between them —\n * `useAggregateRead` joins a read already in the air for the key. The\n * numbers must match the pipeline card's query exactly or the key would\n * name two different questions.\n */\nconst OPEN_DEAL_CEILING = 1000\nconst PIPELINE_CEILING = 20\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\ntype PipelineRow = Aglyn.CrmPipeline & { $id: string }\n\nexport interface ForecastCardProps {\n report: CrmReportScope\n}\n\n/** `Sep 2026` — the month a column is. */\nfunction monthLabel(startMs: number): string {\n return new Date(startMs).toLocaleDateString(undefined, { month: 'short', year: 'numeric' })\n}\n\n/**\n * Forecast by close month (AGL-2620): the open pipeline laid out by the\n * month each deal is expected to close, for the next six, per pipeline,\n * at face value and weighted by the stage.\n *\n * Not period-scoped, like the pipeline card and for the same reason: the\n * forecast is what is open NOW, looked at forward. Deals with no expected\n * close are their own row rather than a footnote — that row is the size\n * of the pipeline nobody has scheduled, which is the number the picker\n * of a close date most needs to see — and deals dated before this month\n * (overdue) or past the sixth (later) are rows too, so the column adds up\n * to the pipeline.\n */\nexport function ForecastCard(props: ForecastCardProps) {\n const { report } = props\n const { scope, tokens, nowMs } = report\n const firestore = useFirestore()\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(scopedCollection(firestore, scope, 'pipelines'), ...visibleToClause(tokens)),\n PIPELINE_CEILING,\n ),\n PIPELINE_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:pipelines') },\n )\n\n const forecast = useMemo(\n () => Aglyn.forecastByCloseMonth(dealWindow.rows, pipelineWindow.rows, nowMs),\n [dealWindow.rows, pipelineWindow.rows, nowMs],\n )\n const currency = useMemo(() => Aglyn.currencyOfDeals(dealWindow.rows), [dealWindow.rows])\n const read = dealWindow.status === 'success' && pipelineWindow.status === 'success'\n const failed = dealWindow.status === 'error' || pipelineWindow.status === 'error'\n\n // A pipeline with nothing open is still a column; an unknown pipeline id\n // is a column named for what it is.\n const columns = forecast.pipelines\n const manyColumns = columns.length > 1\n\n const rows: Array<{ key: string; label: string; cellOf: (row: Aglyn.PipelineForecast) => Aglyn.ForecastCell; total: Aglyn.ForecastCell; muted?: boolean }> = [\n {\n key: 'overdue',\n label: 'Before this month',\n cellOf: (row) => row.overdue,\n total: forecast.overdue,\n muted: true,\n },\n ...forecast.buckets.map((bucket, index) => ({\n key: bucket.key,\n label: monthLabel(bucket.start),\n cellOf: (row: Aglyn.PipelineForecast) => row.months[index],\n total: forecast.months[index],\n })),\n {\n key: 'later',\n label: 'Later',\n cellOf: (row) => row.later,\n total: forecast.later,\n muted: true,\n },\n {\n key: 'undated',\n label: 'No expected close',\n cellOf: (row) => row.undated,\n total: forecast.undated,\n muted: true,\n },\n ].filter((row) => row.key !== 'overdue' && row.key !== 'later' ? true : row.total.count > 0)\n\n const cell = (value: Aglyn.ForecastCell, code: string) =>\n value.count === 0 ? (\n <Typography variant=\"body2\" color=\"text.disabled\">\n {'—'}\n </Typography>\n ) : (\n <Stack sx={{ lineHeight: 1.2 }}>\n <Typography variant=\"body2\">{money(value.amountCents, code)}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${money(value.weightedCents, code)} weighted · ${plural(value.count, 'deal')}`}\n </Typography>\n </Stack>\n )\n\n return (\n <CardDisplay\n header={'Forecast by close month'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#forecast-by-close-month',\n excerpt:\n 'Every open deal by the month it is expected to close, for the ' +\n 'next six months and per pipeline, at face value and weighted by ' +\n 'its stage. Deals with no expected close are their own row.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1.5}>\n {failed ? (\n <Typography variant=\"body2\" color=\"warning.main\">\n {'The forecast could not be read.'}\n </Typography>\n ) : !read ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ) : forecast.total.count === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No open deals yet.'}\n </Typography>\n ) : (\n <Box>\n <ScrollTable size=\"small\" aria-label=\"Forecast by close month\">\n <TableHead>\n <TableRow>\n <TableCell>{'Expected close'}</TableCell>\n {columns.map((column) => (\n <TableCell key={column.pipelineId} align=\"right\">\n {column.name || 'Pipeline not visible'}\n </TableCell>\n ))}\n {manyColumns ? <TableCell align=\"right\">{'All pipelines'}</TableCell> : null}\n </TableRow>\n </TableHead>\n <TableBody>\n {rows.map((row) => (\n <TableRow key={row.key} data-forecast-row={row.key}>\n <TableCell>\n <Typography variant=\"body2\" color={row.muted ? 'text.secondary' : undefined}>\n {row.label}\n </Typography>\n </TableCell>\n {columns.map((column) => (\n <TableCell key={column.pipelineId} align=\"right\">\n {cell(row.cellOf(column), currency.currency)}\n </TableCell>\n ))}\n {manyColumns ? (\n <TableCell align=\"right\">{cell(row.total, currency.currency)}</TableCell>\n ) : null}\n </TableRow>\n ))}\n <TableRow>\n <TableCell>\n <Typography variant=\"subtitle2\">{'Open pipeline'}</Typography>\n </TableCell>\n {columns.map((column) => (\n <TableCell key={column.pipelineId} align=\"right\">\n {cell(column.total, currency.currency)}\n </TableCell>\n ))}\n {manyColumns ? (\n <TableCell align=\"right\">{cell(forecast.total, currency.currency)}</TableCell>\n ) : null}\n </TableRow>\n </TableBody>\n </ScrollTable>\n </Box>\n )}\n {read && currency.mixed ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Deals are in more than one currency; the figures add them as numbers and show ${currency.currency.toUpperCase()}, the most common.`}\n </Typography>\n ) : null}\n {read && dealWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`From the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.`}\n </Typography>\n ) : null}\n {read && pipelineWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Only the first ${PIPELINE_CEILING} pipelines are named.`}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nForecastCard.displayName = 'ForecastCard'\n\nexport default ForecastCard\n"],"names":["Aglyn","money","CardDisplay","ScrollTable","Box","Stack","TableBody","TableCell","TableHead","TableRow","Typography","limit","orderBy","query","where","useMemo","useFirestore","collectionCeiling","plural","reportCacheKey","scopedCollection","visibleToClause","useWindowRead","OPEN_DEAL_CEILING","PIPELINE_CEILING","monthLabel","startMs","Date","toLocaleDateString","undefined","month","year","ForecastCard","props","report","scope","tokens","nowMs","firestore","dealWindow","cacheKey","pipelineWindow","forecast","forecastByCloseMonth","rows","currency","currencyOfDeals","read","status","failed","columns","pipelines","manyColumns","length","key","label","cellOf","row","overdue","total","muted","buckets","map","bucket","index","start","months","later","undated","filter","count","cell","value","code","variant","color","sx","lineHeight","amountCents","weightedCents","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","size","aria-label","column","align","name","pipelineId","data-forecast-row","mixed","toUpperCase","truncated","toLocaleString","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/forecast-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 { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Box,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport { limit, orderBy, query, where } 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 { plural } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { useWindowRead } from './use-aggregate-read'\n\n/**\n * The same window the pipeline card reads, under the SAME cache key and\n * the same dependencies, so the two cards cost one read between them —\n * `useAggregateRead` joins a read already in the air for the key. The\n * numbers must match the pipeline card's query exactly or the key would\n * name two different questions.\n */\nconst OPEN_DEAL_CEILING = 1000\nconst PIPELINE_CEILING = 20\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\ntype PipelineRow = Aglyn.CrmPipeline & { $id: string }\n\nexport interface ForecastCardProps {\n report: CrmReportScope\n}\n\n/** `Sep 2026` — the month a column is. */\nfunction monthLabel(startMs: number): string {\n return new Date(startMs).toLocaleDateString(undefined, { month: 'short', year: 'numeric' })\n}\n\n/**\n * Forecast by close month (AGL-2620): the open pipeline laid out by the\n * month each deal is expected to close, for the next six, per pipeline,\n * at face value and weighted by the stage.\n *\n * Not period-scoped, like the pipeline card and for the same reason: the\n * forecast is what is open NOW, looked at forward. Deals with no expected\n * close are their own row rather than a footnote — that row is the size\n * of the pipeline nobody has scheduled, which is the number the picker\n * of a close date most needs to see — and deals dated before this month\n * (overdue) or past the sixth (later) are rows too, so the column adds up\n * to the pipeline.\n */\nexport function ForecastCard(props: ForecastCardProps) {\n const { report } = props\n const { scope, tokens, nowMs } = report\n const firestore = useFirestore()\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(scopedCollection(firestore, scope, 'pipelines'), ...visibleToClause(tokens)),\n PIPELINE_CEILING,\n ),\n PIPELINE_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:pipelines') },\n )\n\n const forecast = useMemo(\n () => Aglyn.forecastByCloseMonth(dealWindow.rows, pipelineWindow.rows, nowMs),\n [dealWindow.rows, pipelineWindow.rows, nowMs],\n )\n const currency = useMemo(() => Aglyn.currencyOfDeals(dealWindow.rows), [dealWindow.rows])\n const read = dealWindow.status === 'success' && pipelineWindow.status === 'success'\n const failed = dealWindow.status === 'error' || pipelineWindow.status === 'error'\n\n // A pipeline with nothing open is still a column; an unknown pipeline id\n // is a column named for what it is.\n const columns = forecast.pipelines\n const manyColumns = columns.length > 1\n\n const rows: Array<{ key: string; label: string; cellOf: (row: Aglyn.PipelineForecast) => Aglyn.ForecastCell; total: Aglyn.ForecastCell; muted?: boolean }> = [\n {\n key: 'overdue',\n label: 'Before this month',\n cellOf: (row) => row.overdue,\n total: forecast.overdue,\n muted: true,\n },\n ...forecast.buckets.map((bucket, index) => ({\n key: bucket.key,\n label: monthLabel(bucket.start),\n cellOf: (row: Aglyn.PipelineForecast) => row.months[index],\n total: forecast.months[index],\n })),\n {\n key: 'later',\n label: 'Later',\n cellOf: (row) => row.later,\n total: forecast.later,\n muted: true,\n },\n {\n key: 'undated',\n label: 'No expected close',\n cellOf: (row) => row.undated,\n total: forecast.undated,\n muted: true,\n },\n ].filter((row) => row.key !== 'overdue' && row.key !== 'later' ? true : row.total.count > 0)\n\n const cell = (value: Aglyn.ForecastCell, code: string) =>\n value.count === 0 ? (\n <Typography variant=\"body2\" color=\"text.disabled\">\n {'—'}\n </Typography>\n ) : (\n <Stack sx={{ lineHeight: 1.2 }}>\n <Typography variant=\"body2\">{money(value.amountCents, code)}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${money(value.weightedCents, code)} weighted · ${plural(value.count, 'deal')}`}\n </Typography>\n </Stack>\n )\n\n return (\n <CardDisplay\n header={'Forecast by close month'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#forecast-by-close-month',\n excerpt:\n 'Every open deal by the month it is expected to close, for the ' +\n 'next six months and per pipeline, at face value and weighted by ' +\n 'its stage. Deals with no expected close are their own row.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1.5}>\n {failed ? (\n <Typography variant=\"body2\" color=\"warning.main\">\n {'The forecast could not be read.'}\n </Typography>\n ) : !read ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ) : forecast.total.count === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No open deals yet.'}\n </Typography>\n ) : (\n <Box>\n <ScrollTable size=\"small\" aria-label=\"Forecast by close month\">\n <TableHead>\n <TableRow>\n <TableCell>{'Expected close'}</TableCell>\n {columns.map((column) => (\n <TableCell key={column.pipelineId} align=\"right\">\n {column.name || 'Pipeline not visible'}\n </TableCell>\n ))}\n {manyColumns ? <TableCell align=\"right\">{'All pipelines'}</TableCell> : null}\n </TableRow>\n </TableHead>\n <TableBody>\n {rows.map((row) => (\n <TableRow key={row.key} data-forecast-row={row.key}>\n <TableCell>\n <Typography variant=\"body2\" color={row.muted ? 'text.secondary' : undefined}>\n {row.label}\n </Typography>\n </TableCell>\n {columns.map((column) => (\n <TableCell key={column.pipelineId} align=\"right\">\n {cell(row.cellOf(column), currency.currency)}\n </TableCell>\n ))}\n {manyColumns ? (\n <TableCell align=\"right\">{cell(row.total, currency.currency)}</TableCell>\n ) : null}\n </TableRow>\n ))}\n <TableRow>\n <TableCell>\n <Typography variant=\"subtitle2\">{'Open pipeline'}</Typography>\n </TableCell>\n {columns.map((column) => (\n <TableCell key={column.pipelineId} align=\"right\">\n {cell(column.total, currency.currency)}\n </TableCell>\n ))}\n {manyColumns ? (\n <TableCell align=\"right\">{cell(forecast.total, currency.currency)}</TableCell>\n ) : null}\n </TableRow>\n </TableBody>\n </ScrollTable>\n </Box>\n )}\n {read && currency.mixed ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Deals are in more than one currency; the figures add them as numbers and show ${currency.currency.toUpperCase()}, the most common.`}\n </Typography>\n ) : null}\n {read && dealWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`From the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.`}\n </Typography>\n ) : null}\n {read && pipelineWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Only the first ${PIPELINE_CEILING} pipelines are named.`}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nForecastCard.displayName = 'ForecastCard'\n\nexport default ForecastCard\n"],"names":["Aglyn","money","CardDisplay","ScrollTable","Box","Stack","TableBody","TableCell","TableHead","TableRow","Typography","limit","orderBy","query","where","useMemo","useFirestore","collectionCeiling","plural","reportCacheKey","scopedCollection","visibleToClause","useWindowRead","OPEN_DEAL_CEILING","PIPELINE_CEILING","monthLabel","startMs","Date","toLocaleDateString","undefined","month","year","ForecastCard","props","report","scope","tokens","nowMs","firestore","dealWindow","cacheKey","pipelineWindow","forecast","forecastByCloseMonth","rows","currency","currencyOfDeals","read","status","failed","columns","pipelines","manyColumns","length","key","label","cellOf","row","overdue","total","muted","buckets","map","bucket","index","start","months","later","undated","filter","count","cell","value","code","variant","color","sx","lineHeight","amountCents","weightedCents","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","size","aria-label","column","align","name","pipelineId","data-forecast-row","mixed","toUpperCase","truncated","toLocaleString","displayName"],"mappings":"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"}
@@ -16,7 +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 { money } from "@aglyn/shared-ui-email-campaigns/components/report-figures";
19
+ import { money } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
20
20
  import { AppLink, CardDisplay } from "@aglyn/shared-ui-jsx";
21
21
  import { Section } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
22
22
  import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
@@ -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-email-campaigns/components/report-figures'\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'\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: Aglyn.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: Aglyn.pipelineTotals(orphans, null),\n weightedCents: pipelines.reduce(\n (total, entry) => total + entry.totals.weightedCents,\n 0,\n ),\n top,\n stageName,\n currency: Aglyn.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 >\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 <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}\n caption={\n dealWindow.truncated\n ? `Ranked within the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.`\n : undefined\n }\n />\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","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","pipelineTotals","known","Set","orphans","stageName","dealStageById","find","stageId","name","top","sort","a","b","slice","orphanTotals","weightedCents","reduce","total","entry","currency","currencyOfDeals","stuck","stuckDeals","dealsRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","direction","sx","flexWrap","label","value","toLocaleString","note","href","section","truncated","riseIsGood","color","undefined","severity","mixed","variant","toUpperCase","length","title","stages","row","stage","id","display","unplaced","emptyText","size","align","expectedCloseAtMs","String","filename","columns","Date","toISOString","toFixed","disabled","caption","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 { 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'\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: Aglyn.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: Aglyn.pipelineTotals(orphans, null),\n weightedCents: pipelines.reduce(\n (total, entry) => total + entry.totals.weightedCents,\n 0,\n ),\n top,\n stageName,\n currency: Aglyn.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 >\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 <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}\n caption={\n dealWindow.truncated\n ? `Ranked within the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.`\n : undefined\n }\n />\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","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","pipelineTotals","known","Set","orphans","stageName","dealStageById","find","stageId","name","top","sort","a","b","slice","orphanTotals","weightedCents","reduce","total","entry","currency","currencyOfDeals","stuck","stuckDeals","dealsRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","direction","sx","flexWrap","label","value","toLocaleString","note","href","section","truncated","riseIsGood","color","undefined","severity","mixed","variant","toUpperCase","length","title","stages","row","stage","id","display","unplaced","emptyText","size","align","expectedCloseAtMs","String","filename","columns","Date","toISOString","toFixed","disabled","caption","displayName"],"mappings":"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"}
@@ -16,7 +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 { money } from "@aglyn/shared-ui-email-campaigns/components/report-figures";
19
+ import { money } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
20
20
  import { CardDisplay } from "@aglyn/shared-ui-jsx";
21
21
  import { percent } from "@aglyn/shared-ui-jsx/components/measured-figures.component";
22
22
  import { Alert, Stack, Typography } from "@mui/material";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/won-lost-by-owner-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-email-campaigns/components/report-figures'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { percent } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport { limit, orderBy, query, where } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useOrgMemberOptions } from '../../hooks/use-org-member-options'\nimport { ReportBreakdown } from './report-breakdown'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { useWindowRead } from './use-aggregate-read'\n\n/**\n * The same ceiling the Won and lost card reads its chart under, spelled\n * from one constant so the two cards share the read rather than each\n * paying for its own.\n */\nconst CLOSED_DEAL_CEILING = 500\n\n/** How many owners the breakdown draws before folding the rest into one row. */\nconst OWNER_ROW_CEILING = 8\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\n\nexport interface WonLostByOwnerCardProps {\n report: CrmReportScope\n}\n\n/**\n * Who closed what (AGL-2662): the period's wins and losses, by the person\n * who owned the deal at the close.\n *\n * ## It costs no read\n *\n * The two windows are the SAME queries the Won and lost card builds, under\n * the same cache keys — `closed:won` and `closed:lost` — so\n * `useAggregateRead` either answers this card from the remembered window or\n * joins the other card's in-flight promise. Two cards, one pair of reads.\n * A key of its own here would have doubled the most expensive read on the\n * page to ask a different question of the same rows.\n *\n * ## Why the rows can under-report a very busy period\n *\n * The window is bounded, and the bound is on the deals rather than on the\n * owners — so a period with more than {@link CLOSED_DEAL_CEILING} closes of\n * an outcome is tallied over the most recent of them, and the caption says\n * so. A server-side group-by would need one aggregate per owner, and the\n * owner list is not known before the read.\n */\nexport function WonLostByOwnerCard(props: WonLostByOwnerCardProps) {\n const { report } = props\n const { scope, tokens, range } = report\n const firestore = useFirestore()\n // The roster resolves a uid into the name the team reads; a uid it no\n // longer carries — somebody who has left — is named by its own id rather\n // than dropped, because their closed deals still happened.\n const roster = useOrgMemberOptions(scope[1])\n\n const closedInPeriod = (status: Aglyn.CrmDealStatus) =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', status),\n where('closedAtMs', '>=', range.from),\n where('closedAtMs', '<', range.to),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n )\n\n const wonWindow = useWindowRead<DealRow>(\n () => closedInPeriod('won'),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:won') },\n )\n const lostWindow = useWindowRead<DealRow>(\n () => closedInPeriod('lost'),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:lost') },\n )\n\n const owners = useMemo(\n () => Aglyn.wonLostByOwner(wonWindow.rows, lostWindow.rows),\n [wonWindow.rows, lostWindow.rows],\n )\n const currency = useMemo(\n () => Aglyn.currencyOfDeals(wonWindow.rows).currency,\n [wonWindow.rows],\n )\n\n const read = wonWindow.status === 'success' && lostWindow.status === 'success'\n const failed = wonWindow.status === 'error' || lostWindow.status === 'error'\n\n const rows = useMemo(() => {\n const named = (uid: string) =>\n uid\n ? (Aglyn.findOrgMember(roster.options, uid)?.label ?? uid)\n : 'Unassigned'\n const shown = owners.slice(0, OWNER_ROW_CEILING)\n const folded = owners.slice(OWNER_ROW_CEILING)\n const rest = folded.reduce(\n (total, owner) => ({\n won: total.won + owner.won,\n lost: total.lost + owner.lost,\n wonAmountCents: total.wonAmountCents + owner.wonAmountCents,\n }),\n { won: 0, lost: 0, wonAmountCents: 0 },\n )\n return [\n ...shown.map((owner) => ({\n key: owner.uid || 'unassigned',\n label: named(owner.uid),\n value: owner.won,\n display: `${owner.won.toLocaleString()} won`,\n note:\n `${owner.lost.toLocaleString()} lost · ` +\n `${owner.winRate === null ? 'no rate' : `${percent(owner.winRate)} win rate`} · ` +\n `${money(owner.wonAmountCents, currency)}`,\n color: 'success.main',\n })),\n ...(folded.length\n ? [\n {\n key: '$rest',\n label: `${folded.length} more`,\n value: rest.won,\n display: `${rest.won.toLocaleString()} won`,\n note: `${rest.lost.toLocaleString()} lost · ${money(rest.wonAmountCents, currency)}`,\n color: 'success.main',\n },\n ]\n : []),\n ]\n }, [owners, roster.options, currency])\n\n return (\n <CardDisplay\n header={'Won and lost by owner'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#won-and-lost-by-owner',\n excerpt:\n 'Closed deals grouped by the person who owned them at the close, ' +\n 'ranked by how much closed rather than by win rate, with the deals ' +\n 'nobody owned kept as a row of their own.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1.5}>\n {failed ? (\n <Alert severity=\"warning\">\n {'The closed deals could not be read, so this is not an empty period.'}\n </Alert>\n ) : null}\n <ReportBreakdown\n rows={rows}\n emptyText={read ? 'Nothing closed in this period.' : 'Reading…'}\n />\n {wonWindow.truncated || lostWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Counted over the ${CLOSED_DEAL_CEILING} most recently closed of each outcome; this period held more.`}\n </Typography>\n ) : null}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Credited to whoever owned the deal when it closed, not to whoever created it.'}\n </Typography>\n </Stack>\n </CardDisplay>\n )\n}\nWonLostByOwnerCard.displayName = 'WonLostByOwnerCard'\n\nexport default WonLostByOwnerCard\n"],"names":["Aglyn","money","CardDisplay","percent","Alert","Stack","Typography","limit","orderBy","query","where","useMemo","useFirestore","useOrgMemberOptions","ReportBreakdown","reportCacheKey","scopedCollection","visibleToClause","useWindowRead","CLOSED_DEAL_CEILING","OWNER_ROW_CEILING","WonLostByOwnerCard","props","report","scope","tokens","range","firestore","roster","closedInPeriod","status","from","to","wonWindow","cacheKey","lostWindow","owners","wonLostByOwner","rows","currency","currencyOfDeals","read","failed","named","uid","findOrgMember","options","label","shown","slice","folded","rest","reduce","total","owner","won","lost","wonAmountCents","map","key","value","display","toLocaleString","note","winRate","color","length","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","severity","emptyText","truncated","variant","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/won-lost-by-owner-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 { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { percent } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport { limit, orderBy, query, where } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useOrgMemberOptions } from '../../hooks/use-org-member-options'\nimport { ReportBreakdown } from './report-breakdown'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { useWindowRead } from './use-aggregate-read'\n\n/**\n * The same ceiling the Won and lost card reads its chart under, spelled\n * from one constant so the two cards share the read rather than each\n * paying for its own.\n */\nconst CLOSED_DEAL_CEILING = 500\n\n/** How many owners the breakdown draws before folding the rest into one row. */\nconst OWNER_ROW_CEILING = 8\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\n\nexport interface WonLostByOwnerCardProps {\n report: CrmReportScope\n}\n\n/**\n * Who closed what (AGL-2662): the period's wins and losses, by the person\n * who owned the deal at the close.\n *\n * ## It costs no read\n *\n * The two windows are the SAME queries the Won and lost card builds, under\n * the same cache keys — `closed:won` and `closed:lost` — so\n * `useAggregateRead` either answers this card from the remembered window or\n * joins the other card's in-flight promise. Two cards, one pair of reads.\n * A key of its own here would have doubled the most expensive read on the\n * page to ask a different question of the same rows.\n *\n * ## Why the rows can under-report a very busy period\n *\n * The window is bounded, and the bound is on the deals rather than on the\n * owners — so a period with more than {@link CLOSED_DEAL_CEILING} closes of\n * an outcome is tallied over the most recent of them, and the caption says\n * so. A server-side group-by would need one aggregate per owner, and the\n * owner list is not known before the read.\n */\nexport function WonLostByOwnerCard(props: WonLostByOwnerCardProps) {\n const { report } = props\n const { scope, tokens, range } = report\n const firestore = useFirestore()\n // The roster resolves a uid into the name the team reads; a uid it no\n // longer carries — somebody who has left — is named by its own id rather\n // than dropped, because their closed deals still happened.\n const roster = useOrgMemberOptions(scope[1])\n\n const closedInPeriod = (status: Aglyn.CrmDealStatus) =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', status),\n where('closedAtMs', '>=', range.from),\n where('closedAtMs', '<', range.to),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n )\n\n const wonWindow = useWindowRead<DealRow>(\n () => closedInPeriod('won'),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:won') },\n )\n const lostWindow = useWindowRead<DealRow>(\n () => closedInPeriod('lost'),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:lost') },\n )\n\n const owners = useMemo(\n () => Aglyn.wonLostByOwner(wonWindow.rows, lostWindow.rows),\n [wonWindow.rows, lostWindow.rows],\n )\n const currency = useMemo(\n () => Aglyn.currencyOfDeals(wonWindow.rows).currency,\n [wonWindow.rows],\n )\n\n const read = wonWindow.status === 'success' && lostWindow.status === 'success'\n const failed = wonWindow.status === 'error' || lostWindow.status === 'error'\n\n const rows = useMemo(() => {\n const named = (uid: string) =>\n uid\n ? (Aglyn.findOrgMember(roster.options, uid)?.label ?? uid)\n : 'Unassigned'\n const shown = owners.slice(0, OWNER_ROW_CEILING)\n const folded = owners.slice(OWNER_ROW_CEILING)\n const rest = folded.reduce(\n (total, owner) => ({\n won: total.won + owner.won,\n lost: total.lost + owner.lost,\n wonAmountCents: total.wonAmountCents + owner.wonAmountCents,\n }),\n { won: 0, lost: 0, wonAmountCents: 0 },\n )\n return [\n ...shown.map((owner) => ({\n key: owner.uid || 'unassigned',\n label: named(owner.uid),\n value: owner.won,\n display: `${owner.won.toLocaleString()} won`,\n note:\n `${owner.lost.toLocaleString()} lost · ` +\n `${owner.winRate === null ? 'no rate' : `${percent(owner.winRate)} win rate`} · ` +\n `${money(owner.wonAmountCents, currency)}`,\n color: 'success.main',\n })),\n ...(folded.length\n ? [\n {\n key: '$rest',\n label: `${folded.length} more`,\n value: rest.won,\n display: `${rest.won.toLocaleString()} won`,\n note: `${rest.lost.toLocaleString()} lost · ${money(rest.wonAmountCents, currency)}`,\n color: 'success.main',\n },\n ]\n : []),\n ]\n }, [owners, roster.options, currency])\n\n return (\n <CardDisplay\n header={'Won and lost by owner'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#won-and-lost-by-owner',\n excerpt:\n 'Closed deals grouped by the person who owned them at the close, ' +\n 'ranked by how much closed rather than by win rate, with the deals ' +\n 'nobody owned kept as a row of their own.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1.5}>\n {failed ? (\n <Alert severity=\"warning\">\n {'The closed deals could not be read, so this is not an empty period.'}\n </Alert>\n ) : null}\n <ReportBreakdown\n rows={rows}\n emptyText={read ? 'Nothing closed in this period.' : 'Reading…'}\n />\n {wonWindow.truncated || lostWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Counted over the ${CLOSED_DEAL_CEILING} most recently closed of each outcome; this period held more.`}\n </Typography>\n ) : null}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Credited to whoever owned the deal when it closed, not to whoever created it.'}\n </Typography>\n </Stack>\n </CardDisplay>\n )\n}\nWonLostByOwnerCard.displayName = 'WonLostByOwnerCard'\n\nexport default WonLostByOwnerCard\n"],"names":["Aglyn","money","CardDisplay","percent","Alert","Stack","Typography","limit","orderBy","query","where","useMemo","useFirestore","useOrgMemberOptions","ReportBreakdown","reportCacheKey","scopedCollection","visibleToClause","useWindowRead","CLOSED_DEAL_CEILING","OWNER_ROW_CEILING","WonLostByOwnerCard","props","report","scope","tokens","range","firestore","roster","closedInPeriod","status","from","to","wonWindow","cacheKey","lostWindow","owners","wonLostByOwner","rows","currency","currencyOfDeals","read","failed","named","uid","findOrgMember","options","label","shown","slice","folded","rest","reduce","total","owner","won","lost","wonAmountCents","map","key","value","display","toLocaleString","note","winRate","color","length","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","severity","emptyText","truncated","variant","displayName"],"mappings":"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"}