@aglyn/plugins-crm 1.0.0-beta.219 → 1.0.0-beta.221

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.
Files changed (136) hide show
  1. package/README.md +2 -0
  2. package/package.json +14 -15
  3. package/src/lib/components/company-import-drawer.d.ts +1 -1
  4. package/src/lib/components/contact-associations-card.js +4 -3
  5. package/src/lib/components/contact-associations-card.js.map +1 -1
  6. package/src/lib/components/crm-booking-zone.d.ts +8 -2
  7. package/src/lib/components/crm-booking-zone.js.map +1 -1
  8. package/src/lib/components/crm-glance-card.js +2 -1
  9. package/src/lib/components/crm-glance-card.js.map +1 -1
  10. package/src/lib/components/deal-products-card.js +27 -16
  11. package/src/lib/components/deal-products-card.js.map +1 -1
  12. package/src/lib/components/lead-campaigns-card.d.ts +4 -4
  13. package/src/lib/components/lead-campaigns-card.js +9 -8
  14. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  15. package/src/lib/components/lead-properties-card.js +4 -0
  16. package/src/lib/components/lead-properties-card.js.map +1 -1
  17. package/src/lib/components/lead-status-chip.js +3 -1
  18. package/src/lib/components/lead-status-chip.js.map +1 -1
  19. package/src/lib/components/leads-bulk-bar.js +7 -9
  20. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  21. package/src/lib/components/leads-section.js +5 -1
  22. package/src/lib/components/leads-section.js.map +1 -1
  23. package/src/lib/components/new-lead-drawer.js +5 -4
  24. package/src/lib/components/new-lead-drawer.js.map +1 -1
  25. package/src/lib/components/reports/activity-card.js +4 -3
  26. package/src/lib/components/reports/activity-card.js.map +1 -1
  27. package/src/lib/components/reports/closed-deals-card.js +4 -3
  28. package/src/lib/components/reports/closed-deals-card.js.map +1 -1
  29. package/src/lib/components/reports/contacts-mix-card.js +3 -2
  30. package/src/lib/components/reports/contacts-mix-card.js.map +1 -1
  31. package/src/lib/components/reports/contacts-trend-card.js +2 -1
  32. package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
  33. package/src/lib/components/reports/forecast-card.js +3 -2
  34. package/src/lib/components/reports/forecast-card.js.map +1 -1
  35. package/src/lib/components/reports/lead-funnel-card.js +8 -6
  36. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  37. package/src/lib/components/reports/pipeline-card.js +4 -3
  38. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  39. package/src/lib/components/reports/report-format.d.ts +1 -1
  40. package/src/lib/components/reports/report-format.js.map +1 -1
  41. package/src/lib/components/reports/report-scope.d.ts +1 -1
  42. package/src/lib/components/reports/report-scope.js.map +1 -1
  43. package/src/lib/components/reports/source-conversion-card.js +2 -1
  44. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  45. package/src/lib/components/reports/tasks-card.js +2 -1
  46. package/src/lib/components/reports/tasks-card.js.map +1 -1
  47. package/src/lib/components/reports/won-lost-by-owner-card.js +3 -2
  48. package/src/lib/components/reports/won-lost-by-owner-card.js.map +1 -1
  49. package/src/lib/components/reports-section.js +4 -4
  50. package/src/lib/components/reports-section.js.map +1 -1
  51. package/src/lib/constants/bundle-common.d.ts +2 -0
  52. package/src/lib/constants/bundle-common.js +1 -0
  53. package/src/lib/constants/bundle-common.js.map +1 -1
  54. package/src/lib/declarations.console-server.d.ts +4 -0
  55. package/src/lib/declarations.console-server.js +12 -0
  56. package/src/lib/declarations.console-server.js.map +1 -1
  57. package/src/lib/declarations.server.d.ts +6 -0
  58. package/src/lib/declarations.server.js +51 -1
  59. package/src/lib/declarations.server.js.map +1 -1
  60. package/src/lib/hooks/use-crm-campaigns.d.ts +2 -2
  61. package/src/lib/hooks/use-crm-campaigns.js +3 -3
  62. package/src/lib/hooks/use-crm-campaigns.js.map +1 -1
  63. package/src/lib/model/campaign-filing-activity.d.ts +1 -1
  64. package/src/lib/model/campaign-filing-activity.js +1 -1
  65. package/src/lib/model/campaign-filing-activity.js.map +1 -1
  66. package/src/lib/model/contact-record.js +1 -1
  67. package/src/lib/model/contact-record.js.map +1 -1
  68. package/src/lib/model/crm-digest.d.ts +45 -3
  69. package/src/lib/model/crm-digest.js +75 -11
  70. package/src/lib/model/crm-digest.js.map +1 -1
  71. package/src/lib/model/crm-inbound.d.ts +162 -0
  72. package/src/lib/model/crm-inbound.js +208 -0
  73. package/src/lib/model/crm-inbound.js.map +1 -0
  74. package/src/lib/model/crm-lead-import.d.ts +1 -1
  75. package/src/lib/model/crm-lead-import.js +4 -4
  76. package/src/lib/model/crm-lead-import.js.map +1 -1
  77. package/src/lib/model/crm-reports.d.ts +467 -0
  78. package/src/lib/model/crm-reports.js +667 -0
  79. package/src/lib/model/crm-reports.js.map +1 -0
  80. package/src/lib/model/lead-filters.d.ts +4 -3
  81. package/src/lib/model/lead-filters.js +7 -5
  82. package/src/lib/model/lead-filters.js.map +1 -1
  83. package/src/lib/plugin.js +36 -0
  84. package/src/lib/plugin.js.map +1 -1
  85. package/src/lib/server/api-v1/crm-leads.js +1 -0
  86. package/src/lib/server/api-v1/crm-leads.js.map +1 -1
  87. package/src/lib/server/api-v1/openapi.js +1 -1
  88. package/src/lib/server/api-v1/openapi.js.map +1 -1
  89. package/src/lib/server/campaign-filing-activity.d.ts +1 -1
  90. package/src/lib/server/campaign-filing-activity.js +7 -11
  91. package/src/lib/server/campaign-filing-activity.js.map +1 -1
  92. package/src/lib/server/capture-contact.js +19 -5
  93. package/src/lib/server/capture-contact.js.map +1 -1
  94. package/src/lib/server/contact-email-engagement.d.ts +33 -0
  95. package/src/lib/server/contact-email-engagement.js +146 -0
  96. package/src/lib/server/contact-email-engagement.js.map +1 -0
  97. package/src/lib/server/contact-update.js +3 -3
  98. package/src/lib/server/contact-update.js.map +1 -1
  99. package/src/lib/server/container-detach.d.ts +31 -0
  100. package/src/lib/server/container-detach.js +115 -0
  101. package/src/lib/server/container-detach.js.map +1 -0
  102. package/src/lib/server/crm-inbound-email.d.ts +162 -0
  103. package/src/lib/server/crm-inbound-email.js +259 -0
  104. package/src/lib/server/crm-inbound-email.js.map +1 -0
  105. package/src/lib/server/crm-records-meter.d.ts +50 -0
  106. package/src/lib/server/crm-records-meter.js +126 -0
  107. package/src/lib/server/crm-records-meter.js.map +1 -0
  108. package/src/lib/server/daily-digest-route.d.ts +2 -8
  109. package/src/lib/server/daily-digest-route.js +15 -25
  110. package/src/lib/server/daily-digest-route.js.map +1 -1
  111. package/src/lib/server/inbound-address.js +5 -3
  112. package/src/lib/server/inbound-address.js.map +1 -1
  113. package/src/lib/server/inbound-route.js +7 -4
  114. package/src/lib/server/inbound-route.js.map +1 -1
  115. package/src/lib/server/lead-campaign-carry.js +3 -3
  116. package/src/lib/server/lead-campaign-carry.js.map +1 -1
  117. package/src/lib/server/lead-create.d.ts +1 -1
  118. package/src/lib/server/lead-create.js +10 -15
  119. package/src/lib/server/lead-create.js.map +1 -1
  120. package/src/lib/server/lead-nurturing.d.ts +23 -0
  121. package/src/lib/server/lead-nurturing.js +82 -0
  122. package/src/lib/server/lead-nurturing.js.map +1 -0
  123. package/src/lib/server/leads-import.js +7 -7
  124. package/src/lib/server/leads-import.js.map +1 -1
  125. package/src/lib/server/record-email-state.js +23 -0
  126. package/src/lib/server/record-email-state.js.map +1 -1
  127. package/src/lib/server/record-timeline.d.ts +12 -0
  128. package/src/lib/server/record-timeline.js +127 -15
  129. package/src/lib/server/record-timeline.js.map +1 -1
  130. package/src/lib/server/task-reminders-route.js +4 -2
  131. package/src/lib/server/task-reminders-route.js.map +1 -1
  132. package/src/lib/server/task-routes.d.ts +2 -0
  133. package/src/lib/server/task-routes.js +22 -3
  134. package/src/lib/server/task-routes.js.map +1 -1
  135. package/src/lib/usage-axes.js +8 -1
  136. package/src/lib/usage-axes.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/lead-funnel-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 { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport {\n collection,\n type Firestore,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../../hooks/use-crm-org-mount'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename } from './report-format'\nimport { type CrmReportScope, reportCacheKey } from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport {\n type AggregateRead,\n useAggregateRead,\n useWindowRead,\n type WindowRead,\n} from './use-aggregate-read'\n\n/**\n * How many of the period's leads the funnel is placed from — the Leads\n * section's own window, over the period rather than over everything.\n *\n * The captured count is a server aggregate and never meets this bound; the\n * funnel needs each lead's status and reason, so it reads the period's\n * newest two hundred and says when there were more. At the organization\n * level the bound is PER SITE (AGL-2634): each site's window is its own,\n * and a site with a thousand leads must not crowd out a site with ten.\n */\nexport const LEAD_CEILING = 200\n/** How many reasons the bar list draws; the rest are counted beneath it. */\nconst REASONS_SHOWN = 8\n\n/** The CSV's columns: one file for both the statuses and the reasons. */\nconst COLUMNS = ['Kind', 'Label', 'Count', 'Share'] as const\n\ntype LeadRow = Record<string, unknown> & Aglyn.CrmLeadFields & { $id: string }\n\n/** The period's captured count, and the previous period's for the delta. */\ninterface CapturedFigures {\n current: number\n previous: number\n}\n\nexport interface LeadFunnelCardProps {\n report: CrmReportScope\n /**\n * The site whose leads are placed. A lead lives under its host, not the\n * org — `hosts/{hostId}/leads`, private by path, no `visibleTo` — so the\n * report's org scope cannot reach it and the card is told the site. At\n * the organization level `null` (AGL-2630): the card then reads every\n * site the mount lists, one window each (AGL-2634).\n */\n hostId: string | null\n}\n\nconst LEAD_FUNNEL_HELP = Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#lead-funnel',\n excerpt:\n 'Of the leads this site captured in the period, how many are ' +\n 'still new, being worked, qualified or unqualified — and the ' +\n 'reasons the unqualified ones were closed. Placed by when each ' +\n 'lead was first seen.',\n})\n\n/**\n * The leads a scope may see, first seen in a range (AGL-3275).\n *\n * `visibleTo` is the site's tokens under a site, and `null` at the\n * organization level where an org-wide member reads without a clause. The\n * range stays a single-field one; with the scope clause it composes with the\n * `visibleTo` + `firstSeenAtMs` entry in `cloud/firebase-firestore.indexes.json`.\n */\nfunction leadsBetween(\n firestore: Firestore,\n orgId: string,\n visibleTo: readonly string[] | null,\n from: number,\n to: number,\n) {\n return query(\n collection(firestore, 'orgs', orgId, 'leads'),\n ...(visibleTo ? [where('visibleTo', 'array-contains-any', [...visibleTo])] : []),\n where('firstSeenAtMs', '>=', from),\n where('firstSeenAtMs', '<', to),\n )\n}\n\nconst countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n/**\n * The lead funnel, at whichever level the report is mounted (AGL-2624,\n * AGL-2630, AGL-2634).\n *\n * Under a site: that site. At the organization level: every site the\n * mount lists, one window each, totaled — the org hub's reports total\n * every site, and the leads are no exception now that the fan-out is\n * bounded by the org's site list. With no sites to read the card says so\n * instead of guessing.\n */\nexport function LeadFunnelCard(props: LeadFunnelCardProps) {\n const { report, hostId } = props\n const mount = useCrmOrgMount()\n if (hostId) return <SiteLeadFunnelCard report={report} hostId={hostId} />\n if (mount && (!mount.hostsReady || mount.hosts.length > 0)) {\n return <OrgLeadFunnelCard report={report} />\n }\n return (\n <CardDisplay header={'Lead funnel'} help={LEAD_FUNNEL_HELP} contentGutterX contentGutterY>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mount\n ? 'Leads live under a site, and this organization has no sites yet.'\n : 'Leads live under a site, and this report is mounted under none.'}\n </Typography>\n </CardDisplay>\n )\n}\n\n/**\n * The lead funnel (AGL-2624): of the leads this site captured in the\n * period, how many are still new, being worked, qualified or unqualified —\n * and why the unqualified ones were closed.\n *\n * A cohort by first sighting, followed to where each lead stands now,\n * because that is the question a period asks of leads — \"what became of\n * the ones we got\" — and because a lead's status has no history on the\n * document: only its current value. The window is a range on\n * `firstSeenAtMs`, which the capture door stamps once on creation and\n * never again, so a returning visitor's second form is not a second lead\n * in a second period. A single-field range needs no composite index.\n *\n * Read directly off `hosts/{hostId}/leads` under the site's own rules —\n * any member of the site may read it — the way the Leads section reads\n * it, so the funnel and the list can never disagree about who may see a\n * lead.\n */\nfunction SiteLeadFunnelCard(props: { report: CrmReportScope; hostId: string }) {\n const { report, hostId } = props\n const { range } = report\n const firestore = useFirestore()\n\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], report.tokens, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:counts:${hostId}`) },\n )\n const window = useWindowRead<LeadRow>(\n () =>\n query(\n leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n LEAD_CEILING,\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:window:${hostId}`) },\n )\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n (figures !== null ? ` of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'This site’s leads could not be read.'}\n />\n )\n}\nSiteLeadFunnelCard.displayName = 'SiteLeadFunnelCard'\n\n/**\n * THE ORGANIZATION'S FUNNEL (AGL-2634, AGL-3275): every lead the org holds.\n *\n * This totaled one aggregate and one window PER SITE, because the sites'\n * lead collections were disjoint. They share one collection now, so a sum\n * would count a lead that two brands in a consent group both hold once for\n * each of them — and the merge that existed so a busy site could not crowd\n * out a quiet one has nothing left to balance. One aggregate and one\n * bounded read, unscoped, as an org-wide member's reads are.\n */\nfunction OrgLeadFunnelCard(props: { report: CrmReportScope }) {\n const { report } = props\n const { range } = report\n const firestore = useFirestore()\n const mount = useCrmOrgMount()\n const hostIds = useMemo(\n () => (mount?.hostsReady ? mount.hosts.map((host) => host.id) : null),\n [mount],\n )\n const key = hostIds?.join('\\n') ?? ''\n\n /*\n * ONE READ, not one per site (AGL-3275). The sites share a collection now,\n * so a lead two brands in a consent group both hold would be counted once\n * for each of them — and the org's funnel would report more leads than the\n * org has. `null` tokens is the org-wide read that carries no clause.\n */\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], null, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], null, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:counts:org') },\n )\n const merged = useAggregateRead<{ rows: LeadRow[]; truncated: boolean }>(\n () =>\n getDocs(\n query(\n leadsBetween(firestore, report.scope[1], null, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n ).then((snapshot) =>\n ceilingedWindow(\n snapshot.docs.map(\n (document) => ({ ...document.data(), $id: document.id }) as unknown as LeadRow,\n ),\n LEAD_CEILING,\n ),\n ),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:window:org') },\n )\n const window = useMemo<WindowRead<LeadRow>>(\n () => ({\n rows: merged.value?.rows ?? [],\n truncated: merged.value?.truncated ?? false,\n status: merged.status,\n }),\n [merged],\n )\n const sites = hostIds?.length ?? 0\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n subheader={`Every site (${sites.toLocaleString()})`}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n ` — at most ${LEAD_CEILING.toLocaleString()}` +\n (figures !== null ? ` — of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'The organization’s leads could not be read.'}\n />\n )\n}\nOrgLeadFunnelCard.displayName = 'OrgLeadFunnelCard'\n\n/**\n * The funnel as drawn, whichever level fed it: four tiles, the statuses,\n * the reasons, the export. Pure over the two reads.\n */\nfunction LeadFunnelBody(props: {\n report: CrmReportScope\n subheader?: string\n captured: AggregateRead<CapturedFigures>\n window: WindowRead<LeadRow>\n caption?: string\n errorText: string\n}) {\n const { report, subheader, captured, window, caption, errorText } = props\n const { period, routes } = report\n const status = window.status\n\n const funnel = useMemo(() => Aglyn.leadFunnel(window.rows), [window])\n const read = status === 'success'\n const figures = captured.value\n const share = (count: number, of: number): string | null =>\n of > 0 ? percent(count / of) : null\n const reasons = funnel.reasons.slice(0, REASONS_SHOWN)\n const moreReasons = funnel.reasons.length - reasons.length\n\n return (\n <CardDisplay\n header={'Lead funnel'}\n subheader={subheader}\n help={LEAD_FUNNEL_HELP}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <ReportExport\n filename={reportFilename('lead-funnel', period)}\n columns={COLUMNS}\n rows={() => [\n ...Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => [\n 'Status',\n Aglyn.CRM_LEAD_STATUS_LABELS[leadStatus],\n funnel.byStatus[leadStatus],\n share(funnel.byStatus[leadStatus], funnel.total) ?? '',\n ]),\n ...funnel.reasons.map((reason) => [\n 'Unqualified reason',\n reason.label,\n reason.count,\n share(reason.count, funnel.byStatus.unqualified) ?? '',\n ]),\n ]}\n disabled={!read || !funnel.total}\n />\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('leads')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open leads'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Leads captured'}\n value={figures ? figures.current.toLocaleString() : null}\n deltaPct={figures ? Aglyn.deltaPercent(figures.current, figures.previous) : null}\n note={'first seen in the period'}\n href={routes.section('leads')}\n />\n <ReportStatTile\n label={'Qualified'}\n value={read ? funnel.byStatus.qualified.toLocaleString() : null}\n note={\n funnel.qualifiedRate === null\n ? read\n ? 'nobody captured in the period'\n : undefined\n : `${percent(funnel.qualifiedRate)} of those captured`\n }\n />\n <ReportStatTile\n label={'Unqualified'}\n value={read ? funnel.byStatus.unqualified.toLocaleString() : null}\n note={\n funnel.unqualifiedRate === null\n ? undefined\n : `${percent(funnel.unqualifiedRate)} of those captured`\n }\n riseIsGood={false}\n />\n <ReportStatTile\n label={'Still open'}\n value={read ? funnel.open.toLocaleString() : null}\n note={'new or being worked'}\n href={routes.section('leads')}\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{errorText}</Alert>\n ) : null}\n <Section title={'Where they stand'}>\n <ReportBreakdown\n rows={Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => ({\n key: leadStatus,\n label: Aglyn.CRM_LEAD_STATUS_LABELS[leadStatus],\n value: funnel.byStatus[leadStatus],\n note: share(funnel.byStatus[leadStatus], funnel.total) ?? undefined,\n color:\n leadStatus === 'qualified'\n ? 'success.main'\n : leadStatus === 'unqualified'\n ? 'warning.main'\n : undefined,\n }))}\n emptyText={''}\n />\n {read && !funnel.total ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No leads captured in this period.'}\n </Typography>\n ) : null}\n </Section>\n <Section title={'Why leads were unqualified'}>\n <ReportBreakdown\n rows={reasons.map((reason) => ({\n key: reason.key,\n label: reason.label,\n value: reason.count,\n note: share(reason.count, funnel.byStatus.unqualified) ?? undefined,\n color: 'warning.main',\n }))}\n emptyText={\n status === 'loading' ? 'Reading…' : 'No lead was unqualified in this period.'\n }\n />\n {moreReasons > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`And ${plural(moreReasons, 'other reason')}, each given once or twice.`}\n </Typography>\n ) : null}\n </Section>\n {/* What the funnel, and so the export, is placed from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadFunnelBody.displayName = 'LeadFunnelBody'\nLeadFunnelCard.displayName = 'LeadFunnelCard'\n\nexport default LeadFunnelCard\n"],"names":["Aglyn","AppLink","CardDisplay","percent","Section","ceilingedWindow","Alert","Button","Stack","Typography","collection","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","useCrmOrgMount","ReportBreakdown","ReportExport","plural","reportFilename","reportCacheKey","ReportStatTile","useAggregateRead","useWindowRead","LEAD_CEILING","REASONS_SHOWN","COLUMNS","LEAD_FUNNEL_HELP","pluginDocsHelp","anchor","excerpt","leadsBetween","firestore","orgId","visibleTo","from","to","countOf","target","then","snapshot","data","count","LeadFunnelCard","props","report","hostId","mount","SiteLeadFunnelCard","hostsReady","hosts","length","OrgLeadFunnelCard","header","help","contentGutterX","contentGutterY","variant","color","range","captured","Promise","all","scope","tokens","previousFrom","previousTo","current","previous","cacheKey","window","figures","value","sampled","status","truncated","rows","LeadFunnelBody","caption","toLocaleString","undefined","errorText","displayName","hostIds","map","host","id","key","join","merged","docs","document","$id","sites","subheader","period","routes","funnel","leadFunnel","read","share","of","reasons","slice","moreReasons","HeaderProps","action","direction","spacing","filename","columns","CRM_LEAD_STATUSES","leadStatus","CRM_LEAD_STATUS_LABELS","byStatus","total","reason","label","unqualified","disabled","component","componentVariant","nativeButton","href","section","size","sx","flexWrap","deltaPct","deltaPercent","note","qualified","qualifiedRate","unqualifiedRate","riseIsGood","open","severity","title","emptyText"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,YAAYA,WAAW,eAAc;AACrC,SAASC,OAAO,EAAEC,WAAW,QAAQ,uBAAsB;AAC3D,SACEC,OAAO,EACPC,OAAO,QACF,6DAA4D;AACnE,SAASC,eAAe,QAAQ,+DAA8D;AAC9F,SAASC,KAAK,EAAEC,MAAM,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAChE,SACEC,UAAU,EAEVC,kBAAkB,EAClBC,OAAO,EACPC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,KAAK,QACA,qBAAoB;AAC3B,SAASC,OAAO,QAAQ,QAAO;AAC/B,SAASC,YAAY,QAAQ,iCAAgC;AAC7D,SAASC,cAAc,QAAQ,mCAA+B;AAC9D,SAASC,eAAe,QAAQ,wBAAoB;AACpD,SAASC,YAAY,QAAQ,qBAAiB;AAC9C,SAASC,MAAM,EAAEC,cAAc,QAAQ,qBAAiB;AACxD,SAA8BC,cAAc,QAAQ,oBAAgB;AACpE,SAASC,cAAc,QAAQ,wBAAoB;AACnD,SAEEC,gBAAgB,EAChBC,aAAa,QAER,0BAAsB;AAE7B;;;;;;;;;CASC,GACD,OAAO,MAAMC,eAAe,IAAG;AAC/B,0EAA0E,GAC1E,MAAMC,gBAAgB;AAEtB,uEAAuE,GACvE,MAAMC,UAAU;IAAC;IAAQ;IAAS;IAAS;CAAQ;AAsBnD,MAAMC,mBAAmB/B,MAAMgC,cAAc,CAAC,cAAc;IAC1DC,QAAQ;IACRC,SACE,iEACA,iEACA,mEACA;AACJ;AAEA;;;;;;;CAOC,GACD,SAASC,aACPC,SAAoB,EACpBC,KAAa,EACbC,SAAmC,EACnCC,IAAY,EACZC,EAAU;IAEV,OAAOzB,MACLL,WAAW0B,WAAW,QAAQC,OAAO,aACjCC,YAAY;QAACtB,MAAM,aAAa,sBAAsB;eAAIsB;SAAU;KAAE,GAAG,EAAE,EAC/EtB,MAAM,iBAAiB,MAAMuB,OAC7BvB,MAAM,iBAAiB,KAAKwB;AAEhC;AAEA,MAAMC,UAAU,CAACC,SACf/B,mBAAmB+B,QAAQC,IAAI,CAAC,CAACC,WAAaA,SAASC,IAAI,GAAGC,KAAK;AAErE;;;;;;;;;CASC,GACD,OAAO,SAASC,eAAeC,KAA0B;IACvD,MAAM,EAAEC,MAAM,EAAEC,MAAM,EAAE,GAAGF;IAC3B,MAAMG,QAAQhC;IACd,IAAI+B,QAAQ,qBAAO,KAACE;QAAmBH,QAAQA;QAAQC,QAAQA;;IAC/D,IAAIC,SAAU,CAAA,CAACA,MAAME,UAAU,IAAIF,MAAMG,KAAK,CAACC,MAAM,GAAG,CAAA,GAAI;QAC1D,qBAAO,KAACC;YAAkBP,QAAQA;;IACpC;IACA,qBACE,KAAC/C;QAAYuD,QAAQ;QAAeC,MAAM3B;QAAkB4B,cAAc;QAACC,cAAc;kBACvF,cAAA,KAACnD;YAAWoD,SAAQ;YAAQC,OAAM;sBAC/BX,QACG,qEACA;;;AAIZ;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,SAASC,mBAAmBJ,KAAiD;IAC3E,MAAM,EAAEC,MAAM,EAAEC,MAAM,EAAE,GAAGF;IAC3B,MAAM,EAAEe,KAAK,EAAE,GAAGd;IAClB,MAAMb,YAAYlB;IAElB,MAAM8C,WAAWtC,iBACf,IACEuC,QAAQC,GAAG,CAAC;YACVzB,QAAQN,aAAaC,WAAWa,OAAOkB,KAAK,CAAC,EAAE,EAAElB,OAAOmB,MAAM,EAAEL,MAAMxB,IAAI,EAAEwB,MAAMvB,EAAE;YACpFC,QACEN,aAAaC,WAAWa,OAAOkB,KAAK,CAAC,EAAE,EAAElB,OAAOmB,MAAM,EAAEL,MAAMM,YAAY,EAAEN,MAAMO,UAAU;SAE/F,EAAE3B,IAAI,CAAC,CAAC,CAAC4B,SAASC,SAAS,GAAM,CAAA;gBAAED;gBAASC;YAAS,CAAA,IACxD;QAACpC;QAAWc;QAAQa;KAAM,EAC1B;QAAEU,UAAUjD,eAAeyB,QAAQ,CAAC,aAAa,EAAEC,QAAQ;IAAE;IAE/D,MAAMwB,SAAS/C,cACb,IACEZ,MACEoB,aAAaC,WAAWa,OAAOkB,KAAK,CAAC,EAAE,EAAElB,OAAOmB,MAAM,EAAEL,MAAMxB,IAAI,EAAEwB,MAAMvB,EAAE,GAC5E1B,QAAQ,iBAAiB,SACzBD,MAAMe,eAAe,KAEzBA,cACA;QAACQ;QAAWc;QAAQa;KAAM,EAC1B;QAAEU,UAAUjD,eAAeyB,QAAQ,CAAC,aAAa,EAAEC,QAAQ;IAAE;IAE/D,MAAMyB,UAAUX,SAASY,KAAK;IAC9B,MAAMC,UACJH,OAAOI,MAAM,KAAK,aACjBJ,CAAAA,OAAOK,SAAS,IAAKJ,YAAY,QAAQA,QAAQJ,OAAO,GAAGG,OAAOM,IAAI,CAACzB,MAAM;IAChF,qBACE,KAAC0B;QACChC,QAAQA;QACRe,UAAUA;QACVU,QAAQA;QACRQ,SACEL,UACI,CAAC,gBAAgB,EAAEH,OAAOM,IAAI,CAACzB,MAAM,CAAC4B,cAAc,GAAG,6BAA6B,CAAC,GACpFR,CAAAA,YAAY,OAAO,CAAC,IAAI,EAAEA,QAAQJ,OAAO,CAACY,cAAc,IAAI,GAAG,EAAC,IACjE,gEACAC;QAENC,WAAW;;AAGjB;AACAjC,mBAAmBkC,WAAW,GAAG;AAEjC;;;;;;;;;CASC,GACD,SAAS9B,kBAAkBR,KAAiC;;IAC1D,MAAM,EAAEC,MAAM,EAAE,GAAGD;IACnB,MAAM,EAAEe,KAAK,EAAE,GAAGd;IAClB,MAAMb,YAAYlB;IAClB,MAAMiC,QAAQhC;IACd,MAAMoE,UAAUtE,QACd,IAAOkC,CAAAA,yBAAAA,MAAOE,UAAU,IAAGF,MAAMG,KAAK,CAACkC,GAAG,CAAC,CAACC,OAASA,KAAKC,EAAE,IAAI,MAChE;QAACvC;KAAM;IAET,MAAMwC,cAAMJ,2BAAAA,QAASK,IAAI,CAAC,wBAAS;IAEnC;;;;;GAKC,GACD,MAAM5B,WAAWtC,iBACf,IACEuC,QAAQC,GAAG,CAAC;YACVzB,QAAQN,aAAaC,WAAWa,OAAOkB,KAAK,CAAC,EAAE,EAAE,MAAMJ,MAAMxB,IAAI,EAAEwB,MAAMvB,EAAE;YAC3EC,QACEN,aAAaC,WAAWa,OAAOkB,KAAK,CAAC,EAAE,EAAE,MAAMJ,MAAMM,YAAY,EAAEN,MAAMO,UAAU;SAEtF,EAAE3B,IAAI,CAAC,CAAC,CAAC4B,SAASC,SAAS,GAAM,CAAA;gBAAED;gBAASC;YAAS,CAAA,IACxD;QAACpC;QAAWa,OAAOkB,KAAK;QAAEJ;KAAM,EAChC;QAAEU,UAAUjD,eAAeyB,QAAQ;IAAoB;IAEzD,MAAM4C,SAASnE,iBACb,IACEd,QACEG,MACEoB,aAAaC,WAAWa,OAAOkB,KAAK,CAAC,EAAE,EAAE,MAAMJ,MAAMxB,IAAI,EAAEwB,MAAMvB,EAAE,GACnE1B,QAAQ,iBAAiB,SACzBD,MAAMe,eAAe,KAEvBe,IAAI,CAAC,CAACC,WACNvC,gBACEuC,SAASkD,IAAI,CAACN,GAAG,CACf,CAACO,WAAc,aAAKA,SAASlD,IAAI;oBAAImD,KAAKD,SAASL,EAAE;qBAEvD9D,gBAGN;QAACQ;QAAWa,OAAOkB,KAAK;QAAEJ;KAAM,EAChC;QAAEU,UAAUjD,eAAeyB,QAAQ;IAAoB;IAEzD,MAAMyB,SAASzD,QACb;;YACQ4E,eACKA;eAFN;YACLb,IAAI,WAAEa,gBAAAA,OAAOjB,KAAK,qBAAZiB,cAAcb,IAAI,mBAAI,EAAE;YAC9BD,SAAS,YAAEc,iBAAAA,OAAOjB,KAAK,qBAAZiB,eAAcd,SAAS,oBAAI;YACtCD,QAAQe,OAAOf,MAAM;QACvB;OACA;QAACe;KAAO;IAEV,MAAMI,iBAAQV,2BAAAA,QAAShC,MAAM,oBAAI;IACjC,MAAMoB,UAAUX,SAASY,KAAK;IAC9B,MAAMC,UACJH,OAAOI,MAAM,KAAK,aACjBJ,CAAAA,OAAOK,SAAS,IAAKJ,YAAY,QAAQA,QAAQJ,OAAO,GAAGG,OAAOM,IAAI,CAACzB,MAAM;IAChF,qBACE,KAAC0B;QACChC,QAAQA;QACRiD,WAAW,CAAC,YAAY,EAAED,MAAMd,cAAc,GAAG,CAAC,CAAC;QACnDnB,UAAUA;QACVU,QAAQA;QACRQ,SACEL,UACI,CAAC,gBAAgB,EAAEH,OAAOM,IAAI,CAACzB,MAAM,CAAC4B,cAAc,GAAG,6BAA6B,CAAC,GACrF,CAAC,WAAW,EAAEvD,aAAauD,cAAc,IAAI,GAC5CR,CAAAA,YAAY,OAAO,CAAC,MAAM,EAAEA,QAAQJ,OAAO,CAACY,cAAc,IAAI,GAAG,EAAC,IACnE,gEACAC;QAENC,WAAW;;AAGjB;AACA7B,kBAAkB8B,WAAW,GAAG;AAEhC;;;CAGC,GACD,SAASL,eAAejC,KAOvB;IACC,MAAM,EAAEC,MAAM,EAAEiD,SAAS,EAAElC,QAAQ,EAAEU,MAAM,EAAEQ,OAAO,EAAEG,SAAS,EAAE,GAAGrC;IACpE,MAAM,EAAEmD,MAAM,EAAEC,MAAM,EAAE,GAAGnD;IAC3B,MAAM6B,SAASJ,OAAOI,MAAM;IAE5B,MAAMuB,SAASpF,QAAQ,IAAMjB,MAAMsG,UAAU,CAAC5B,OAAOM,IAAI,GAAG;QAACN;KAAO;IACpE,MAAM6B,OAAOzB,WAAW;IACxB,MAAMH,UAAUX,SAASY,KAAK;IAC9B,MAAM4B,QAAQ,CAAC1D,OAAe2D,KAC5BA,KAAK,IAAItG,QAAQ2C,QAAQ2D,MAAM;IACjC,MAAMC,UAAUL,OAAOK,OAAO,CAACC,KAAK,CAAC,GAAG9E;IACxC,MAAM+E,cAAcP,OAAOK,OAAO,CAACnD,MAAM,GAAGmD,QAAQnD,MAAM;IAE1D,qBACE,KAACrD;QACCuD,QAAQ;QACRyC,WAAWA;QACXxC,MAAM3B;QACN4B,cAAc;QACdC,cAAc;QACdiD,aAAa;YACXC,sBACE,MAACtG;gBAAMuG,WAAU;gBAAMC,SAAS;;kCAC9B,KAAC3F;wBACC4F,UAAU1F,eAAe,eAAe4E;wBACxCe,SAASpF;wBACTkD,MAAM,IAAM;mCACPhF,MAAMmH,iBAAiB,CAAC3B,GAAG,CAAC,CAAC4B;wCAI9BZ;2CAJ6C;wCAC7C;wCACAxG,MAAMqH,sBAAsB,CAACD,WAAW;wCACxCf,OAAOiB,QAAQ,CAACF,WAAW;yCAC3BZ,SAAAA,MAAMH,OAAOiB,QAAQ,CAACF,WAAW,EAAEf,OAAOkB,KAAK,aAA/Cf,SAAoD;qCACrD;;mCACEH,OAAOK,OAAO,CAAClB,GAAG,CAAC,CAACgC;wCAIrBhB;2CAJgC;wCAChC;wCACAgB,OAAOC,KAAK;wCACZD,OAAO1E,KAAK;yCACZ0D,SAAAA,MAAMgB,OAAO1E,KAAK,EAAEuD,OAAOiB,QAAQ,CAACI,WAAW,aAA/ClB,SAAoD;qCACrD;;6BACF;wBACDmB,UAAU,CAACpB,QAAQ,CAACF,OAAOkB,KAAK;;kCAElC,KAAChH;wBACCqH,WAAW3H;uBACN;wBAAE4H,kBAAkB;wBAASC,cAAc;oBAAM;wBACtDC,MAAM3B,OAAO4B,OAAO,CAAC;wBACrBC,MAAK;wBACLnE,OAAM;kCAEL;;;;QAIT;kBAEA,cAAA,MAACtD;YAAMwG,SAAS;;8BACd,MAACxG;oBAAMuG,WAAU;oBAAMC,SAAS;oBAAGkB,IAAI;wBAAEC,UAAU;oBAAO;;sCACxD,KAAC1G;4BACCgG,OAAO;4BACP7C,OAAOD,UAAUA,QAAQJ,OAAO,CAACY,cAAc,KAAK;4BACpDiD,UAAUzD,UAAU3E,MAAMqI,YAAY,CAAC1D,QAAQJ,OAAO,EAAEI,QAAQH,QAAQ,IAAI;4BAC5E8D,MAAM;4BACNP,MAAM3B,OAAO4B,OAAO,CAAC;;sCAEvB,KAACvG;4BACCgG,OAAO;4BACP7C,OAAO2B,OAAOF,OAAOiB,QAAQ,CAACiB,SAAS,CAACpD,cAAc,KAAK;4BAC3DmD,MACEjC,OAAOmC,aAAa,KAAK,OACrBjC,OACE,kCACAnB,YACF,GAAGjF,QAAQkG,OAAOmC,aAAa,EAAE,kBAAkB,CAAC;;sCAG5D,KAAC/G;4BACCgG,OAAO;4BACP7C,OAAO2B,OAAOF,OAAOiB,QAAQ,CAACI,WAAW,CAACvC,cAAc,KAAK;4BAC7DmD,MACEjC,OAAOoC,eAAe,KAAK,OACvBrD,YACA,GAAGjF,QAAQkG,OAAOoC,eAAe,EAAE,kBAAkB,CAAC;4BAE5DC,YAAY;;sCAEd,KAACjH;4BACCgG,OAAO;4BACP7C,OAAO2B,OAAOF,OAAOsC,IAAI,CAACxD,cAAc,KAAK;4BAC7CmD,MAAM;4BACNP,MAAM3B,OAAO4B,OAAO,CAAC;;;;gBAGxBhE,SAASc,MAAM,KAAK,WAAWA,WAAW,wBACzC,KAACxE;oBAAMsI,UAAS;8BAAWvD;qBACzB;8BACJ,MAACjF;oBAAQyI,OAAO;;sCACd,KAACzH;4BACC4D,MAAMhF,MAAMmH,iBAAiB,CAAC3B,GAAG,CAAC,CAAC4B;oCAI3BZ;uCAJ2C;oCACjDb,KAAKyB;oCACLK,OAAOzH,MAAMqH,sBAAsB,CAACD,WAAW;oCAC/CxC,OAAOyB,OAAOiB,QAAQ,CAACF,WAAW;oCAClCkB,IAAI,GAAE9B,SAAAA,MAAMH,OAAOiB,QAAQ,CAACF,WAAW,EAAEf,OAAOkB,KAAK,aAA/Cf,SAAoDpB;oCAC1DtB,OACEsD,eAAe,cACX,iBACAA,eAAe,gBACb,iBACAhC;gCACV;;4BACA0D,WAAW;;wBAEZvC,QAAQ,CAACF,OAAOkB,KAAK,iBACpB,KAAC9G;4BAAWoD,SAAQ;4BAAQC,OAAM;sCAC/B;6BAED;;;8BAEN,MAAC1D;oBAAQyI,OAAO;;sCACd,KAACzH;4BACC4D,MAAM0B,QAAQlB,GAAG,CAAC,CAACgC;oCAIXhB;uCAJuB;oCAC7Bb,KAAK6B,OAAO7B,GAAG;oCACf8B,OAAOD,OAAOC,KAAK;oCACnB7C,OAAO4C,OAAO1E,KAAK;oCACnBwF,IAAI,GAAE9B,SAAAA,MAAMgB,OAAO1E,KAAK,EAAEuD,OAAOiB,QAAQ,CAACI,WAAW,aAA/ClB,SAAoDpB;oCAC1DtB,OAAO;gCACT;;4BACAgF,WACEhE,WAAW,YAAY,aAAa;;wBAGvC8B,cAAc,kBACb,KAACnG;4BAAWoD,SAAQ;4BAAUC,OAAM;sCACjC,CAAC,IAAI,EAAExC,OAAOsF,aAAa,gBAAgB,2BAA2B,CAAC;6BAExE;;;gBAGL1B,wBACC,KAACzE;oBAAWoD,SAAQ;oBAAUC,OAAM;8BACjCoB;qBAED;;;;AAIZ;AACAD,eAAeK,WAAW,GAAG;AAC7BvC,eAAeuC,WAAW,GAAG;AAE7B,eAAevC,eAAc"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/lead-funnel-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 { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport {\n collection,\n type Firestore,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../../hooks/use-crm-org-mount'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename } from './report-format'\nimport { type CrmReportScope, reportCacheKey } from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport {\n type AggregateRead,\n useAggregateRead,\n useWindowRead,\n type WindowRead,\n} from './use-aggregate-read'\nimport { deltaPercent, leadFunnel } from '../../model/crm-reports'\n\n/**\n * How many of the period's leads the funnel is placed from — the Leads\n * section's own window, over the period rather than over everything.\n *\n * The captured count is a server aggregate and never meets this bound; the\n * funnel needs each lead's status and reason, so it reads the period's\n * newest two hundred and says when there were more. At the organization\n * level the bound is PER SITE (AGL-2634): each site's window is its own,\n * and a site with a thousand leads must not crowd out a site with ten.\n */\nexport const LEAD_CEILING = 200\n/** How many reasons the bar list draws; the rest are counted beneath it. */\nconst REASONS_SHOWN = 8\n\n/** The CSV's columns: one file for both the statuses and the reasons. */\nconst COLUMNS = ['Kind', 'Label', 'Count', 'Share'] as const\n\ntype LeadRow = Record<string, unknown> & Aglyn.CrmLeadFields & { $id: string }\n\n/** The period's captured count, and the previous period's for the delta. */\ninterface CapturedFigures {\n current: number\n previous: number\n}\n\nexport interface LeadFunnelCardProps {\n report: CrmReportScope\n /**\n * The site whose leads are placed. A lead lives under its host, not the\n * org — `hosts/{hostId}/leads`, private by path, no `visibleTo` — so the\n * report's org scope cannot reach it and the card is told the site. At\n * the organization level `null` (AGL-2630): the card then reads every\n * site the mount lists, one window each (AGL-2634).\n */\n hostId: string | null\n}\n\nconst LEAD_FUNNEL_HELP = Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#lead-funnel',\n excerpt:\n 'Of the leads this site captured in the period, how many are ' +\n 'still new, being worked, qualified or unqualified — and the ' +\n 'reasons the unqualified ones were closed. Placed by when each ' +\n 'lead was first seen.',\n})\n\n/**\n * The leads a scope may see, first seen in a range (AGL-3275).\n *\n * `visibleTo` is the site's tokens under a site, and `null` at the\n * organization level where an org-wide member reads without a clause. The\n * range stays a single-field one; with the scope clause it composes with the\n * `visibleTo` + `firstSeenAtMs` entries in `cloud/firebase-firestore.indexes.json`\n * — the descending one for the window, the ascending one for the counts,\n * which name no order (AGL-3443).\n */\nfunction leadsBetween(\n firestore: Firestore,\n orgId: string,\n visibleTo: readonly string[] | null,\n from: number,\n to: number,\n) {\n return query(\n collection(firestore, 'orgs', orgId, 'leads'),\n ...(visibleTo ? [where('visibleTo', 'array-contains-any', [...visibleTo])] : []),\n where('firstSeenAtMs', '>=', from),\n where('firstSeenAtMs', '<', to),\n )\n}\n\nconst countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n/**\n * The lead funnel, at whichever level the report is mounted (AGL-2624,\n * AGL-2630, AGL-2634).\n *\n * Under a site: that site. At the organization level: every site the\n * mount lists, one window each, totaled — the org hub's reports total\n * every site, and the leads are no exception now that the fan-out is\n * bounded by the org's site list. With no sites to read the card says so\n * instead of guessing.\n */\nexport function LeadFunnelCard(props: LeadFunnelCardProps) {\n const { report, hostId } = props\n const mount = useCrmOrgMount()\n if (hostId) return <SiteLeadFunnelCard report={report} hostId={hostId} />\n if (mount && (!mount.hostsReady || mount.hosts.length > 0)) {\n return <OrgLeadFunnelCard report={report} />\n }\n return (\n <CardDisplay header={'Lead funnel'} help={LEAD_FUNNEL_HELP} contentGutterX contentGutterY>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mount\n ? 'Leads live under a site, and this organization has no sites yet.'\n : 'Leads live under a site, and this report is mounted under none.'}\n </Typography>\n </CardDisplay>\n )\n}\n\n/**\n * The lead funnel (AGL-2624): of the leads this site captured in the\n * period, how many are still new, being worked, qualified or unqualified —\n * and why the unqualified ones were closed.\n *\n * A cohort by first sighting, followed to where each lead stands now,\n * because that is the question a period asks of leads — \"what became of\n * the ones we got\" — and because a lead's status has no history on the\n * document: only its current value. The window is a range on\n * `firstSeenAtMs`, which the capture door stamps once on creation and\n * never again, so a returning visitor's second form is not a second lead\n * in a second period. A single-field range needs no composite index.\n *\n * Read directly off `hosts/{hostId}/leads` under the site's own rules —\n * any member of the site may read it — the way the Leads section reads\n * it, so the funnel and the list can never disagree about who may see a\n * lead.\n */\nfunction SiteLeadFunnelCard(props: { report: CrmReportScope; hostId: string }) {\n const { report, hostId } = props\n const { range } = report\n const firestore = useFirestore()\n\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], report.tokens, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:counts:${hostId}`) },\n )\n const window = useWindowRead<LeadRow>(\n () =>\n query(\n leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n LEAD_CEILING,\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:window:${hostId}`) },\n )\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n (figures !== null ? ` of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'This site’s leads could not be read.'}\n />\n )\n}\nSiteLeadFunnelCard.displayName = 'SiteLeadFunnelCard'\n\n/**\n * THE ORGANIZATION'S FUNNEL (AGL-2634, AGL-3275): every lead the org holds.\n *\n * This totaled one aggregate and one window PER SITE, because the sites'\n * lead collections were disjoint. They share one collection now, so a sum\n * would count a lead that two brands in a consent group both hold once for\n * each of them — and the merge that existed so a busy site could not crowd\n * out a quiet one has nothing left to balance. One aggregate and one\n * bounded read, unscoped, as an org-wide member's reads are.\n */\nfunction OrgLeadFunnelCard(props: { report: CrmReportScope }) {\n const { report } = props\n const { range } = report\n const firestore = useFirestore()\n const mount = useCrmOrgMount()\n const hostIds = useMemo(\n () => (mount?.hostsReady ? mount.hosts.map((host) => host.id) : null),\n [mount],\n )\n\n /*\n * ONE READ, not one per site (AGL-3275). The sites share a collection now,\n * so a lead two brands in a consent group both hold would be counted once\n * for each of them — and the org's funnel would report more leads than the\n * org has. `null` tokens is the org-wide read that carries no clause.\n */\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], null, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], null, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:counts:org') },\n )\n const merged = useAggregateRead<{ rows: LeadRow[]; truncated: boolean }>(\n () =>\n getDocs(\n query(\n leadsBetween(firestore, report.scope[1], null, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n ).then((snapshot) =>\n ceilingedWindow(\n snapshot.docs.map(\n (document) => ({ ...document.data(), $id: document.id }) as unknown as LeadRow,\n ),\n LEAD_CEILING,\n ),\n ),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:window:org') },\n )\n const window = useMemo<WindowRead<LeadRow>>(\n () => ({\n rows: merged.value?.rows ?? [],\n truncated: merged.value?.truncated ?? false,\n status: merged.status,\n }),\n [merged],\n )\n const sites = hostIds?.length ?? 0\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n subheader={`Every site (${sites.toLocaleString()})`}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n ` — at most ${LEAD_CEILING.toLocaleString()}` +\n (figures !== null ? ` — of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'The organization’s leads could not be read.'}\n />\n )\n}\nOrgLeadFunnelCard.displayName = 'OrgLeadFunnelCard'\n\n/**\n * The funnel as drawn, whichever level fed it: four tiles, the statuses,\n * the reasons, the export. Pure over the two reads.\n */\nfunction LeadFunnelBody(props: {\n report: CrmReportScope\n subheader?: string\n captured: AggregateRead<CapturedFigures>\n window: WindowRead<LeadRow>\n caption?: string\n errorText: string\n}) {\n const { report, subheader, captured, window, caption, errorText } = props\n const { period, routes } = report\n const status = window.status\n\n const funnel = useMemo(() => leadFunnel(window.rows), [window])\n const read = status === 'success'\n const figures = captured.value\n const share = (count: number, of: number): string | null =>\n of > 0 ? percent(count / of) : null\n const reasons = funnel.reasons.slice(0, REASONS_SHOWN)\n const moreReasons = funnel.reasons.length - reasons.length\n\n return (\n <CardDisplay\n header={'Lead funnel'}\n subheader={subheader}\n help={LEAD_FUNNEL_HELP}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <ReportExport\n filename={reportFilename('lead-funnel', period)}\n columns={COLUMNS}\n rows={() => [\n ...Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => [\n 'Status',\n Aglyn.CRM_LEAD_STATUS_LABELS[leadStatus],\n funnel.byStatus[leadStatus],\n share(funnel.byStatus[leadStatus], funnel.total) ?? '',\n ]),\n ...funnel.reasons.map((reason) => [\n 'Unqualified reason',\n reason.label,\n reason.count,\n share(reason.count, funnel.byStatus.unqualified) ?? '',\n ]),\n ]}\n disabled={!read || !funnel.total}\n />\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('leads')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open leads'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Leads captured'}\n value={figures ? figures.current.toLocaleString() : null}\n deltaPct={figures ? deltaPercent(figures.current, figures.previous) : null}\n note={'first seen in the period'}\n href={routes.section('leads')}\n />\n <ReportStatTile\n label={'Qualified'}\n value={read ? funnel.byStatus.qualified.toLocaleString() : null}\n note={\n funnel.qualifiedRate === null\n ? read\n ? 'nobody captured in the period'\n : undefined\n : `${percent(funnel.qualifiedRate)} of those captured`\n }\n />\n <ReportStatTile\n label={'Unqualified'}\n value={read ? funnel.byStatus.unqualified.toLocaleString() : null}\n note={\n funnel.unqualifiedRate === null\n ? undefined\n : `${percent(funnel.unqualifiedRate)} of those captured`\n }\n riseIsGood={false}\n />\n <ReportStatTile\n label={'Still open'}\n value={read ? funnel.open.toLocaleString() : null}\n note={'new or being worked'}\n href={routes.section('leads')}\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{errorText}</Alert>\n ) : null}\n <Section title={'Where they stand'}>\n <ReportBreakdown\n rows={Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => ({\n key: leadStatus,\n label: Aglyn.CRM_LEAD_STATUS_LABELS[leadStatus],\n value: funnel.byStatus[leadStatus],\n note: share(funnel.byStatus[leadStatus], funnel.total) ?? undefined,\n color:\n leadStatus === 'qualified'\n ? 'success.main'\n : leadStatus === 'unqualified'\n ? 'warning.main'\n : undefined,\n }))}\n emptyText={''}\n />\n {read && !funnel.total ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No leads captured in this period.'}\n </Typography>\n ) : null}\n </Section>\n <Section title={'Why leads were unqualified'}>\n <ReportBreakdown\n rows={reasons.map((reason) => ({\n key: reason.key,\n label: reason.label,\n value: reason.count,\n note: share(reason.count, funnel.byStatus.unqualified) ?? undefined,\n color: 'warning.main',\n }))}\n emptyText={\n status === 'loading' ? 'Reading…' : 'No lead was unqualified in this period.'\n }\n />\n {moreReasons > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`And ${plural(moreReasons, 'other reason')}, each given once or twice.`}\n </Typography>\n ) : null}\n </Section>\n {/* What the funnel, and so the export, is placed from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadFunnelBody.displayName = 'LeadFunnelBody'\nLeadFunnelCard.displayName = 'LeadFunnelCard'\n\nexport default LeadFunnelCard\n"],"names":["Aglyn","AppLink","CardDisplay","percent","Section","ceilingedWindow","Alert","Button","Stack","Typography","collection","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","useCrmOrgMount","ReportBreakdown","ReportExport","plural","reportFilename","reportCacheKey","ReportStatTile","useAggregateRead","useWindowRead","deltaPercent","leadFunnel","LEAD_CEILING","REASONS_SHOWN","COLUMNS","LEAD_FUNNEL_HELP","pluginDocsHelp","anchor","excerpt","leadsBetween","firestore","orgId","visibleTo","from","to","countOf","target","then","snapshot","data","count","LeadFunnelCard","props","report","hostId","mount","SiteLeadFunnelCard","hostsReady","hosts","length","OrgLeadFunnelCard","header","help","contentGutterX","contentGutterY","variant","color","range","captured","Promise","all","scope","tokens","previousFrom","previousTo","current","previous","cacheKey","window","figures","value","sampled","status","truncated","rows","LeadFunnelBody","caption","toLocaleString","undefined","errorText","displayName","hostIds","map","host","id","merged","docs","document","$id","sites","subheader","period","routes","funnel","read","share","of","reasons","slice","moreReasons","HeaderProps","action","direction","spacing","filename","columns","CRM_LEAD_STATUSES","leadStatus","CRM_LEAD_STATUS_LABELS","byStatus","total","reason","label","unqualified","disabled","component","componentVariant","nativeButton","href","section","size","sx","flexWrap","deltaPct","note","qualified","qualifiedRate","unqualifiedRate","riseIsGood","open","severity","title","key","emptyText"],"mappings":"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"}
@@ -31,6 +31,7 @@ import { plural, reportFilename, shortDate } from "./report-format.js";
31
31
  import { reportCacheKey, scopedCollection, visibleToClause } from "./report-scope.js";
32
32
  import { ReportStatTile } from "./report-stat-tile.js";
33
33
  import { useAggregateRead, useWindowRead } from "./use-aggregate-read.js";
34
+ import { currencyOfDeals, pipelineTotals } from "../../model/crm-reports.js";
34
35
  /**
35
36
  * How many open deals the by-stage chart, the forecast and the top-deals
36
37
  * table are read from.
@@ -116,7 +117,7 @@ const TOP_DEALS = 10;
116
117
  var _byPipeline_get;
117
118
  return {
118
119
  pipeline,
119
- totals: Aglyn.pipelineTotals((_byPipeline_get = byPipeline.get(pipeline.$id)) != null ? _byPipeline_get : [], pipeline)
120
+ totals: pipelineTotals((_byPipeline_get = byPipeline.get(pipeline.$id)) != null ? _byPipeline_get : [], pipeline)
120
121
  };
121
122
  });
122
123
  const known = new Set(pipelineWindow.rows.map((pipeline)=>pipeline.$id));
@@ -137,11 +138,11 @@ const TOP_DEALS = 10;
137
138
  }).slice(0, TOP_DEALS);
138
139
  return {
139
140
  pipelines,
140
- orphanTotals: Aglyn.pipelineTotals(orphans, null),
141
+ orphanTotals: pipelineTotals(orphans, null),
141
142
  weightedCents: pipelines.reduce((total, entry)=>total + entry.totals.weightedCents, 0),
142
143
  top,
143
144
  stageName,
144
- currency: Aglyn.currencyOfDeals(dealWindow.rows)
145
+ currency: currencyOfDeals(dealWindow.rows)
145
146
  };
146
147
  }, [
147
148
  dealWindow,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/pipeline-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { collectionCeiling } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename, shortDate } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\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 HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('top-open-deals')}\n columns={TOP_DEAL_COLUMNS}\n rows={() =>\n summary.top.map((deal) => [\n deal.title || deal.$id,\n summary.stageName(deal),\n typeof deal.expectedCloseAtMs === 'number' && deal.expectedCloseAtMs > 0\n ? new Date(deal.expectedCloseAtMs).toISOString().slice(0, 10)\n : '',\n (Number(deal.amountCents ?? 0) / 100).toFixed(2),\n String(deal.currency || currency).toUpperCase(),\n ])\n }\n disabled={!dealsRead || !summary.top.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open deals'}\n value={open.value ? open.value.count.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Pipeline value'}\n value={open.value ? money(open.value.amountCents, currency) : null}\n note={'face value of every open deal'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Weighted forecast'}\n value={dealsRead ? money(summary.weightedCents, currency) : null}\n note={\n dealWindow.truncated\n ? `from the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated`\n : 'each deal at its stage odds'\n }\n />\n {/*\n Open deals with no open task against them (AGL-2661): the ones\n nobody has a next step for. Painted as a warning above zero,\n because a deal with nothing planned is a deal going nowhere; the\n tile opens the deals list, whose \"No next activity\" chip lists\n them.\n */}\n <ReportStatTile\n label={'Stuck deals'}\n value={dealsRead ? stuck.count.toLocaleString() : null}\n note={'open, with no next activity'}\n riseIsGood={false}\n color={stuck.count > 0 ? 'warning.main' : undefined}\n href={routes.section('deals')}\n />\n </Stack>\n {open.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The deal totals could not be read.'}\n </Alert>\n ) : null}\n {summary.currency.mixed ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Deals are in more than one currency; the totals add them as numbers and show ${currency.toUpperCase()}, the most common.`}\n </Typography>\n ) : null}\n {dealsRead && !summary.pipelines.length && !dealWindow.rows.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No open deals yet.'}\n </Typography>\n ) : null}\n {summary.pipelines.map(({ pipeline, totals }) => (\n <Section key={pipeline.$id} title={pipeline.name || 'Pipeline'}>\n <ReportBreakdown\n rows={[\n ...totals.stages.map((row) => ({\n key: row.stage.id,\n label: row.stage.name,\n value: row.amountCents,\n display: money(row.amountCents, currency),\n note: `${plural(row.count, 'deal')} · ${money(row.weightedCents, currency)} weighted`,\n })),\n ...(totals.unplaced.count\n ? [\n {\n key: '$unplaced',\n label: 'Stage no longer in this pipeline',\n value: totals.unplaced.amountCents,\n display: money(totals.unplaced.amountCents, currency),\n note: `${plural(totals.unplaced.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]\n : []),\n ]}\n emptyText={'This pipeline has no open stages.'}\n />\n </Section>\n ))}\n {summary.orphanTotals.count ? (\n <Section title={'Deals in no pipeline'}>\n <ReportBreakdown\n rows={[\n {\n key: '$orphans',\n label: 'No pipeline',\n value: summary.orphanTotals.amountCents,\n display: money(summary.orphanTotals.amountCents, currency),\n note: `${plural(summary.orphanTotals.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]}\n emptyText={''}\n />\n </Section>\n ) : null}\n {pipelineWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Only the first ${PIPELINE_CEILING} pipelines are charted.`}\n </Typography>\n ) : null}\n {summary.top.length ? (\n <Section title={'Top open deals'}>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Deal'}</TableCell>\n <TableCell>{'Stage'}</TableCell>\n <TableCell>{'Expected close'}</TableCell>\n <TableCell align=\"right\">{'Amount'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {summary.top.map((deal) => (\n <TableRow key={deal.$id}>\n <TableCell>\n <AppLink href={routes.deal(deal.$id)}>\n {deal.title || deal.$id}\n </AppLink>\n </TableCell>\n <TableCell>{summary.stageName(deal)}</TableCell>\n <TableCell>{shortDate(deal.expectedCloseAtMs)}</TableCell>\n <TableCell align=\"right\">\n {money(\n Number(deal.amountCents ?? 0),\n String(deal.currency || currency),\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {/* What the ranking, and so the export, is drawn from when that is not everything. */}\n {dealWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Ranked within the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.`}\n </Typography>\n ) : null}\n </Section>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nPipelineCard.displayName = 'PipelineCard'\n\nexport default PipelineCard\n"],"names":["Aglyn","money","AppLink","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","count","getAggregateFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","collectionCeiling","ReportBreakdown","ReportExport","plural","reportFilename","shortDate","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","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","HeaderProps","action","filename","columns","title","expectedCloseAtMs","Date","toISOString","toFixed","String","toUpperCase","disabled","length","spacing","direction","sx","flexWrap","label","value","toLocaleString","note","href","section","truncated","riseIsGood","color","undefined","severity","mixed","variant","stages","row","stage","id","display","unplaced","emptyText","size","align","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/pipeline-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { collectionCeiling } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename, shortDate } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { currencyOfDeals, pipelineTotals } from '../../model/crm-reports'\n\n/**\n * How many open deals the by-stage chart, the forecast and the top-deals\n * table are read from.\n *\n * The count and the face value of the pipeline are server aggregates and\n * never meet this bound. The forecast cannot be: it weights each deal by\n * ITS stage's odds, which is a join the server will not do, so it reads the\n * deals and says so when there were more than this.\n */\nconst OPEN_DEAL_CEILING = 1000\n/** More pipelines than this is a shape nobody has asked for; the probe says if it happens. */\nconst PIPELINE_CEILING = 20\nconst TOP_DEALS = 10\n\n/** The top-deals table's columns, which are also the CSV's — the amount split from its currency there. */\nconst TOP_DEAL_COLUMNS = ['Deal', 'Stage', 'Expected close', 'Amount', 'Currency'] as const\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\ntype PipelineRow = Aglyn.CrmPipeline & { $id: string }\n\nexport interface PipelineCardProps {\n report: CrmReportScope\n}\n\n/**\n * The open pipeline: how many deals, what they add up to, what they are\n * worth at their stage odds, where they sit, and the largest of them\n * (AGL-2604).\n *\n * Not period-scoped, deliberately. A pipeline is a stock, not a flow — what\n * is open NOW — and a \"pipeline over the last 30 days\" would be a report on\n * deals created in a window, which is a different question with a different\n * answer. The period picker governs the cards that measure flows.\n */\nexport function PipelineCard(props: PipelineCardProps) {\n const { report } = props\n const { scope, tokens, routes, nowMs } = report\n const firestore = useFirestore()\n\n const open = useAggregateRead(\n () =>\n getAggregateFromServer(\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n ),\n { count: count(), amountCents: sum('amountCents') },\n ).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:open') },\n )\n\n /**\n * The open deals, most recently updated first — the order the\n * `(visibleTo, status, updatedAt)` index the deals list already uses can\n * serve, so the report adds no index for this read.\n */\n const dealWindow = useWindowRead<DealRow>(\n () =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n orderBy('updatedAt', 'desc'),\n limit(OPEN_DEAL_CEILING + 1),\n ),\n OPEN_DEAL_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:deals') },\n )\n const pipelineWindow = useWindowRead<PipelineRow>(\n () =>\n collectionCeiling(\n query(\n scopedCollection(firestore, scope, 'pipelines'),\n ...visibleToClause(tokens),\n ),\n PIPELINE_CEILING,\n ),\n PIPELINE_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:pipelines') },\n )\n const dealsStatus = dealWindow.status\n\n const summary = useMemo(() => {\n const byPipeline = new Map<string, DealRow[]>()\n for (const deal of dealWindow.rows) {\n const key = deal.pipelineId ?? ''\n const list = byPipeline.get(key) ?? []\n list.push(deal)\n byPipeline.set(key, list)\n }\n // Every active pipeline is charted even with nothing open; an archived\n // one only while a deal still sits open in it — the forecast card's\n // rule, so the two cards name the same pipelines.\n const pipelines = pipelineWindow.rows\n .filter((pipeline) => !Aglyn.isPipelineArchived(pipeline) || byPipeline.has(pipeline.$id))\n .map((pipeline) => ({\n pipeline,\n totals: pipelineTotals(byPipeline.get(pipeline.$id) ?? [], pipeline),\n }))\n const known = new Set(pipelineWindow.rows.map((pipeline) => pipeline.$id))\n const orphans = dealWindow.rows.filter(\n (deal) => !known.has(deal.pipelineId ?? ''),\n )\n const stageName = (deal: DealRow): string =>\n Aglyn.dealStageById(\n pipelineWindow.rows.find((pipeline) => pipeline.$id === deal.pipelineId),\n deal.stageId,\n )?.name ?? deal.stageId\n const top = [...dealWindow.rows]\n .sort(\n (a, b) => Number(b.amountCents ?? 0) - Number(a.amountCents ?? 0),\n )\n .slice(0, TOP_DEALS)\n return {\n pipelines,\n orphanTotals: pipelineTotals(orphans, null),\n weightedCents: pipelines.reduce(\n (total, entry) => total + entry.totals.weightedCents,\n 0,\n ),\n top,\n stageName,\n currency: currencyOfDeals(dealWindow.rows),\n }\n }, [dealWindow, pipelineWindow])\n // Open deals with nothing scheduled (AGL-2661), off the same window.\n const stuck = useMemo(() => Aglyn.stuckDeals(dealWindow.rows), [dealWindow.rows])\n\n const currency = summary.currency.currency\n const dealsRead = dealsStatus === 'success'\n\n return (\n <CardDisplay\n header={'Pipeline'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#pipeline',\n excerpt:\n 'Every open deal: how many, what they add up to, and what they ' +\n 'are worth once each is weighted by the odds of its stage. Not ' +\n 'tied to the period — a pipeline is what is open now.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('top-open-deals')}\n columns={TOP_DEAL_COLUMNS}\n rows={() =>\n summary.top.map((deal) => [\n deal.title || deal.$id,\n summary.stageName(deal),\n typeof deal.expectedCloseAtMs === 'number' && deal.expectedCloseAtMs > 0\n ? new Date(deal.expectedCloseAtMs).toISOString().slice(0, 10)\n : '',\n (Number(deal.amountCents ?? 0) / 100).toFixed(2),\n String(deal.currency || currency).toUpperCase(),\n ])\n }\n disabled={!dealsRead || !summary.top.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open deals'}\n value={open.value ? open.value.count.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Pipeline value'}\n value={open.value ? money(open.value.amountCents, currency) : null}\n note={'face value of every open deal'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Weighted forecast'}\n value={dealsRead ? money(summary.weightedCents, currency) : null}\n note={\n dealWindow.truncated\n ? `from the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated`\n : 'each deal at its stage odds'\n }\n />\n {/*\n Open deals with no open task against them (AGL-2661): the ones\n nobody has a next step for. Painted as a warning above zero,\n because a deal with nothing planned is a deal going nowhere; the\n tile opens the deals list, whose \"No next activity\" chip lists\n them.\n */}\n <ReportStatTile\n label={'Stuck deals'}\n value={dealsRead ? stuck.count.toLocaleString() : null}\n note={'open, with no next activity'}\n riseIsGood={false}\n color={stuck.count > 0 ? 'warning.main' : undefined}\n href={routes.section('deals')}\n />\n </Stack>\n {open.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The deal totals could not be read.'}\n </Alert>\n ) : null}\n {summary.currency.mixed ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Deals are in more than one currency; the totals add them as numbers and show ${currency.toUpperCase()}, the most common.`}\n </Typography>\n ) : null}\n {dealsRead && !summary.pipelines.length && !dealWindow.rows.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No open deals yet.'}\n </Typography>\n ) : null}\n {summary.pipelines.map(({ pipeline, totals }) => (\n <Section key={pipeline.$id} title={pipeline.name || 'Pipeline'}>\n <ReportBreakdown\n rows={[\n ...totals.stages.map((row) => ({\n key: row.stage.id,\n label: row.stage.name,\n value: row.amountCents,\n display: money(row.amountCents, currency),\n note: `${plural(row.count, 'deal')} · ${money(row.weightedCents, currency)} weighted`,\n })),\n ...(totals.unplaced.count\n ? [\n {\n key: '$unplaced',\n label: 'Stage no longer in this pipeline',\n value: totals.unplaced.amountCents,\n display: money(totals.unplaced.amountCents, currency),\n note: `${plural(totals.unplaced.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]\n : []),\n ]}\n emptyText={'This pipeline has no open stages.'}\n />\n </Section>\n ))}\n {summary.orphanTotals.count ? (\n <Section title={'Deals in no pipeline'}>\n <ReportBreakdown\n rows={[\n {\n key: '$orphans',\n label: 'No pipeline',\n value: summary.orphanTotals.amountCents,\n display: money(summary.orphanTotals.amountCents, currency),\n note: `${plural(summary.orphanTotals.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]}\n emptyText={''}\n />\n </Section>\n ) : null}\n {pipelineWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Only the first ${PIPELINE_CEILING} pipelines are charted.`}\n </Typography>\n ) : null}\n {summary.top.length ? (\n <Section title={'Top open deals'}>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Deal'}</TableCell>\n <TableCell>{'Stage'}</TableCell>\n <TableCell>{'Expected close'}</TableCell>\n <TableCell align=\"right\">{'Amount'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {summary.top.map((deal) => (\n <TableRow key={deal.$id}>\n <TableCell>\n <AppLink href={routes.deal(deal.$id)}>\n {deal.title || deal.$id}\n </AppLink>\n </TableCell>\n <TableCell>{summary.stageName(deal)}</TableCell>\n <TableCell>{shortDate(deal.expectedCloseAtMs)}</TableCell>\n <TableCell align=\"right\">\n {money(\n Number(deal.amountCents ?? 0),\n String(deal.currency || currency),\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {/* What the ranking, and so the export, is drawn from when that is not everything. */}\n {dealWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Ranked within the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.`}\n </Typography>\n ) : null}\n </Section>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nPipelineCard.displayName = 'PipelineCard'\n\nexport default PipelineCard\n"],"names":["Aglyn","money","AppLink","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","count","getAggregateFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","collectionCeiling","ReportBreakdown","ReportExport","plural","reportFilename","shortDate","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","currencyOfDeals","pipelineTotals","OPEN_DEAL_CEILING","PIPELINE_CEILING","TOP_DEALS","TOP_DEAL_COLUMNS","PipelineCard","props","report","scope","tokens","routes","nowMs","firestore","open","amountCents","then","snapshot","Number","data","cacheKey","dealWindow","pipelineWindow","dealsStatus","status","summary","byPipeline","Map","deal","rows","key","pipelineId","list","get","push","set","pipelines","filter","pipeline","isPipelineArchived","has","$id","map","totals","known","Set","orphans","stageName","dealStageById","find","stageId","name","top","sort","a","b","slice","orphanTotals","weightedCents","reduce","total","entry","currency","stuck","stuckDeals","dealsRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","title","expectedCloseAtMs","Date","toISOString","toFixed","String","toUpperCase","disabled","length","spacing","direction","sx","flexWrap","label","value","toLocaleString","note","href","section","truncated","riseIsGood","color","undefined","severity","mixed","variant","stages","row","stage","id","display","unplaced","emptyText","size","align","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,YAAYA,WAAW,eAAc;AACrC,SAASC,KAAK,QAAQ,6DAA4D;AAClF,SAASC,OAAO,EAAEC,WAAW,QAAQ,uBAAsB;AAC3D,SAASC,OAAO,QAAQ,6DAA4D;AACpF,SAASC,WAAW,QAAQ,yDAAwD;AACpF,SACEC,KAAK,EACLC,KAAK,EACLC,SAAS,EACTC,SAAS,EACTC,SAAS,EACTC,QAAQ,EACRC,UAAU,QACL,gBAAe;AACtB,SACEC,KAAK,EACLC,sBAAsB,EACtBC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,GAAG,EACHC,KAAK,QACA,qBAAoB;AAC3B,SAASC,OAAO,QAAQ,QAAO;AAC/B,SAASC,YAAY,QAAQ,iCAAgC;AAC7D,SAASC,iBAAiB,QAAQ,+DAA8D;AAChG,SAASC,eAAe,QAAQ,wBAAoB;AACpD,SAASC,YAAY,QAAQ,qBAAiB;AAC9C,SAASC,MAAM,EAAEC,cAAc,EAAEC,SAAS,QAAQ,qBAAiB;AACnE,SAEEC,cAAc,EACdC,gBAAgB,EAChBC,eAAe,QACV,oBAAgB;AACvB,SAASC,cAAc,QAAQ,wBAAoB;AACnD,SAASC,gBAAgB,EAAEC,aAAa,QAAQ,0BAAsB;AACtE,SAASC,eAAe,EAAEC,cAAc,QAAQ,6BAAyB;AAEzE;;;;;;;;CAQC,GACD,MAAMC,oBAAoB;AAC1B,4FAA4F,GAC5F,MAAMC,mBAAmB;AACzB,MAAMC,YAAY;AAElB,wGAAwG,GACxG,MAAMC,mBAAmB;IAAC;IAAQ;IAAS;IAAkB;IAAU;CAAW;AASlF;;;;;;;;;CASC,GACD,OAAO,SAASC,aAAaC,KAAwB;IACnD,MAAM,EAAEC,MAAM,EAAE,GAAGD;IACnB,MAAM,EAAEE,KAAK,EAAEC,MAAM,EAAEC,MAAM,EAAEC,KAAK,EAAE,GAAGJ;IACzC,MAAMK,YAAY1B;IAElB,MAAM2B,OAAOhB,iBACX,IACElB,uBACEG,MACEY,iBAAiBkB,WAAWJ,OAAO,aAChCb,gBAAgBc,SACnBzB,MAAM,UAAU,MAAM,UAExB;YAAEN,OAAOA;YAASoC,aAAa/B,IAAI;QAAe,GAClDgC,IAAI,CAAC,CAACC;gBACQA,sBACMA;mBAFA;gBACpBtC,OAAOuC,QAAOD,uBAAAA,SAASE,IAAI,GAAGxC,KAAK,YAArBsC,uBAAyB;gBACvCF,aAAaG,QAAOD,6BAAAA,SAASE,IAAI,GAAGJ,WAAW,YAA3BE,6BAA+B;YACrD;YACF;QAACJ;QAAWJ;QAAOC;QAAQE;KAAM,EACjC;QAAEQ,UAAU1B,eAAec,QAAQ;IAAiB;IAGtD;;;;GAIC,GACD,MAAMa,aAAatB,cACjB,IACEhB,MACEY,iBAAiBkB,WAAWJ,OAAO,aAChCb,gBAAgBc,SACnBzB,MAAM,UAAU,MAAM,SACtBH,QAAQ,aAAa,SACrBD,MAAMqB,oBAAoB,KAE9BA,mBACA;QAACW;QAAWJ;QAAOC;QAAQE;KAAM,EACjC;QAAEQ,UAAU1B,eAAec,QAAQ;IAAkB;IAEvD,MAAMc,iBAAiBvB,cACrB,IACEX,kBACEL,MACEY,iBAAiBkB,WAAWJ,OAAO,iBAChCb,gBAAgBc,UAErBP,mBAEJA,kBACA;QAACU;QAAWJ;QAAOC;QAAQE;KAAM,EACjC;QAAEQ,UAAU1B,eAAec,QAAQ;IAAsB;IAE3D,MAAMe,cAAcF,WAAWG,MAAM;IAErC,MAAMC,UAAUvC,QAAQ;QACtB,MAAMwC,aAAa,IAAIC;QACvB,KAAK,MAAMC,QAAQP,WAAWQ,IAAI,CAAE;gBACtBD,kBACCF;YADb,MAAMI,OAAMF,mBAAAA,KAAKG,UAAU,YAAfH,mBAAmB;YAC/B,MAAMI,QAAON,kBAAAA,WAAWO,GAAG,CAACH,gBAAfJ,kBAAuB,EAAE;YACtCM,KAAKE,IAAI,CAACN;YACVF,WAAWS,GAAG,CAACL,KAAKE;QACtB;QACA,uEAAuE;QACvE,oEAAoE;QACpE,kDAAkD;QAClD,MAAMI,YAAYd,eAAeO,IAAI,CAClCQ,MAAM,CAAC,CAACC,WAAa,CAACxE,MAAMyE,kBAAkB,CAACD,aAAaZ,WAAWc,GAAG,CAACF,SAASG,GAAG,GACvFC,GAAG,CAAC,CAACJ;gBAEmBZ;mBAFL;gBAClBY;gBACAK,QAAQ1C,gBAAeyB,kBAAAA,WAAWO,GAAG,CAACK,SAASG,GAAG,aAA3Bf,kBAAgC,EAAE,EAAEY;YAC7D;;QACF,MAAMM,QAAQ,IAAIC,IAAIvB,eAAeO,IAAI,CAACa,GAAG,CAAC,CAACJ,WAAaA,SAASG,GAAG;QACxE,MAAMK,UAAUzB,WAAWQ,IAAI,CAACQ,MAAM,CACpC,CAACT;gBAAoBA;mBAAX,CAACgB,MAAMJ,GAAG,EAACZ,mBAAAA,KAAKG,UAAU,YAAfH,mBAAmB;;QAE1C,MAAMmB,YAAY,CAACnB;;gBACjB9D;4BAAAA,uBAAAA,MAAMkF,aAAa,CACjB1B,eAAeO,IAAI,CAACoB,IAAI,CAAC,CAACX,WAAaA,SAASG,GAAG,KAAKb,KAAKG,UAAU,GACvEH,KAAKsB,OAAO,sBAFdpF,qBAGGqF,IAAI,mBAAIvB,KAAKsB,OAAO;;QACzB,MAAME,MAAM;eAAI/B,WAAWQ,IAAI;SAAC,CAC7BwB,IAAI,CACH,CAACC,GAAGC;gBAAaA,gBAA6BD;mBAApCpC,QAAOqC,iBAAAA,EAAExC,WAAW,YAAbwC,iBAAiB,KAAKrC,QAAOoC,iBAAAA,EAAEvC,WAAW,YAAbuC,iBAAiB;WAEhEE,KAAK,CAAC,GAAGpD;QACZ,OAAO;YACLgC;YACAqB,cAAcxD,eAAe6C,SAAS;YACtCY,eAAetB,UAAUuB,MAAM,CAC7B,CAACC,OAAOC,QAAUD,QAAQC,MAAMlB,MAAM,CAACe,aAAa,EACpD;YAEFN;YACAL;YACAe,UAAU9D,gBAAgBqB,WAAWQ,IAAI;QAC3C;IACF,GAAG;QAACR;QAAYC;KAAe;IAC/B,qEAAqE;IACrE,MAAMyC,QAAQ7E,QAAQ,IAAMpB,MAAMkG,UAAU,CAAC3C,WAAWQ,IAAI,GAAG;QAACR,WAAWQ,IAAI;KAAC;IAEhF,MAAMiC,WAAWrC,QAAQqC,QAAQ,CAACA,QAAQ;IAC1C,MAAMG,YAAY1C,gBAAgB;IAElC,qBACE,KAACtD;QACCiG,QAAQ;QACRC,MAAMrG,MAAMsG,cAAc,CAAC,cAAc;YACvCC,QAAQ;YACRC,SACE,mEACA,mEACA;QACJ;QACAC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,sBACE,KAACpF;gBACCqF,UAAUnF,eAAe;gBACzBoF,SAASvE;gBACTwB,MAAM,IACJJ,QAAQ2B,GAAG,CAACV,GAAG,CAAC,CAACd;4BAMPA;+BANgB;4BACxBA,KAAKiD,KAAK,IAAIjD,KAAKa,GAAG;4BACtBhB,QAAQsB,SAAS,CAACnB;4BAClB,OAAOA,KAAKkD,iBAAiB,KAAK,YAAYlD,KAAKkD,iBAAiB,GAAG,IACnE,IAAIC,KAAKnD,KAAKkD,iBAAiB,EAAEE,WAAW,GAAGxB,KAAK,CAAC,GAAG,MACxD;4BACHtC,CAAAA,QAAOU,oBAAAA,KAAKb,WAAW,YAAhBa,oBAAoB,KAAK,GAAE,EAAGqD,OAAO,CAAC;4BAC9CC,OAAOtD,KAAKkC,QAAQ,IAAIA,UAAUqB,WAAW;yBAC9C;;gBAEHC,UAAU,CAACnB,aAAa,CAACxC,QAAQ2B,GAAG,CAACiC,MAAM;;QAGjD;kBAEA,cAAA,MAAChH;YAAMiH,SAAS;;8BACd,MAACjH;oBAAMkH,WAAU;oBAAMD,SAAS;oBAAGE,IAAI;wBAAEC,UAAU;oBAAO;;sCACxD,KAAC5F;4BACC6F,OAAO;4BACPC,OAAO7E,KAAK6E,KAAK,GAAG7E,KAAK6E,KAAK,CAAChH,KAAK,CAACiH,cAAc,KAAK;4BACxDC,MAAM;4BACNC,MAAMnF,OAAOoF,OAAO,CAAC;;sCAEvB,KAAClG;4BACC6F,OAAO;4BACPC,OAAO7E,KAAK6E,KAAK,GAAG5H,MAAM+C,KAAK6E,KAAK,CAAC5E,WAAW,EAAE+C,YAAY;4BAC9D+B,MAAM;4BACNC,MAAMnF,OAAOoF,OAAO,CAAC;;sCAEvB,KAAClG;4BACC6F,OAAO;4BACPC,OAAO1B,YAAYlG,MAAM0D,QAAQiC,aAAa,EAAEI,YAAY;4BAC5D+B,MACExE,WAAW2E,SAAS,GAChB,CAAC,SAAS,EAAE9F,kBAAkB0F,cAAc,GAAG,sBAAsB,CAAC,GACtE;;sCAUR,KAAC/F;4BACC6F,OAAO;4BACPC,OAAO1B,YAAYF,MAAMpF,KAAK,CAACiH,cAAc,KAAK;4BAClDC,MAAM;4BACNI,YAAY;4BACZC,OAAOnC,MAAMpF,KAAK,GAAG,IAAI,iBAAiBwH;4BAC1CL,MAAMnF,OAAOoF,OAAO,CAAC;;;;gBAGxBjF,KAAKU,MAAM,KAAK,wBACf,KAACpD;oBAAMgI,UAAS;8BACb;qBAED;gBACH3E,QAAQqC,QAAQ,CAACuC,KAAK,iBACrB,KAAC3H;oBAAW4H,SAAQ;oBAAUJ,OAAM;8BACjC,CAAC,6EAA6E,EAAEpC,SAASqB,WAAW,GAAG,kBAAkB,CAAC;qBAE3H;gBACHlB,aAAa,CAACxC,QAAQW,SAAS,CAACiD,MAAM,IAAI,CAAChE,WAAWQ,IAAI,CAACwD,MAAM,iBAChE,KAAC3G;oBAAW4H,SAAQ;oBAAQJ,OAAM;8BAC/B;qBAED;gBACHzE,QAAQW,SAAS,CAACM,GAAG,CAAC,CAAC,EAAEJ,QAAQ,EAAEK,MAAM,EAAE,iBAC1C,KAACzE;wBAA2B2G,OAAOvC,SAASa,IAAI,IAAI;kCAClD,cAAA,KAAC9D;4BACCwC,MAAM;mCACDc,OAAO4D,MAAM,CAAC7D,GAAG,CAAC,CAAC8D,MAAS,CAAA;wCAC7B1E,KAAK0E,IAAIC,KAAK,CAACC,EAAE;wCACjBhB,OAAOc,IAAIC,KAAK,CAACtD,IAAI;wCACrBwC,OAAOa,IAAIzF,WAAW;wCACtB4F,SAAS5I,MAAMyI,IAAIzF,WAAW,EAAE+C;wCAChC+B,MAAM,GAAGtG,OAAOiH,IAAI7H,KAAK,EAAE,QAAQ,GAAG,EAAEZ,MAAMyI,IAAI9C,aAAa,EAAEI,UAAU,SAAS,CAAC;oCACvF,CAAA;mCACInB,OAAOiE,QAAQ,CAACjI,KAAK,GACrB;oCACE;wCACEmD,KAAK;wCACL4D,OAAO;wCACPC,OAAOhD,OAAOiE,QAAQ,CAAC7F,WAAW;wCAClC4F,SAAS5I,MAAM4E,OAAOiE,QAAQ,CAAC7F,WAAW,EAAE+C;wCAC5C+B,MAAM,GAAGtG,OAAOoD,OAAOiE,QAAQ,CAACjI,KAAK,EAAE,QAAQ,eAAe,CAAC;wCAC/DuH,OAAO;oCACT;iCACD,GACD,EAAE;6BACP;4BACDW,WAAW;;uBAvBDvE,SAASG,GAAG;gBA2B3BhB,QAAQgC,YAAY,CAAC9E,KAAK,iBACzB,KAACT;oBAAQ2G,OAAO;8BACd,cAAA,KAACxF;wBACCwC,MAAM;4BACJ;gCACEC,KAAK;gCACL4D,OAAO;gCACPC,OAAOlE,QAAQgC,YAAY,CAAC1C,WAAW;gCACvC4F,SAAS5I,MAAM0D,QAAQgC,YAAY,CAAC1C,WAAW,EAAE+C;gCACjD+B,MAAM,GAAGtG,OAAOkC,QAAQgC,YAAY,CAAC9E,KAAK,EAAE,QAAQ,eAAe,CAAC;gCACpEuH,OAAO;4BACT;yBACD;wBACDW,WAAW;;qBAGb;gBACHvF,eAAe0E,SAAS,iBACvB,KAACtH;oBAAW4H,SAAQ;oBAAUJ,OAAM;8BACjC,CAAC,eAAe,EAAE/F,iBAAiB,uBAAuB,CAAC;qBAE5D;gBACHsB,QAAQ2B,GAAG,CAACiC,MAAM,iBACjB,MAACnH;oBAAQ2G,OAAO;;sCACd,MAAC1G;4BAAY2I,MAAK;;8CAChB,KAACtI;8CACC,cAAA,MAACC;;0DACC,KAACF;0DAAW;;0DACZ,KAACA;0DAAW;;0DACZ,KAACA;0DAAW;;0DACZ,KAACA;gDAAUwI,OAAM;0DAAS;;;;;8CAG9B,KAACzI;8CACEmD,QAAQ2B,GAAG,CAACV,GAAG,CAAC,CAACd;4CAWHA;6DAVb,MAACnD;;8DACC,KAACF;8DACC,cAAA,KAACP;wDAAQ8H,MAAMnF,OAAOiB,IAAI,CAACA,KAAKa,GAAG;kEAChCb,KAAKiD,KAAK,IAAIjD,KAAKa,GAAG;;;8DAG3B,KAAClE;8DAAWkD,QAAQsB,SAAS,CAACnB;;8DAC9B,KAACrD;8DAAWkB,UAAUmC,KAAKkD,iBAAiB;;8DAC5C,KAACvG;oDAAUwI,OAAM;8DACdhJ,MACCmD,QAAOU,oBAAAA,KAAKb,WAAW,YAAhBa,oBAAoB,IAC3BsD,OAAOtD,KAAKkC,QAAQ,IAAIA;;;2CAXflC,KAAKa,GAAG;;;;;wBAmB5BpB,WAAW2E,SAAS,iBACnB,KAACtH;4BAAW4H,SAAQ;4BAAUJ,OAAM;sCACjC,CAAC,kBAAkB,EAAEhG,kBAAkB0F,cAAc,GAAG,kCAAkC,CAAC;6BAE5F;;qBAEJ;;;;AAIZ;AACAtF,aAAa0G,WAAW,GAAG;AAE3B,eAAe1G,aAAY"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import type { CrmReportPeriod } from '@aglyn/aglyn';
17
+ import type { CrmReportPeriod } from '../../model/crm-reports';
18
18
  /**
19
19
  * The label under one week's bar — the day the week starts, as `Sep 1`.
20
20
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/report-format.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { CrmReportPeriod } from '@aglyn/aglyn'\n\n/**\n * The label under one week's bar — the day the week starts, as `Sep 1`.\n *\n * The start alone, because a bar is narrow: \"Sep 1 – Sep 7\" under thirteen\n * bars truncates to nothing readable, and the reader knows every bar is a\n * week because the chart said so.\n */\nexport function weekLabel(startMs: number): string {\n return new Date(startMs).toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n })\n}\n\n/**\n * The name of a report table's CSV — `crm-activity-30d.csv` — so a folder\n * of downloads says which card and which period each file came from. The\n * cards that describe a stock rather than a flow — the open pipeline, the\n * open tasks — have no period and name none.\n */\nexport function reportFilename(card: string, period?: CrmReportPeriod): string {\n return `crm-${card}${period ? `-${period}` : ''}.csv`\n}\n\n/** A date in a table cell, or a dash for none. */\nexport function shortDate(ms: number | null | undefined): string {\n return typeof ms === 'number' && Number.isFinite(ms) && ms > 0\n ? new Date(ms).toLocaleDateString()\n : '—'\n}\n\n/** `1 deal`, `2 deals` — a count with its noun. */\nexport function plural(count: number, singular: string, many = `${singular}s`): string {\n return `${count.toLocaleString()} ${count === 1 ? singular : many}`\n}\n"],"names":["weekLabel","startMs","Date","toLocaleDateString","undefined","month","day","reportFilename","card","period","shortDate","ms","Number","isFinite","plural","count","singular","many","toLocaleString"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;CAMC,GACD,OAAO,SAASA,UAAUC,OAAe;IACvC,OAAO,IAAIC,KAAKD,SAASE,kBAAkB,CAACC,WAAW;QACrDC,OAAO;QACPC,KAAK;IACP;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASC,eAAeC,IAAY,EAAEC,MAAwB;IACnE,OAAO,CAAC,IAAI,EAAED,OAAOC,SAAS,CAAC,CAAC,EAAEA,QAAQ,GAAG,GAAG,IAAI,CAAC;AACvD;AAEA,gDAAgD,GAChD,OAAO,SAASC,UAAUC,EAA6B;IACrD,OAAO,OAAOA,OAAO,YAAYC,OAAOC,QAAQ,CAACF,OAAOA,KAAK,IACzD,IAAIT,KAAKS,IAAIR,kBAAkB,KAC/B;AACN;AAEA,iDAAiD,GACjD,OAAO,SAASW,OAAOC,KAAa,EAAEC,QAAgB,EAAEC,OAAO,GAAGD,SAAS,CAAC,CAAC;IAC3E,OAAO,GAAGD,MAAMG,cAAc,GAAG,CAAC,EAAEH,UAAU,IAAIC,WAAWC,MAAM;AACrE"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/report-format.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { CrmReportPeriod } from '../../model/crm-reports'\n\n/**\n * The label under one week's bar — the day the week starts, as `Sep 1`.\n *\n * The start alone, because a bar is narrow: \"Sep 1 – Sep 7\" under thirteen\n * bars truncates to nothing readable, and the reader knows every bar is a\n * week because the chart said so.\n */\nexport function weekLabel(startMs: number): string {\n return new Date(startMs).toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n })\n}\n\n/**\n * The name of a report table's CSV — `crm-activity-30d.csv` — so a folder\n * of downloads says which card and which period each file came from. The\n * cards that describe a stock rather than a flow — the open pipeline, the\n * open tasks — have no period and name none.\n */\nexport function reportFilename(card: string, period?: CrmReportPeriod): string {\n return `crm-${card}${period ? `-${period}` : ''}.csv`\n}\n\n/** A date in a table cell, or a dash for none. */\nexport function shortDate(ms: number | null | undefined): string {\n return typeof ms === 'number' && Number.isFinite(ms) && ms > 0\n ? new Date(ms).toLocaleDateString()\n : '—'\n}\n\n/** `1 deal`, `2 deals` — a count with its noun. */\nexport function plural(count: number, singular: string, many = `${singular}s`): string {\n return `${count.toLocaleString()} ${count === 1 ? singular : many}`\n}\n"],"names":["weekLabel","startMs","Date","toLocaleDateString","undefined","month","day","reportFilename","card","period","shortDate","ms","Number","isFinite","plural","count","singular","many","toLocaleString"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;CAMC,GACD,OAAO,SAASA,UAAUC,OAAe;IACvC,OAAO,IAAIC,KAAKD,SAASE,kBAAkB,CAACC,WAAW;QACrDC,OAAO;QACPC,KAAK;IACP;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASC,eAAeC,IAAY,EAAEC,MAAwB;IACnE,OAAO,CAAC,IAAI,EAAED,OAAOC,SAAS,CAAC,CAAC,EAAEA,QAAQ,GAAG,GAAG,IAAI,CAAC;AACvD;AAEA,gDAAgD,GAChD,OAAO,SAASC,UAAUC,EAA6B;IACrD,OAAO,OAAOA,OAAO,YAAYC,OAAOC,QAAQ,CAACF,OAAOA,KAAK,IACzD,IAAIT,KAAKS,IAAIR,kBAAkB,KAC/B;AACN;AAEA,iDAAiD,GACjD,OAAO,SAASW,OAAOC,KAAa,EAAEC,QAAgB,EAAEC,OAAO,GAAGD,SAAS,CAAC,CAAC;IAC3E,OAAO,GAAGD,MAAMG,cAAc,GAAG,CAAC,EAAEH,UAAU,IAAIC,WAAWC,MAAM;AACrE"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import type { CrmReportPeriod, CrmReportRange } from '@aglyn/aglyn';
17
+ import type { CrmReportPeriod, CrmReportRange } from '../../model/crm-reports';
18
18
  import { type CollectionReference, type Firestore, type QueryConstraint } from 'firebase/firestore';
19
19
  import type { CrmRoutes } from '../../model/crm-routes';
20
20
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/report-scope.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { CrmReportPeriod, CrmReportRange } from '@aglyn/aglyn'\nimport {\n collection,\n type CollectionReference,\n type Firestore,\n type QueryConstraint,\n where,\n} from 'firebase/firestore'\nimport type { CrmRoutes } from '../../model/crm-routes'\n\n/**\n * What every report card is handed, resolved once by the section (AGL-2604).\n *\n * The cards are independent — each opens its own reads and draws its own\n * figures — but they must agree on four things or the page contradicts\n * itself: WHICH org's collections they read, WHO the reader is (the scope\n * tokens every query filters on), WHICH holder's facet a contact field is\n * read through, and WHEN the period starts and ends. Resolving those in the\n * section and passing them down is what keeps a card from computing its own\n * \"now\" a second after its neighbor's, or its own token list without the\n * org-wide token.\n */\nexport interface CrmReportScope {\n /** `['orgs', orgId]` — the org-shared data root every CRM collection lives under. */\n scope: readonly ['orgs', string]\n /**\n * The scope tokens this reader may see — the `array-contains-any` operand\n * of every query a card runs, and the same predicate the rules evaluate\n * — or `null` at the ORGANIZATION level (AGL-2630), where an org-wide\n * member's reports carry no clause and total every site at once.\n */\n tokens: readonly string[] | null\n /**\n * The consent group whose facet the contact fields are read through, or\n * `null` at the organization level, where each contact's own primary\n * holder is read instead (`contactPrimaryGroup`, from {@link org}).\n */\n groupId: string | null\n /** The org document, for the per-contact holder when {@link groupId} is null. */\n org: Record<string, unknown> | null\n period: CrmReportPeriod\n range: CrmReportRange\n /** The moment the period was anchored at, so every card shares one clock. */\n nowMs: number\n routes: CrmRoutes\n}\n\n/**\n * What every remembered report answer is keyed by: the org, the reader's\n * tokens, the holder's facet and the period — never the clock. A reopened\n * section anchors a new `nowMs`, and the whole point of remembering is that\n * the reopening is the same question; the TTL, not the key, decides when the\n * answer has aged out. `cardKey` names the read within the card so that two\n * counts on one card do not overwrite each other.\n */\nexport function reportCacheKey(\n report: Pick<CrmReportScope, 'scope' | 'tokens' | 'groupId' | 'period'>,\n cardKey: string,\n): string {\n return `${reportCachePrefix(report)}${report.period}|${cardKey}`\n}\n\n/** Everything one reader's reports of one org are remembered under — what Refresh forgets. */\nexport function reportCachePrefix(\n report: Pick<CrmReportScope, 'scope' | 'tokens' | 'groupId'>,\n): string {\n // `*` for the org level's \"every token\" and \"every holder\": a real token\n // list never contains it, so the two levels cannot share an answer.\n const tokens = report.tokens ? [...report.tokens].sort().join(',') : '*'\n return `crm-report|${report.scope[1]}|${tokens}|${report.groupId ?? '*'}|`\n}\n\n/** One CRM collection under the report's org. */\nexport function scopedCollection(\n firestore: Firestore,\n scope: CrmReportScope['scope'],\n name: string,\n): CollectionReference {\n return collection(firestore, scope[0], scope[1], name)\n}\n\n/**\n * The visibility predicate, as the clause every report query starts with —\n * spread into the query, because at the organization level it is NOTHING.\n *\n * A function rather than a convention so that a card cannot forget it: a\n * query on a CRM collection without it is permission-denied rather than a\n * leak, per the rules, but a denied aggregate renders as a dash and a\n * missing clause would be diagnosed as \"the count is broken\" rather than as\n * what it is. The org level (AGL-2630) drops it on purpose: the rules admit\n * an org-wide member to every row, and a report there is a total over\n * every site.\n */\nexport function visibleToClause(\n tokens: readonly string[] | null,\n): QueryConstraint[] {\n return tokens ? [where('visibleTo', 'array-contains-any', [...tokens])] : []\n}\n"],"names":["collection","where","reportCacheKey","report","cardKey","reportCachePrefix","period","tokens","sort","join","scope","groupId","scopedCollection","firestore","name","visibleToClause"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SACEA,UAAU,EAIVC,KAAK,QACA,qBAAoB;AAwC3B;;;;;;;CAOC,GACD,OAAO,SAASC,eACdC,MAAuE,EACvEC,OAAe;IAEf,OAAO,GAAGC,kBAAkBF,UAAUA,OAAOG,MAAM,CAAC,CAAC,EAAEF,SAAS;AAClE;AAEA,4FAA4F,GAC5F,OAAO,SAASC,kBACdF,MAA4D;QAKVA;IAHlD,yEAAyE;IACzE,oEAAoE;IACpE,MAAMI,SAASJ,OAAOI,MAAM,GAAG;WAAIJ,OAAOI,MAAM;KAAC,CAACC,IAAI,GAAGC,IAAI,CAAC,OAAO;IACrE,OAAO,CAAC,WAAW,EAAEN,OAAOO,KAAK,CAAC,EAAE,CAAC,CAAC,EAAEH,OAAO,CAAC,GAAEJ,kBAAAA,OAAOQ,OAAO,YAAdR,kBAAkB,IAAI,CAAC,CAAC;AAC5E;AAEA,+CAA+C,GAC/C,OAAO,SAASS,iBACdC,SAAoB,EACpBH,KAA8B,EAC9BI,IAAY;IAEZ,OAAOd,WAAWa,WAAWH,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAEI;AACnD;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,gBACdR,MAAgC;IAEhC,OAAOA,SAAS;QAACN,MAAM,aAAa,sBAAsB;eAAIM;SAAO;KAAE,GAAG,EAAE;AAC9E"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/report-scope.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { CrmReportPeriod, CrmReportRange } from '../../model/crm-reports'\nimport {\n collection,\n type CollectionReference,\n type Firestore,\n type QueryConstraint,\n where,\n} from 'firebase/firestore'\nimport type { CrmRoutes } from '../../model/crm-routes'\n\n/**\n * What every report card is handed, resolved once by the section (AGL-2604).\n *\n * The cards are independent — each opens its own reads and draws its own\n * figures — but they must agree on four things or the page contradicts\n * itself: WHICH org's collections they read, WHO the reader is (the scope\n * tokens every query filters on), WHICH holder's facet a contact field is\n * read through, and WHEN the period starts and ends. Resolving those in the\n * section and passing them down is what keeps a card from computing its own\n * \"now\" a second after its neighbor's, or its own token list without the\n * org-wide token.\n */\nexport interface CrmReportScope {\n /** `['orgs', orgId]` — the org-shared data root every CRM collection lives under. */\n scope: readonly ['orgs', string]\n /**\n * The scope tokens this reader may see — the `array-contains-any` operand\n * of every query a card runs, and the same predicate the rules evaluate\n * — or `null` at the ORGANIZATION level (AGL-2630), where an org-wide\n * member's reports carry no clause and total every site at once.\n */\n tokens: readonly string[] | null\n /**\n * The consent group whose facet the contact fields are read through, or\n * `null` at the organization level, where each contact's own primary\n * holder is read instead (`contactPrimaryGroup`, from {@link org}).\n */\n groupId: string | null\n /** The org document, for the per-contact holder when {@link groupId} is null. */\n org: Record<string, unknown> | null\n period: CrmReportPeriod\n range: CrmReportRange\n /** The moment the period was anchored at, so every card shares one clock. */\n nowMs: number\n routes: CrmRoutes\n}\n\n/**\n * What every remembered report answer is keyed by: the org, the reader's\n * tokens, the holder's facet and the period — never the clock. A reopened\n * section anchors a new `nowMs`, and the whole point of remembering is that\n * the reopening is the same question; the TTL, not the key, decides when the\n * answer has aged out. `cardKey` names the read within the card so that two\n * counts on one card do not overwrite each other.\n */\nexport function reportCacheKey(\n report: Pick<CrmReportScope, 'scope' | 'tokens' | 'groupId' | 'period'>,\n cardKey: string,\n): string {\n return `${reportCachePrefix(report)}${report.period}|${cardKey}`\n}\n\n/** Everything one reader's reports of one org are remembered under — what Refresh forgets. */\nexport function reportCachePrefix(\n report: Pick<CrmReportScope, 'scope' | 'tokens' | 'groupId'>,\n): string {\n // `*` for the org level's \"every token\" and \"every holder\": a real token\n // list never contains it, so the two levels cannot share an answer.\n const tokens = report.tokens ? [...report.tokens].sort().join(',') : '*'\n return `crm-report|${report.scope[1]}|${tokens}|${report.groupId ?? '*'}|`\n}\n\n/** One CRM collection under the report's org. */\nexport function scopedCollection(\n firestore: Firestore,\n scope: CrmReportScope['scope'],\n name: string,\n): CollectionReference {\n return collection(firestore, scope[0], scope[1], name)\n}\n\n/**\n * The visibility predicate, as the clause every report query starts with —\n * spread into the query, because at the organization level it is NOTHING.\n *\n * A function rather than a convention so that a card cannot forget it: a\n * query on a CRM collection without it is permission-denied rather than a\n * leak, per the rules, but a denied aggregate renders as a dash and a\n * missing clause would be diagnosed as \"the count is broken\" rather than as\n * what it is. The org level (AGL-2630) drops it on purpose: the rules admit\n * an org-wide member to every row, and a report there is a total over\n * every site.\n */\nexport function visibleToClause(\n tokens: readonly string[] | null,\n): QueryConstraint[] {\n return tokens ? [where('visibleTo', 'array-contains-any', [...tokens])] : []\n}\n"],"names":["collection","where","reportCacheKey","report","cardKey","reportCachePrefix","period","tokens","sort","join","scope","groupId","scopedCollection","firestore","name","visibleToClause"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SACEA,UAAU,EAIVC,KAAK,QACA,qBAAoB;AAwC3B;;;;;;;CAOC,GACD,OAAO,SAASC,eACdC,MAAuE,EACvEC,OAAe;IAEf,OAAO,GAAGC,kBAAkBF,UAAUA,OAAOG,MAAM,CAAC,CAAC,EAAEF,SAAS;AAClE;AAEA,4FAA4F,GAC5F,OAAO,SAASC,kBACdF,MAA4D;QAKVA;IAHlD,yEAAyE;IACzE,oEAAoE;IACpE,MAAMI,SAASJ,OAAOI,MAAM,GAAG;WAAIJ,OAAOI,MAAM;KAAC,CAACC,IAAI,GAAGC,IAAI,CAAC,OAAO;IACrE,OAAO,CAAC,WAAW,EAAEN,OAAOO,KAAK,CAAC,EAAE,CAAC,CAAC,EAAEH,OAAO,CAAC,GAAEJ,kBAAAA,OAAOQ,OAAO,YAAdR,kBAAkB,IAAI,CAAC,CAAC;AAC5E;AAEA,+CAA+C,GAC/C,OAAO,SAASS,iBACdC,SAAoB,EACpBH,KAA8B,EAC9BI,IAAY;IAEZ,OAAOd,WAAWa,WAAWH,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAEI;AACnD;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,gBACdR,MAAgC;IAEhC,OAAOA,SAAS;QAACN,MAAM,aAAa,sBAAsB;eAAIM;SAAO;KAAE,GAAG,EAAE;AAC9E"}
@@ -28,6 +28,7 @@ import { reportFilename } from "./report-format.js";
28
28
  import { reportCacheKey, scopedCollection, visibleToClause } from "./report-scope.js";
29
29
  import { ReportStatTile } from "./report-stat-tile.js";
30
30
  import { useAggregateRead, useWindowRead } from "./use-aggregate-read.js";
31
+ import { conversionBySource } from "../../model/crm-reports.js";
31
32
  /**
32
33
  * How many of the period's contacts the by-source table is read from.
33
34
  *
@@ -83,7 +84,7 @@ import { useAggregateRead, useWindowRead } from "./use-aggregate-read.js";
83
84
  cacheKey: reportCacheKey(report, 'sources:window')
84
85
  });
85
86
  const status = window.status;
86
- const conversion = useMemo(()=>Aglyn.conversionBySource(window.rows.map((row)=>Aglyn.readContactFacet(row, groupId))), [
87
+ const conversion = useMemo(()=>conversionBySource(window.rows.map((row)=>Aglyn.readContactFacet(row, groupId))), [
87
88
  window,
88
89
  groupId
89
90
  ]);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/source-conversion-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 { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} 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 getCountFromServer,\n limit,\n orderBy,\n query,\n Timestamp,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportExport } from './report-export'\nimport { reportFilename } 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 of the period's contacts the by-source table is read from.\n *\n * The captured count is a server aggregate and never meets this bound;\n * the table needs each contact's sources and stage off this holder's\n * facet, which no aggregate can read, so it reads the period's newest\n * thousand and says when there were more — the same bound the sources\n * mix keeps, over the period rather than all time.\n */\nconst CONTACT_CEILING = 1000\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = ['Source', 'Captured', 'Customers', 'Conversion'] as const\n\nexport interface SourceConversionCardProps {\n report: CrmReportScope\n}\n\n/**\n * Conversion by source (AGL-2624): of the people captured through each\n * door in the period, how many are customers now.\n *\n * A COHORT by capture date, not a count of purchases in the period: the\n * question is \"how well does each source convert\", and the honest\n * population for it is the people that source brought in, followed to\n * where they stand today. A form lead captured on day one and buying on\n * day twenty is a conversion the form earned; a customer captured last\n * year who bought again this month is not evidence about this period's\n * doors. So the window is the period's contacts by `createdAt` — the\n * `(visibleTo, createdAt)` index the trend card counts through — and\n * \"customer\" is read off the facet: the stage at `customer` or past it,\n * or an order on the books whatever the stage says.\n *\n * Through THIS group's facet, like every by-source figure: a source is\n * which of this business's doors the person came through, and a stage is\n * where this business's team put them.\n */\nexport function SourceConversionCard(props: SourceConversionCardProps) {\n const { report } = props\n const { scope, tokens, groupId, range, period, routes } = report\n const firestore = useFirestore()\n\n const capturedInPeriod = () =>\n query(\n scopedCollection(firestore, scope, 'contacts'),\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(range.from)),\n where('createdAt', '<', Timestamp.fromMillis(range.to)),\n )\n\n const captured = useAggregateRead<number>(\n () => getCountFromServer(capturedInPeriod()).then((snapshot) => snapshot.data().count),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:captured') },\n )\n const window = useWindowRead<Record<string, unknown>>(\n () =>\n query(\n capturedInPeriod(),\n orderBy('createdAt', 'desc'),\n limit(CONTACT_CEILING + 1),\n ),\n CONTACT_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:window') },\n )\n const status = window.status\n\n const conversion = useMemo(\n () =>\n Aglyn.conversionBySource(\n window.rows.map((row) => Aglyn.readContactFacet(row, groupId)),\n ),\n [window, groupId],\n )\n const read = status === 'success'\n const rate = read\n ? measuredRate(conversion.customers, conversion.total, 'captured')\n : null\n const labelOf = (source: Aglyn.ContactSource): string =>\n Aglyn.CONTACT_SOURCE_LABELS[source] ?? source\n const sampled =\n read &&\n (window.truncated ||\n (captured.value !== null && captured.value > window.rows.length))\n const caption = sampled\n ? `Read from the ${window.rows.length.toLocaleString()} most recently captured contacts` +\n (captured.value !== null ? ` of ${captured.value.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n\n return (\n <CardDisplay\n header={'Conversion by source'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#conversion-by-source',\n excerpt:\n 'Of the people each capture surface brought in during the ' +\n 'period, how many are customers now — at the customer stage or ' +\n 'past it, or with an order on the books. Read through this ' +\n 'site’s own view of each person.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('conversion-by-source', period)}\n columns={COLUMNS}\n rows={() =>\n conversion.rows.map((row) => [\n labelOf(row.source),\n row.captured,\n row.customers,\n percent(row.rate),\n ])\n }\n disabled={!read || !conversion.rows.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Captured'}\n value={captured.value !== null ? captured.value.toLocaleString() : null}\n note={'in the period, counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Now customers'}\n value={read ? conversion.customers.toLocaleString() : null}\n note={'of those captured'}\n />\n <ReportStatTile\n label={'Conversion'}\n value={rate ? percent(rate.value) : null}\n note={\n rate\n ? `${rate.numerator.toLocaleString()} of ${rate.denominator.toLocaleString()} ${rate.denominatorLabel}`\n : read\n ? 'nobody captured in the period'\n : undefined\n }\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{'The period’s contacts could not be read.'}</Alert>\n ) : null}\n <Section title={'By source'}>\n {conversion.rows.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {conversion.rows.map((row) => (\n <TableRow key={row.source}>\n <TableCell>{labelOf(row.source)}</TableCell>\n <TableCell align=\"right\">{row.captured.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.customers.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{percent(row.rate)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {status === 'loading' ? 'Reading…' : 'Nobody captured in this period.'}\n </Typography>\n )}\n {conversion.rows.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'A person captured two ways counts under both.' +\n (conversion.unsourced\n ? ` ${conversion.unsourced.toLocaleString()} with no source on this site are counted in the tiles only.`\n : '')}\n </Typography>\n ) : null}\n {/* What the table, and so the export, is drawn from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nSourceConversionCard.displayName = 'SourceConversionCard'\n\nexport default SourceConversionCard\n"],"names":["Aglyn","CardDisplay","measuredRate","percent","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","Timestamp","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","CONTACT_CEILING","COLUMNS","SourceConversionCard","props","report","scope","tokens","groupId","range","period","routes","firestore","capturedInPeriod","fromMillis","from","to","captured","then","snapshot","data","count","cacheKey","window","status","conversion","conversionBySource","rows","map","row","readContactFacet","read","rate","customers","total","labelOf","source","CONTACT_SOURCE_LABELS","sampled","truncated","value","length","caption","toLocaleString","undefined","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","disabled","spacing","direction","sx","flexWrap","label","note","href","section","numerator","denominator","denominatorLabel","severity","title","size","column","index","align","variant","color","unsourced","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,YAAYA,WAAW,eAAc;AACrC,SAASC,WAAW,QAAQ,uBAAsB;AAClD,SACEC,YAAY,EACZC,OAAO,EACPC,OAAO,QACF,6DAA4D;AACnE,SAASC,WAAW,QAAQ,yDAAwD;AACpF,SACEC,KAAK,EACLC,KAAK,EACLC,SAAS,EACTC,SAAS,EACTC,SAAS,EACTC,QAAQ,EACRC,UAAU,QACL,gBAAe;AACtB,SACEC,kBAAkB,EAClBC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,SAAS,EACTC,KAAK,QACA,qBAAoB;AAC3B,SAASC,OAAO,QAAQ,QAAO;AAC/B,SAASC,YAAY,QAAQ,iCAAgC;AAC7D,SAASC,YAAY,QAAQ,qBAAiB;AAC9C,SAASC,cAAc,QAAQ,qBAAiB;AAChD,SAEEC,cAAc,EACdC,gBAAgB,EAChBC,eAAe,QACV,oBAAgB;AACvB,SAASC,cAAc,QAAQ,wBAAoB;AACnD,SAASC,gBAAgB,EAAEC,aAAa,QAAQ,0BAAsB;AAEtE;;;;;;;;CAQC,GACD,MAAMC,kBAAkB;AAExB,mDAAmD,GACnD,MAAMC,UAAU;IAAC;IAAU;IAAY;IAAa;CAAa;AAMjE;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,qBAAqBC,KAAgC;IACnE,MAAM,EAAEC,MAAM,EAAE,GAAGD;IACnB,MAAM,EAAEE,KAAK,EAAEC,MAAM,EAAEC,OAAO,EAAEC,KAAK,EAAEC,MAAM,EAAEC,MAAM,EAAE,GAAGN;IAC1D,MAAMO,YAAYpB;IAElB,MAAMqB,mBAAmB,IACvBzB,MACEQ,iBAAiBgB,WAAWN,OAAO,gBAChCT,gBAAgBU,SACnBjB,MAAM,aAAa,MAAMD,UAAUyB,UAAU,CAACL,MAAMM,IAAI,IACxDzB,MAAM,aAAa,KAAKD,UAAUyB,UAAU,CAACL,MAAMO,EAAE;IAGzD,MAAMC,WAAWlB,iBACf,IAAMd,mBAAmB4B,oBAAoBK,IAAI,CAAC,CAACC,WAAaA,SAASC,IAAI,GAAGC,KAAK,GACrF;QAACT;QAAWN;QAAOC;QAAQE;KAAM,EACjC;QAAEa,UAAU3B,eAAeU,QAAQ;IAAoB;IAEzD,MAAMkB,SAASvB,cACb,IACEZ,MACEyB,oBACA1B,QAAQ,aAAa,SACrBD,MAAMe,kBAAkB,KAE5BA,iBACA;QAACW;QAAWN;QAAOC;QAAQE;KAAM,EACjC;QAAEa,UAAU3B,eAAeU,QAAQ;IAAkB;IAEvD,MAAMmB,SAASD,OAAOC,MAAM;IAE5B,MAAMC,aAAalC,QACjB,IACEnB,MAAMsD,kBAAkB,CACtBH,OAAOI,IAAI,CAACC,GAAG,CAAC,CAACC,MAAQzD,MAAM0D,gBAAgB,CAACD,KAAKrB,YAEzD;QAACe;QAAQf;KAAQ;IAEnB,MAAMuB,OAAOP,WAAW;IACxB,MAAMQ,OAAOD,OACTzD,aAAamD,WAAWQ,SAAS,EAAER,WAAWS,KAAK,EAAE,cACrD;IACJ,MAAMC,UAAU,CAACC;YACfhE;gBAAAA,sCAAAA,MAAMiE,qBAAqB,CAACD,OAAO,YAAnChE,sCAAuCgE;;IACzC,MAAME,UACJP,QACCR,CAAAA,OAAOgB,SAAS,IACdtB,SAASuB,KAAK,KAAK,QAAQvB,SAASuB,KAAK,GAAGjB,OAAOI,IAAI,CAACc,MAAM;IACnE,MAAMC,UAAUJ,UACZ,CAAC,cAAc,EAAEf,OAAOI,IAAI,CAACc,MAAM,CAACE,cAAc,GAAG,gCAAgC,CAAC,GACrF1B,CAAAA,SAASuB,KAAK,KAAK,OAAO,CAAC,IAAI,EAAEvB,SAASuB,KAAK,CAACG,cAAc,IAAI,GAAG,EAAC,IACvE,gEACAC;IAEJ,qBACE,KAACvE;QACCwE,QAAQ;QACRC,MAAM1E,MAAM2E,cAAc,CAAC,cAAc;YACvCC,QAAQ;YACRC,SACE,8DACA,mEACA,+DACA;QACJ;QACAC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,sBACE,KAAC5D;gBACC6D,UAAU5D,eAAe,wBAAwBgB;gBACjD6C,SAASrD;gBACTyB,MAAM,IACJF,WAAWE,IAAI,CAACC,GAAG,CAAC,CAACC,MAAQ;4BAC3BM,QAAQN,IAAIO,MAAM;4BAClBP,IAAIZ,QAAQ;4BACZY,IAAII,SAAS;4BACb1D,QAAQsD,IAAIG,IAAI;yBACjB;gBAEHwB,UAAU,CAACzB,QAAQ,CAACN,WAAWE,IAAI,CAACc,MAAM;;QAGhD;kBAEA,cAAA,MAAC9D;YAAM8E,SAAS;;8BACd,MAAC9E;oBAAM+E,WAAU;oBAAMD,SAAS;oBAAGE,IAAI;wBAAEC,UAAU;oBAAO;;sCACxD,KAAC9D;4BACC+D,OAAO;4BACPrB,OAAOvB,SAASuB,KAAK,KAAK,OAAOvB,SAASuB,KAAK,CAACG,cAAc,KAAK;4BACnEmB,MAAM;4BACNC,MAAMpD,OAAOqD,OAAO,CAAC;;sCAEvB,KAAClE;4BACC+D,OAAO;4BACPrB,OAAOT,OAAON,WAAWQ,SAAS,CAACU,cAAc,KAAK;4BACtDmB,MAAM;;sCAER,KAAChE;4BACC+D,OAAO;4BACPrB,OAAOR,OAAOzD,QAAQyD,KAAKQ,KAAK,IAAI;4BACpCsB,MACE9B,OACI,GAAGA,KAAKiC,SAAS,CAACtB,cAAc,GAAG,IAAI,EAAEX,KAAKkC,WAAW,CAACvB,cAAc,GAAG,CAAC,EAAEX,KAAKmC,gBAAgB,EAAE,GACrGpC,OACE,kCACAa;;;;gBAIX3B,SAASO,MAAM,KAAK,WAAWA,WAAW,wBACzC,KAAC9C;oBAAM0F,UAAS;8BAAW;qBACzB;8BACJ,MAAC5F;oBAAQ6F,OAAO;;wBACb5C,WAAWE,IAAI,CAACc,MAAM,iBACrB,MAAChE;4BAAY6F,MAAK;;8CAChB,KAACxF;8CACC,cAAA,KAACC;kDACEmB,QAAQ0B,GAAG,CAAC,CAAC2C,QAAQC,sBACpB,KAAC3F;gDAAuB4F,OAAOD,QAAQ,UAAU;0DAC9CD;+CADaA;;;8CAMtB,KAAC3F;8CACE6C,WAAWE,IAAI,CAACC,GAAG,CAAC,CAACC,oBACpB,MAAC9C;;8DACC,KAACF;8DAAWsD,QAAQN,IAAIO,MAAM;;8DAC9B,KAACvD;oDAAU4F,OAAM;8DAAS5C,IAAIZ,QAAQ,CAAC0B,cAAc;;8DACrD,KAAC9D;oDAAU4F,OAAM;8DAAS5C,IAAII,SAAS,CAACU,cAAc;;8DACtD,KAAC9D;oDAAU4F,OAAM;8DAASlG,QAAQsD,IAAIG,IAAI;;;2CAJ7BH,IAAIO,MAAM;;;2CAU/B,KAACpD;4BAAW0F,SAAQ;4BAAQC,OAAM;sCAC/BnD,WAAW,YAAY,aAAa;;wBAGxCC,WAAWE,IAAI,CAACc,MAAM,iBACrB,KAACzD;4BAAW0F,SAAQ;4BAAUC,OAAM;sCACjC,kDACElD,CAAAA,WAAWmD,SAAS,GACjB,CAAC,CAAC,EAAEnD,WAAWmD,SAAS,CAACjC,cAAc,GAAG,2DAA2D,CAAC,GACtG,EAAC;6BAEP;wBAEHD,wBACC,KAAC1D;4BAAW0F,SAAQ;4BAAUC,OAAM;sCACjCjC;6BAED;;;;;;AAKd;AACAvC,qBAAqB0E,WAAW,GAAG;AAEnC,eAAe1E,qBAAoB"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/source-conversion-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 { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} 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 getCountFromServer,\n limit,\n orderBy,\n query,\n Timestamp,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { conversionBySource } from '../../model/crm-reports'\n\n/**\n * How many of the period's contacts the by-source table is read from.\n *\n * The captured count is a server aggregate and never meets this bound;\n * the table needs each contact's sources and stage off this holder's\n * facet, which no aggregate can read, so it reads the period's newest\n * thousand and says when there were more — the same bound the sources\n * mix keeps, over the period rather than all time.\n */\nconst CONTACT_CEILING = 1000\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = ['Source', 'Captured', 'Customers', 'Conversion'] as const\n\nexport interface SourceConversionCardProps {\n report: CrmReportScope\n}\n\n/**\n * Conversion by source (AGL-2624): of the people captured through each\n * door in the period, how many are customers now.\n *\n * A COHORT by capture date, not a count of purchases in the period: the\n * question is \"how well does each source convert\", and the honest\n * population for it is the people that source brought in, followed to\n * where they stand today. A form lead captured on day one and buying on\n * day twenty is a conversion the form earned; a customer captured last\n * year who bought again this month is not evidence about this period's\n * doors. So the window is the period's contacts by `createdAt` — the\n * `(visibleTo, createdAt)` index the trend card counts through — and\n * \"customer\" is read off the facet: the stage at `customer` or past it,\n * or an order on the books whatever the stage says.\n *\n * Through THIS group's facet, like every by-source figure: a source is\n * which of this business's doors the person came through, and a stage is\n * where this business's team put them.\n */\nexport function SourceConversionCard(props: SourceConversionCardProps) {\n const { report } = props\n const { scope, tokens, groupId, range, period, routes } = report\n const firestore = useFirestore()\n\n const capturedInPeriod = () =>\n query(\n scopedCollection(firestore, scope, 'contacts'),\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(range.from)),\n where('createdAt', '<', Timestamp.fromMillis(range.to)),\n )\n\n const captured = useAggregateRead<number>(\n () => getCountFromServer(capturedInPeriod()).then((snapshot) => snapshot.data().count),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:captured') },\n )\n const window = useWindowRead<Record<string, unknown>>(\n () =>\n query(\n capturedInPeriod(),\n orderBy('createdAt', 'desc'),\n limit(CONTACT_CEILING + 1),\n ),\n CONTACT_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:window') },\n )\n const status = window.status\n\n const conversion = useMemo(\n () =>\n conversionBySource(\n window.rows.map((row) => Aglyn.readContactFacet(row, groupId)),\n ),\n [window, groupId],\n )\n const read = status === 'success'\n const rate = read\n ? measuredRate(conversion.customers, conversion.total, 'captured')\n : null\n const labelOf = (source: Aglyn.ContactSource): string =>\n Aglyn.CONTACT_SOURCE_LABELS[source] ?? source\n const sampled =\n read &&\n (window.truncated ||\n (captured.value !== null && captured.value > window.rows.length))\n const caption = sampled\n ? `Read from the ${window.rows.length.toLocaleString()} most recently captured contacts` +\n (captured.value !== null ? ` of ${captured.value.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n\n return (\n <CardDisplay\n header={'Conversion by source'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#conversion-by-source',\n excerpt:\n 'Of the people each capture surface brought in during the ' +\n 'period, how many are customers now — at the customer stage or ' +\n 'past it, or with an order on the books. Read through this ' +\n 'site’s own view of each person.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('conversion-by-source', period)}\n columns={COLUMNS}\n rows={() =>\n conversion.rows.map((row) => [\n labelOf(row.source),\n row.captured,\n row.customers,\n percent(row.rate),\n ])\n }\n disabled={!read || !conversion.rows.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Captured'}\n value={captured.value !== null ? captured.value.toLocaleString() : null}\n note={'in the period, counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Now customers'}\n value={read ? conversion.customers.toLocaleString() : null}\n note={'of those captured'}\n />\n <ReportStatTile\n label={'Conversion'}\n value={rate ? percent(rate.value) : null}\n note={\n rate\n ? `${rate.numerator.toLocaleString()} of ${rate.denominator.toLocaleString()} ${rate.denominatorLabel}`\n : read\n ? 'nobody captured in the period'\n : undefined\n }\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{'The period’s contacts could not be read.'}</Alert>\n ) : null}\n <Section title={'By source'}>\n {conversion.rows.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {conversion.rows.map((row) => (\n <TableRow key={row.source}>\n <TableCell>{labelOf(row.source)}</TableCell>\n <TableCell align=\"right\">{row.captured.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.customers.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{percent(row.rate)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {status === 'loading' ? 'Reading…' : 'Nobody captured in this period.'}\n </Typography>\n )}\n {conversion.rows.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'A person captured two ways counts under both.' +\n (conversion.unsourced\n ? ` ${conversion.unsourced.toLocaleString()} with no source on this site are counted in the tiles only.`\n : '')}\n </Typography>\n ) : null}\n {/* What the table, and so the export, is drawn from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nSourceConversionCard.displayName = 'SourceConversionCard'\n\nexport default SourceConversionCard\n"],"names":["Aglyn","CardDisplay","measuredRate","percent","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","Timestamp","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","conversionBySource","CONTACT_CEILING","COLUMNS","SourceConversionCard","props","report","scope","tokens","groupId","range","period","routes","firestore","capturedInPeriod","fromMillis","from","to","captured","then","snapshot","data","count","cacheKey","window","status","conversion","rows","map","row","readContactFacet","read","rate","customers","total","labelOf","source","CONTACT_SOURCE_LABELS","sampled","truncated","value","length","caption","toLocaleString","undefined","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","disabled","spacing","direction","sx","flexWrap","label","note","href","section","numerator","denominator","denominatorLabel","severity","title","size","column","index","align","variant","color","unsourced","displayName"],"mappings":"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"}
@@ -29,6 +29,7 @@ import { reportFilename } from "./report-format.js";
29
29
  import { reportCacheKey, scopedCollection, visibleToClause } from "./report-scope.js";
30
30
  import { ReportStatTile } from "./report-stat-tile.js";
31
31
  import { useAggregateRead, useWindowRead } from "./use-aggregate-read.js";
32
+ import { localDayBounds } from "../../model/crm-reports.js";
32
33
  /**
33
34
  * How many open tasks the by-assignee table is grouped from.
34
35
  *
@@ -65,7 +66,7 @@ import { useAggregateRead, useWindowRead } from "./use-aggregate-read.js";
65
66
  const { report } = props;
66
67
  const { scope, tokens, nowMs, routes } = report;
67
68
  const firestore = useFirestore();
68
- const day = useMemo(()=>Aglyn.localDayBounds(nowMs), [
69
+ const day = useMemo(()=>localDayBounds(nowMs), [
69
70
  nowMs
70
71
  ]);
71
72
  const openTasks = ()=>query(scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks), ...visibleToClause(tokens), where('status', '==', 'open'));