@aglyn/plugins-crm 1.0.0-beta.231 → 1.0.0-beta.232

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 (53) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/activity-timeline.js +1 -0
  3. package/src/lib/components/activity-timeline.js.map +1 -1
  4. package/src/lib/components/assignment-rule-drawer.js +1 -0
  5. package/src/lib/components/assignment-rule-drawer.js.map +1 -1
  6. package/src/lib/components/contact-associations-card.js +1 -0
  7. package/src/lib/components/contact-associations-card.js.map +1 -1
  8. package/src/lib/components/contact-duplicates-card.js +1 -0
  9. package/src/lib/components/contact-duplicates-card.js.map +1 -1
  10. package/src/lib/components/contact-field-drawer.js +1 -0
  11. package/src/lib/components/contact-field-drawer.js.map +1 -1
  12. package/src/lib/components/contact-known-by-card.js +1 -0
  13. package/src/lib/components/contact-known-by-card.js.map +1 -1
  14. package/src/lib/components/contact-timeline-card.js +1 -0
  15. package/src/lib/components/contact-timeline-card.js.map +1 -1
  16. package/src/lib/components/crm-glance-card.js +1 -0
  17. package/src/lib/components/crm-glance-card.js.map +1 -1
  18. package/src/lib/components/crm-record-header.js +2 -0
  19. package/src/lib/components/crm-record-header.js.map +1 -1
  20. package/src/lib/components/deals-section.js +1 -0
  21. package/src/lib/components/deals-section.js.map +1 -1
  22. package/src/lib/components/fields-section.js +4 -0
  23. package/src/lib/components/fields-section.js.map +1 -1
  24. package/src/lib/components/lead-history-card.js +1 -0
  25. package/src/lib/components/lead-history-card.js.map +1 -1
  26. package/src/lib/components/lead-surfaces-note.js +1 -0
  27. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  28. package/src/lib/components/linked-deals-card.js +1 -0
  29. package/src/lib/components/linked-deals-card.js.map +1 -1
  30. package/src/lib/components/picklist-values-card.js +1 -0
  31. package/src/lib/components/picklist-values-card.js.map +1 -1
  32. package/src/lib/components/recipes-card.js +1 -0
  33. package/src/lib/components/recipes-card.js.map +1 -1
  34. package/src/lib/components/reports/activity-card.js +1 -0
  35. package/src/lib/components/reports/activity-card.js.map +1 -1
  36. package/src/lib/components/reports/closed-deals-card.js +1 -0
  37. package/src/lib/components/reports/closed-deals-card.js.map +1 -1
  38. package/src/lib/components/reports/contacts-trend-card.js +1 -0
  39. package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
  40. package/src/lib/components/reports/lead-funnel-card.js +1 -0
  41. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  42. package/src/lib/components/reports/lead-source-card.js +1 -0
  43. package/src/lib/components/reports/lead-source-card.js.map +1 -1
  44. package/src/lib/components/reports/pipeline-card.js +1 -0
  45. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  46. package/src/lib/components/reports/source-conversion-card.js +1 -0
  47. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  48. package/src/lib/components/reports/tasks-card.js +1 -0
  49. package/src/lib/components/reports/tasks-card.js.map +1 -1
  50. package/src/lib/components/reports-section.js +2 -0
  51. package/src/lib/components/reports-section.js.map +1 -1
  52. package/src/lib/components/tasks-calendar.js +1 -0
  53. package/src/lib/components/tasks-calendar.js.map +1 -1
@@ -179,6 +179,7 @@ OrgLeadSourceCard.displayName = 'OrgLeadSourceCard';
179
179
  spacing: 2,
180
180
  children: [
181
181
  /*#__PURE__*/ _jsxs(Stack, {
182
+ useFlexGap: true,
182
183
  direction: "row",
183
184
  spacing: 3,
184
185
  sx: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/lead-source-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 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 { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport { getDocs, limit, orderBy, query } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../../hooks/use-crm-org-mount'\nimport { useLeadSourcePicklist } from '../../hooks/use-lead-source-picklist'\nimport {\n LEAD_SOURCE_DIRECTION_LABELS,\n LEAD_SOURCE_UNSPECIFIED_LABEL,\n type LeadSourceDirection,\n} from '../../model/lead-source-direction'\nimport { leadsByLeadSource } from '../../model/crm-reports'\nimport { LEAD_CEILING, leadsBetween } from './lead-funnel-card'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport { type CrmReportScope, reportCacheKey } from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead, type WindowRead } from './use-aggregate-read'\n\ntype LeadRow = Record<string, unknown> & Aglyn.CrmLeadFields & { $id: string }\n\n/** The CSV's columns: one file for both tables. */\nconst COLUMNS = ['Kind', 'Label', 'Direction', 'Leads', 'Qualified', 'Conversion'] as const\n\nconst LEAD_SOURCE_HELP = Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#lead-sources',\n excerpt:\n 'The leads first seen in the period by their lead source, and by the ' +\n 'direction each source is grouped under on the Fields page — Inbound, ' +\n 'Outbound or Unspecified — with how many of each were qualified.',\n})\n\nconst directionLabel = (direction: LeadSourceDirection | null): string =>\n direction ? LEAD_SOURCE_DIRECTION_LABELS[direction] : LEAD_SOURCE_UNSPECIFIED_LABEL\n\nexport interface LeadSourceCardProps {\n report: CrmReportScope\n /** The site whose leads are read, or `null` at the organization level — as the Lead funnel takes it. */\n hostId: string | null\n}\n\n/**\n * Lead sources (AGL-3511): of the leads first seen in the period, how many\n * came from each lead source and in each direction, and how many of each\n * were qualified.\n *\n * The SAME window the Lead funnel places — the period's newest leads by\n * `firstSeenAtMs`, under the same remembered key — so the two cards read\n * the period once between them and can never disagree about which leads\n * it holds. The lead source is grouped through the org's own list as it\n * stands now: a value moved to Outbound on the Fields page reports as\n * Outbound from that moment, for every lead that holds it.\n */\nexport function LeadSourceCard(props: LeadSourceCardProps) {\n const { report, hostId } = props\n const mount = useCrmOrgMount()\n if (hostId) return <SiteLeadSourceCard report={report} hostId={hostId} />\n if (mount && (!mount.hostsReady || mount.hosts.length > 0)) {\n return <OrgLeadSourceCard report={report} />\n }\n return (\n <CardDisplay header={'Lead sources'} help={LEAD_SOURCE_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}\nLeadSourceCard.displayName = 'LeadSourceCard'\n\n/** One site's window, under the Lead funnel's own key. */\nfunction SiteLeadSourceCard(props: { report: CrmReportScope; hostId: string }) {\n const { report, hostId } = props\n const { range } = report\n const firestore = useFirestore()\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 return <LeadSourceBody report={report} window={window} />\n}\nSiteLeadSourceCard.displayName = 'SiteLeadSourceCard'\n\n/** The organization's one unscoped window, under the Lead funnel's own key. */\nfunction OrgLeadSourceCard(props: { report: CrmReportScope }) {\n const { report } = props\n const { range } = report\n const firestore = useFirestore()\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 return <LeadSourceBody report={report} window={window} />\n}\nOrgLeadSourceCard.displayName = 'OrgLeadSourceCard'\n\n/** The card as drawn, whichever level fed it. Pure over the window and the org's list. */\nfunction LeadSourceBody(props: { report: CrmReportScope; window: WindowRead<LeadRow> }) {\n const { report, window } = props\n const { period } = report\n const { picklist } = useLeadSourcePicklist(report.scope[1])\n const breakdown = useMemo(() => leadsByLeadSource(window.rows, picklist), [window, picklist])\n const read = window.status === 'success'\n const direction = (id: LeadSourceDirection | null) =>\n breakdown.directions.find((row) => row.direction === id)\n const shareOf = (count: number | undefined): string | undefined =>\n read && breakdown.total > 0 && count !== undefined\n ? `${percent(count / breakdown.total)} of the period's leads`\n : undefined\n const sourceLabel = (label: string, listed: boolean) =>\n label ? (listed ? label : `${label} (not in the list)`) : 'No lead source'\n\n return (\n <CardDisplay\n header={'Lead sources'}\n help={LEAD_SOURCE_HELP}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('lead-sources', period)}\n columns={COLUMNS}\n rows={() => [\n ...breakdown.directions.map((row) => [\n 'Direction',\n directionLabel(row.direction),\n '',\n row.leads,\n row.qualified,\n row.leads ? percent(row.rate) : '',\n ]),\n ...breakdown.rows.map((row) => [\n 'Lead source',\n sourceLabel(row.label, row.listed),\n directionLabel(row.direction),\n row.leads,\n row.qualified,\n percent(row.rate),\n ]),\n ]}\n disabled={!read || !breakdown.total}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={LEAD_SOURCE_DIRECTION_LABELS.inbound}\n value={read ? (direction('inbound')?.leads ?? 0).toLocaleString() : null}\n note={shareOf(direction('inbound')?.leads)}\n />\n <ReportStatTile\n label={LEAD_SOURCE_DIRECTION_LABELS.outbound}\n value={read ? (direction('outbound')?.leads ?? 0).toLocaleString() : null}\n note={shareOf(direction('outbound')?.leads)}\n />\n <ReportStatTile\n label={LEAD_SOURCE_UNSPECIFIED_LABEL}\n value={read ? (direction(null)?.leads ?? 0).toLocaleString() : null}\n note={'no lead source, or one in no group'}\n />\n </Stack>\n {window.status === 'error' ? (\n <Alert severity=\"warning\">{'The period’s leads could not be read.'}</Alert>\n ) : null}\n <Section title={'By direction'}>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {['Direction', 'Leads', 'Qualified', 'Conversion'].map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {breakdown.directions.map((row) => (\n <TableRow key={row.direction ?? 'none'}>\n <TableCell>{directionLabel(row.direction)}</TableCell>\n <TableCell align=\"right\">{read ? row.leads.toLocaleString() : '—'}</TableCell>\n <TableCell align=\"right\">{read ? row.qualified.toLocaleString() : '—'}</TableCell>\n <TableCell align=\"right\">{read && row.leads ? percent(row.rate) : '—'}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Section>\n <Section title={'By lead source'}>\n {breakdown.rows.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {['Lead source', 'Direction', 'Leads', 'Qualified', 'Conversion'].map(\n (column, index) => (\n <TableCell key={column} align={index > 1 ? 'right' : 'left'}>\n {column}\n </TableCell>\n ),\n )}\n </TableRow>\n </TableHead>\n <TableBody>\n {breakdown.rows.map((row) => (\n <TableRow key={row.label || 'none'}>\n <TableCell>{sourceLabel(row.label, row.listed)}</TableCell>\n <TableCell>{directionLabel(row.direction)}</TableCell>\n <TableCell align=\"right\">{row.leads.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.qualified.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 {window.status === 'loading' ? 'Reading…' : 'No leads first seen in this period.'}\n </Typography>\n )}\n {read && window.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Read from the ${window.rows.length.toLocaleString()} most recently captured leads in the period.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nLeadSourceBody.displayName = 'LeadSourceBody'\n\nexport default LeadSourceCard\n"],"names":["Aglyn","CardDisplay","percent","Section","ScrollTable","ceilingedWindow","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getDocs","limit","orderBy","query","useMemo","useFirestore","useCrmOrgMount","useLeadSourcePicklist","LEAD_SOURCE_DIRECTION_LABELS","LEAD_SOURCE_UNSPECIFIED_LABEL","leadsByLeadSource","LEAD_CEILING","leadsBetween","ReportExport","reportFilename","reportCacheKey","ReportStatTile","useAggregateRead","useWindowRead","COLUMNS","LEAD_SOURCE_HELP","pluginDocsHelp","anchor","excerpt","directionLabel","direction","LeadSourceCard","props","report","hostId","mount","SiteLeadSourceCard","hostsReady","hosts","length","OrgLeadSourceCard","header","help","contentGutterX","contentGutterY","variant","color","displayName","range","firestore","window","scope","tokens","from","to","cacheKey","LeadSourceBody","merged","then","snapshot","docs","map","document","data","$id","id","rows","value","truncated","status","period","picklist","breakdown","read","directions","find","row","shareOf","count","total","undefined","sourceLabel","label","listed","HeaderProps","action","filename","columns","leads","qualified","rate","disabled","spacing","sx","flexWrap","inbound","toLocaleString","note","outbound","severity","title","size","column","index","align"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/lead-source-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 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 { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport { getDocs, limit, orderBy, query } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../../hooks/use-crm-org-mount'\nimport { useLeadSourcePicklist } from '../../hooks/use-lead-source-picklist'\nimport {\n LEAD_SOURCE_DIRECTION_LABELS,\n LEAD_SOURCE_UNSPECIFIED_LABEL,\n type LeadSourceDirection,\n} from '../../model/lead-source-direction'\nimport { leadsByLeadSource } from '../../model/crm-reports'\nimport { LEAD_CEILING, leadsBetween } from './lead-funnel-card'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport { type CrmReportScope, reportCacheKey } from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead, type WindowRead } from './use-aggregate-read'\n\ntype LeadRow = Record<string, unknown> & Aglyn.CrmLeadFields & { $id: string }\n\n/** The CSV's columns: one file for both tables. */\nconst COLUMNS = ['Kind', 'Label', 'Direction', 'Leads', 'Qualified', 'Conversion'] as const\n\nconst LEAD_SOURCE_HELP = Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#lead-sources',\n excerpt:\n 'The leads first seen in the period by their lead source, and by the ' +\n 'direction each source is grouped under on the Fields page — Inbound, ' +\n 'Outbound or Unspecified — with how many of each were qualified.',\n})\n\nconst directionLabel = (direction: LeadSourceDirection | null): string =>\n direction ? LEAD_SOURCE_DIRECTION_LABELS[direction] : LEAD_SOURCE_UNSPECIFIED_LABEL\n\nexport interface LeadSourceCardProps {\n report: CrmReportScope\n /** The site whose leads are read, or `null` at the organization level — as the Lead funnel takes it. */\n hostId: string | null\n}\n\n/**\n * Lead sources (AGL-3511): of the leads first seen in the period, how many\n * came from each lead source and in each direction, and how many of each\n * were qualified.\n *\n * The SAME window the Lead funnel places — the period's newest leads by\n * `firstSeenAtMs`, under the same remembered key — so the two cards read\n * the period once between them and can never disagree about which leads\n * it holds. The lead source is grouped through the org's own list as it\n * stands now: a value moved to Outbound on the Fields page reports as\n * Outbound from that moment, for every lead that holds it.\n */\nexport function LeadSourceCard(props: LeadSourceCardProps) {\n const { report, hostId } = props\n const mount = useCrmOrgMount()\n if (hostId) return <SiteLeadSourceCard report={report} hostId={hostId} />\n if (mount && (!mount.hostsReady || mount.hosts.length > 0)) {\n return <OrgLeadSourceCard report={report} />\n }\n return (\n <CardDisplay header={'Lead sources'} help={LEAD_SOURCE_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}\nLeadSourceCard.displayName = 'LeadSourceCard'\n\n/** One site's window, under the Lead funnel's own key. */\nfunction SiteLeadSourceCard(props: { report: CrmReportScope; hostId: string }) {\n const { report, hostId } = props\n const { range } = report\n const firestore = useFirestore()\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 return <LeadSourceBody report={report} window={window} />\n}\nSiteLeadSourceCard.displayName = 'SiteLeadSourceCard'\n\n/** The organization's one unscoped window, under the Lead funnel's own key. */\nfunction OrgLeadSourceCard(props: { report: CrmReportScope }) {\n const { report } = props\n const { range } = report\n const firestore = useFirestore()\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 return <LeadSourceBody report={report} window={window} />\n}\nOrgLeadSourceCard.displayName = 'OrgLeadSourceCard'\n\n/** The card as drawn, whichever level fed it. Pure over the window and the org's list. */\nfunction LeadSourceBody(props: { report: CrmReportScope; window: WindowRead<LeadRow> }) {\n const { report, window } = props\n const { period } = report\n const { picklist } = useLeadSourcePicklist(report.scope[1])\n const breakdown = useMemo(() => leadsByLeadSource(window.rows, picklist), [window, picklist])\n const read = window.status === 'success'\n const direction = (id: LeadSourceDirection | null) =>\n breakdown.directions.find((row) => row.direction === id)\n const shareOf = (count: number | undefined): string | undefined =>\n read && breakdown.total > 0 && count !== undefined\n ? `${percent(count / breakdown.total)} of the period's leads`\n : undefined\n const sourceLabel = (label: string, listed: boolean) =>\n label ? (listed ? label : `${label} (not in the list)`) : 'No lead source'\n\n return (\n <CardDisplay\n header={'Lead sources'}\n help={LEAD_SOURCE_HELP}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('lead-sources', period)}\n columns={COLUMNS}\n rows={() => [\n ...breakdown.directions.map((row) => [\n 'Direction',\n directionLabel(row.direction),\n '',\n row.leads,\n row.qualified,\n row.leads ? percent(row.rate) : '',\n ]),\n ...breakdown.rows.map((row) => [\n 'Lead source',\n sourceLabel(row.label, row.listed),\n directionLabel(row.direction),\n row.leads,\n row.qualified,\n percent(row.rate),\n ]),\n ]}\n disabled={!read || !breakdown.total}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={LEAD_SOURCE_DIRECTION_LABELS.inbound}\n value={read ? (direction('inbound')?.leads ?? 0).toLocaleString() : null}\n note={shareOf(direction('inbound')?.leads)}\n />\n <ReportStatTile\n label={LEAD_SOURCE_DIRECTION_LABELS.outbound}\n value={read ? (direction('outbound')?.leads ?? 0).toLocaleString() : null}\n note={shareOf(direction('outbound')?.leads)}\n />\n <ReportStatTile\n label={LEAD_SOURCE_UNSPECIFIED_LABEL}\n value={read ? (direction(null)?.leads ?? 0).toLocaleString() : null}\n note={'no lead source, or one in no group'}\n />\n </Stack>\n {window.status === 'error' ? (\n <Alert severity=\"warning\">{'The period’s leads could not be read.'}</Alert>\n ) : null}\n <Section title={'By direction'}>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {['Direction', 'Leads', 'Qualified', 'Conversion'].map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {breakdown.directions.map((row) => (\n <TableRow key={row.direction ?? 'none'}>\n <TableCell>{directionLabel(row.direction)}</TableCell>\n <TableCell align=\"right\">{read ? row.leads.toLocaleString() : '—'}</TableCell>\n <TableCell align=\"right\">{read ? row.qualified.toLocaleString() : '—'}</TableCell>\n <TableCell align=\"right\">{read && row.leads ? percent(row.rate) : '—'}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Section>\n <Section title={'By lead source'}>\n {breakdown.rows.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {['Lead source', 'Direction', 'Leads', 'Qualified', 'Conversion'].map(\n (column, index) => (\n <TableCell key={column} align={index > 1 ? 'right' : 'left'}>\n {column}\n </TableCell>\n ),\n )}\n </TableRow>\n </TableHead>\n <TableBody>\n {breakdown.rows.map((row) => (\n <TableRow key={row.label || 'none'}>\n <TableCell>{sourceLabel(row.label, row.listed)}</TableCell>\n <TableCell>{directionLabel(row.direction)}</TableCell>\n <TableCell align=\"right\">{row.leads.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.qualified.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 {window.status === 'loading' ? 'Reading…' : 'No leads first seen in this period.'}\n </Typography>\n )}\n {read && window.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Read from the ${window.rows.length.toLocaleString()} most recently captured leads in the period.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nLeadSourceBody.displayName = 'LeadSourceBody'\n\nexport default LeadSourceCard\n"],"names":["Aglyn","CardDisplay","percent","Section","ScrollTable","ceilingedWindow","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getDocs","limit","orderBy","query","useMemo","useFirestore","useCrmOrgMount","useLeadSourcePicklist","LEAD_SOURCE_DIRECTION_LABELS","LEAD_SOURCE_UNSPECIFIED_LABEL","leadsByLeadSource","LEAD_CEILING","leadsBetween","ReportExport","reportFilename","reportCacheKey","ReportStatTile","useAggregateRead","useWindowRead","COLUMNS","LEAD_SOURCE_HELP","pluginDocsHelp","anchor","excerpt","directionLabel","direction","LeadSourceCard","props","report","hostId","mount","SiteLeadSourceCard","hostsReady","hosts","length","OrgLeadSourceCard","header","help","contentGutterX","contentGutterY","variant","color","displayName","range","firestore","window","scope","tokens","from","to","cacheKey","LeadSourceBody","merged","then","snapshot","docs","map","document","data","$id","id","rows","value","truncated","status","period","picklist","breakdown","read","directions","find","row","shareOf","count","total","undefined","sourceLabel","label","listed","HeaderProps","action","filename","columns","leads","qualified","rate","disabled","spacing","useFlexGap","sx","flexWrap","inbound","toLocaleString","note","outbound","severity","title","size","column","index","align"],"mappings":"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"}
@@ -184,6 +184,7 @@ const TOP_DEALS = 10;
184
184
  spacing: 2,
185
185
  children: [
186
186
  /*#__PURE__*/ _jsxs(Stack, {
187
+ useFlexGap: true,
187
188
  direction: "row",
188
189
  spacing: 3,
189
190
  sx: {
@@ -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 { stuckDeals } from '../../model/crm-next-activity'\nimport { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { collectionCeiling } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename, shortDate } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { currencyOfDeals, pipelineTotals } from '../../model/crm-reports'\n\n/**\n * How many open deals the by-stage chart, the forecast and the top-deals\n * table are read from.\n *\n * The count and the face value of the pipeline are server aggregates and\n * never meet this bound. The forecast cannot be: it weights each deal by\n * ITS stage's odds, which is a join the server will not do, so it reads the\n * deals and says so when there were more than this.\n */\nconst OPEN_DEAL_CEILING = 1000\n/** More pipelines than this is a shape nobody has asked for; the probe says if it happens. */\nconst PIPELINE_CEILING = 20\nconst TOP_DEALS = 10\n\n/** The top-deals table's columns, which are also the CSV's — the amount split from its currency there. */\nconst TOP_DEAL_COLUMNS = ['Deal', 'Stage', 'Expected close', 'Amount', 'Currency'] as const\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\ntype PipelineRow = Aglyn.CrmPipeline & { $id: string }\n\nexport interface PipelineCardProps {\n report: CrmReportScope\n}\n\n/**\n * The open pipeline: how many deals, what they add up to, what they are\n * worth at their stage odds, where they sit, and the largest of them\n * (AGL-2604).\n *\n * Not period-scoped, deliberately. A pipeline is a stock, not a flow — what\n * is open NOW — and a \"pipeline over the last 30 days\" would be a report on\n * deals created in a window, which is a different question with a different\n * answer. The period picker governs the cards that measure flows.\n */\nexport function PipelineCard(props: PipelineCardProps) {\n const { report } = props\n const { scope, tokens, routes, nowMs } = report\n const firestore = useFirestore()\n\n const open = useAggregateRead(\n () =>\n getAggregateFromServer(\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n ),\n { count: count(), amountCents: sum('amountCents') },\n ).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:open') },\n )\n\n /**\n * The open deals, most recently updated first — the order the\n * `(visibleTo, status, updatedAt)` index the deals list already uses can\n * serve, so the report adds no index for this read.\n */\n const dealWindow = useWindowRead<DealRow>(\n () =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n orderBy('updatedAt', 'desc'),\n limit(OPEN_DEAL_CEILING + 1),\n ),\n OPEN_DEAL_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:deals') },\n )\n const pipelineWindow = useWindowRead<PipelineRow>(\n () =>\n collectionCeiling(\n query(\n scopedCollection(firestore, scope, 'pipelines'),\n ...visibleToClause(tokens),\n ),\n PIPELINE_CEILING,\n ),\n PIPELINE_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:pipelines') },\n )\n const dealsStatus = dealWindow.status\n\n const summary = useMemo(() => {\n const byPipeline = new Map<string, DealRow[]>()\n for (const deal of dealWindow.rows) {\n const key = deal.pipelineId ?? ''\n const list = byPipeline.get(key) ?? []\n list.push(deal)\n byPipeline.set(key, list)\n }\n // Every active pipeline is charted even with nothing open; an archived\n // one only while a deal still sits open in it — the forecast card's\n // rule, so the two cards name the same pipelines.\n const pipelines = pipelineWindow.rows\n .filter((pipeline) => !Aglyn.isPipelineArchived(pipeline) || byPipeline.has(pipeline.$id))\n .map((pipeline) => ({\n pipeline,\n totals: pipelineTotals(byPipeline.get(pipeline.$id) ?? [], pipeline),\n }))\n const known = new Set(pipelineWindow.rows.map((pipeline) => pipeline.$id))\n const orphans = dealWindow.rows.filter(\n (deal) => !known.has(deal.pipelineId ?? ''),\n )\n const stageName = (deal: DealRow): string =>\n Aglyn.dealStageById(\n pipelineWindow.rows.find((pipeline) => pipeline.$id === deal.pipelineId),\n deal.stageId,\n )?.name ?? deal.stageId\n const top = [...dealWindow.rows]\n .sort(\n (a, b) => Number(b.amountCents ?? 0) - Number(a.amountCents ?? 0),\n )\n .slice(0, TOP_DEALS)\n return {\n pipelines,\n orphanTotals: pipelineTotals(orphans, null),\n weightedCents: pipelines.reduce(\n (total, entry) => total + entry.totals.weightedCents,\n 0,\n ),\n top,\n stageName,\n currency: currencyOfDeals(dealWindow.rows),\n }\n }, [dealWindow, pipelineWindow])\n // Open deals with nothing scheduled (AGL-2661), off the same window.\n const stuck = useMemo(() => stuckDeals(dealWindow.rows), [dealWindow.rows])\n\n const currency = summary.currency.currency\n const dealsRead = dealsStatus === 'success'\n\n return (\n <CardDisplay\n header={'Pipeline'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#pipeline',\n excerpt:\n 'Every open deal: how many, what they add up to, and what they ' +\n 'are worth once each is weighted by the odds of its stage. Not ' +\n 'tied to the period — a pipeline is what is open now.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('top-open-deals')}\n columns={TOP_DEAL_COLUMNS}\n rows={() =>\n summary.top.map((deal) => [\n deal.title || deal.$id,\n summary.stageName(deal),\n typeof deal.expectedCloseAtMs === 'number' && deal.expectedCloseAtMs > 0\n ? new Date(deal.expectedCloseAtMs).toISOString().slice(0, 10)\n : '',\n (Number(deal.amountCents ?? 0) / 100).toFixed(2),\n String(deal.currency || currency).toUpperCase(),\n ])\n }\n disabled={!dealsRead || !summary.top.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open deals'}\n value={open.value ? open.value.count.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Pipeline value'}\n value={open.value ? money(open.value.amountCents, currency) : null}\n note={'face value of every open deal'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Weighted forecast'}\n value={dealsRead ? money(summary.weightedCents, currency) : null}\n note={\n dealWindow.truncated\n ? `from the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated`\n : 'each deal at its stage odds'\n }\n />\n {/*\n Open deals with no open task against them (AGL-2661): the ones\n nobody has a next step for. Painted as a warning above zero,\n because a deal with nothing planned is a deal going nowhere; the\n tile opens the deals list, whose \"No next activity\" chip lists\n them.\n */}\n <ReportStatTile\n label={'Stuck deals'}\n value={dealsRead ? stuck.count.toLocaleString() : null}\n note={'open, with no next activity'}\n riseIsGood={false}\n color={stuck.count > 0 ? 'warning.main' : undefined}\n href={routes.section('deals')}\n />\n </Stack>\n {open.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The deal totals could not be read.'}\n </Alert>\n ) : null}\n {summary.currency.mixed ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Deals are in more than one currency; the totals add them as numbers and show ${currency.toUpperCase()}, the most common.`}\n </Typography>\n ) : null}\n {dealsRead && !summary.pipelines.length && !dealWindow.rows.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No open deals yet.'}\n </Typography>\n ) : null}\n {summary.pipelines.map(({ pipeline, totals }) => (\n <Section key={pipeline.$id} title={pipeline.name || 'Pipeline'}>\n <ReportBreakdown\n rows={[\n ...totals.stages.map((row) => ({\n key: row.stage.id,\n label: row.stage.name,\n value: row.amountCents,\n display: money(row.amountCents, currency),\n note: `${plural(row.count, 'deal')} · ${money(row.weightedCents, currency)} weighted`,\n })),\n ...(totals.unplaced.count\n ? [\n {\n key: '$unplaced',\n label: 'Stage no longer in this pipeline',\n value: totals.unplaced.amountCents,\n display: money(totals.unplaced.amountCents, currency),\n note: `${plural(totals.unplaced.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]\n : []),\n ]}\n emptyText={'This pipeline has no open stages.'}\n />\n </Section>\n ))}\n {summary.orphanTotals.count ? (\n <Section title={'Deals in no pipeline'}>\n <ReportBreakdown\n rows={[\n {\n key: '$orphans',\n label: 'No pipeline',\n value: summary.orphanTotals.amountCents,\n display: money(summary.orphanTotals.amountCents, currency),\n note: `${plural(summary.orphanTotals.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]}\n emptyText={''}\n />\n </Section>\n ) : null}\n {pipelineWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Only the first ${PIPELINE_CEILING} pipelines are charted.`}\n </Typography>\n ) : null}\n {summary.top.length ? (\n <Section title={'Top open deals'}>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Deal'}</TableCell>\n <TableCell>{'Stage'}</TableCell>\n <TableCell>{'Expected close'}</TableCell>\n <TableCell align=\"right\">{'Amount'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {summary.top.map((deal) => (\n <TableRow key={deal.$id}>\n <TableCell>\n <AppLink href={routes.deal(deal.$id)}>\n {deal.title || deal.$id}\n </AppLink>\n </TableCell>\n <TableCell>{summary.stageName(deal)}</TableCell>\n <TableCell>{shortDate(deal.expectedCloseAtMs)}</TableCell>\n <TableCell align=\"right\">\n {money(\n Number(deal.amountCents ?? 0),\n String(deal.currency || currency),\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {/* What the ranking, and so the export, is drawn from when that is not everything. */}\n {dealWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Ranked within the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.`}\n </Typography>\n ) : null}\n </Section>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nPipelineCard.displayName = 'PipelineCard'\n\nexport default PipelineCard\n"],"names":["Aglyn","stuckDeals","money","AppLink","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","count","getAggregateFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","collectionCeiling","ReportBreakdown","ReportExport","plural","reportFilename","shortDate","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","currencyOfDeals","pipelineTotals","OPEN_DEAL_CEILING","PIPELINE_CEILING","TOP_DEALS","TOP_DEAL_COLUMNS","PipelineCard","props","report","scope","tokens","routes","nowMs","firestore","open","amountCents","then","snapshot","Number","data","cacheKey","dealWindow","pipelineWindow","dealsStatus","status","summary","byPipeline","Map","deal","rows","key","pipelineId","list","get","push","set","pipelines","filter","pipeline","isPipelineArchived","has","$id","map","totals","known","Set","orphans","stageName","dealStageById","find","stageId","name","top","sort","a","b","slice","orphanTotals","weightedCents","reduce","total","entry","currency","stuck","dealsRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","title","expectedCloseAtMs","Date","toISOString","toFixed","String","toUpperCase","disabled","length","spacing","direction","sx","flexWrap","label","value","toLocaleString","note","href","section","truncated","riseIsGood","color","undefined","severity","mixed","variant","stages","row","stage","id","display","unplaced","emptyText","size","align","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/pipeline-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { stuckDeals } from '../../model/crm-next-activity'\nimport { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { collectionCeiling } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename, shortDate } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { currencyOfDeals, pipelineTotals } from '../../model/crm-reports'\n\n/**\n * How many open deals the by-stage chart, the forecast and the top-deals\n * table are read from.\n *\n * The count and the face value of the pipeline are server aggregates and\n * never meet this bound. The forecast cannot be: it weights each deal by\n * ITS stage's odds, which is a join the server will not do, so it reads the\n * deals and says so when there were more than this.\n */\nconst OPEN_DEAL_CEILING = 1000\n/** More pipelines than this is a shape nobody has asked for; the probe says if it happens. */\nconst PIPELINE_CEILING = 20\nconst TOP_DEALS = 10\n\n/** The top-deals table's columns, which are also the CSV's — the amount split from its currency there. */\nconst TOP_DEAL_COLUMNS = ['Deal', 'Stage', 'Expected close', 'Amount', 'Currency'] as const\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\ntype PipelineRow = Aglyn.CrmPipeline & { $id: string }\n\nexport interface PipelineCardProps {\n report: CrmReportScope\n}\n\n/**\n * The open pipeline: how many deals, what they add up to, what they are\n * worth at their stage odds, where they sit, and the largest of them\n * (AGL-2604).\n *\n * Not period-scoped, deliberately. A pipeline is a stock, not a flow — what\n * is open NOW — and a \"pipeline over the last 30 days\" would be a report on\n * deals created in a window, which is a different question with a different\n * answer. The period picker governs the cards that measure flows.\n */\nexport function PipelineCard(props: PipelineCardProps) {\n const { report } = props\n const { scope, tokens, routes, nowMs } = report\n const firestore = useFirestore()\n\n const open = useAggregateRead(\n () =>\n getAggregateFromServer(\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n ),\n { count: count(), amountCents: sum('amountCents') },\n ).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:open') },\n )\n\n /**\n * The open deals, most recently updated first — the order the\n * `(visibleTo, status, updatedAt)` index the deals list already uses can\n * serve, so the report adds no index for this read.\n */\n const dealWindow = useWindowRead<DealRow>(\n () =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n orderBy('updatedAt', 'desc'),\n limit(OPEN_DEAL_CEILING + 1),\n ),\n OPEN_DEAL_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:deals') },\n )\n const pipelineWindow = useWindowRead<PipelineRow>(\n () =>\n collectionCeiling(\n query(\n scopedCollection(firestore, scope, 'pipelines'),\n ...visibleToClause(tokens),\n ),\n PIPELINE_CEILING,\n ),\n PIPELINE_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:pipelines') },\n )\n const dealsStatus = dealWindow.status\n\n const summary = useMemo(() => {\n const byPipeline = new Map<string, DealRow[]>()\n for (const deal of dealWindow.rows) {\n const key = deal.pipelineId ?? ''\n const list = byPipeline.get(key) ?? []\n list.push(deal)\n byPipeline.set(key, list)\n }\n // Every active pipeline is charted even with nothing open; an archived\n // one only while a deal still sits open in it — the forecast card's\n // rule, so the two cards name the same pipelines.\n const pipelines = pipelineWindow.rows\n .filter((pipeline) => !Aglyn.isPipelineArchived(pipeline) || byPipeline.has(pipeline.$id))\n .map((pipeline) => ({\n pipeline,\n totals: pipelineTotals(byPipeline.get(pipeline.$id) ?? [], pipeline),\n }))\n const known = new Set(pipelineWindow.rows.map((pipeline) => pipeline.$id))\n const orphans = dealWindow.rows.filter(\n (deal) => !known.has(deal.pipelineId ?? ''),\n )\n const stageName = (deal: DealRow): string =>\n Aglyn.dealStageById(\n pipelineWindow.rows.find((pipeline) => pipeline.$id === deal.pipelineId),\n deal.stageId,\n )?.name ?? deal.stageId\n const top = [...dealWindow.rows]\n .sort(\n (a, b) => Number(b.amountCents ?? 0) - Number(a.amountCents ?? 0),\n )\n .slice(0, TOP_DEALS)\n return {\n pipelines,\n orphanTotals: pipelineTotals(orphans, null),\n weightedCents: pipelines.reduce(\n (total, entry) => total + entry.totals.weightedCents,\n 0,\n ),\n top,\n stageName,\n currency: currencyOfDeals(dealWindow.rows),\n }\n }, [dealWindow, pipelineWindow])\n // Open deals with nothing scheduled (AGL-2661), off the same window.\n const stuck = useMemo(() => stuckDeals(dealWindow.rows), [dealWindow.rows])\n\n const currency = summary.currency.currency\n const dealsRead = dealsStatus === 'success'\n\n return (\n <CardDisplay\n header={'Pipeline'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#pipeline',\n excerpt:\n 'Every open deal: how many, what they add up to, and what they ' +\n 'are worth once each is weighted by the odds of its stage. Not ' +\n 'tied to the period — a pipeline is what is open now.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('top-open-deals')}\n columns={TOP_DEAL_COLUMNS}\n rows={() =>\n summary.top.map((deal) => [\n deal.title || deal.$id,\n summary.stageName(deal),\n typeof deal.expectedCloseAtMs === 'number' && deal.expectedCloseAtMs > 0\n ? new Date(deal.expectedCloseAtMs).toISOString().slice(0, 10)\n : '',\n (Number(deal.amountCents ?? 0) / 100).toFixed(2),\n String(deal.currency || currency).toUpperCase(),\n ])\n }\n disabled={!dealsRead || !summary.top.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open deals'}\n value={open.value ? open.value.count.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Pipeline value'}\n value={open.value ? money(open.value.amountCents, currency) : null}\n note={'face value of every open deal'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Weighted forecast'}\n value={dealsRead ? money(summary.weightedCents, currency) : null}\n note={\n dealWindow.truncated\n ? `from the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated`\n : 'each deal at its stage odds'\n }\n />\n {/*\n Open deals with no open task against them (AGL-2661): the ones\n nobody has a next step for. Painted as a warning above zero,\n because a deal with nothing planned is a deal going nowhere; the\n tile opens the deals list, whose \"No next activity\" chip lists\n them.\n */}\n <ReportStatTile\n label={'Stuck deals'}\n value={dealsRead ? stuck.count.toLocaleString() : null}\n note={'open, with no next activity'}\n riseIsGood={false}\n color={stuck.count > 0 ? 'warning.main' : undefined}\n href={routes.section('deals')}\n />\n </Stack>\n {open.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The deal totals could not be read.'}\n </Alert>\n ) : null}\n {summary.currency.mixed ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Deals are in more than one currency; the totals add them as numbers and show ${currency.toUpperCase()}, the most common.`}\n </Typography>\n ) : null}\n {dealsRead && !summary.pipelines.length && !dealWindow.rows.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No open deals yet.'}\n </Typography>\n ) : null}\n {summary.pipelines.map(({ pipeline, totals }) => (\n <Section key={pipeline.$id} title={pipeline.name || 'Pipeline'}>\n <ReportBreakdown\n rows={[\n ...totals.stages.map((row) => ({\n key: row.stage.id,\n label: row.stage.name,\n value: row.amountCents,\n display: money(row.amountCents, currency),\n note: `${plural(row.count, 'deal')} · ${money(row.weightedCents, currency)} weighted`,\n })),\n ...(totals.unplaced.count\n ? [\n {\n key: '$unplaced',\n label: 'Stage no longer in this pipeline',\n value: totals.unplaced.amountCents,\n display: money(totals.unplaced.amountCents, currency),\n note: `${plural(totals.unplaced.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]\n : []),\n ]}\n emptyText={'This pipeline has no open stages.'}\n />\n </Section>\n ))}\n {summary.orphanTotals.count ? (\n <Section title={'Deals in no pipeline'}>\n <ReportBreakdown\n rows={[\n {\n key: '$orphans',\n label: 'No pipeline',\n value: summary.orphanTotals.amountCents,\n display: money(summary.orphanTotals.amountCents, currency),\n note: `${plural(summary.orphanTotals.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]}\n emptyText={''}\n />\n </Section>\n ) : null}\n {pipelineWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Only the first ${PIPELINE_CEILING} pipelines are charted.`}\n </Typography>\n ) : null}\n {summary.top.length ? (\n <Section title={'Top open deals'}>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Deal'}</TableCell>\n <TableCell>{'Stage'}</TableCell>\n <TableCell>{'Expected close'}</TableCell>\n <TableCell align=\"right\">{'Amount'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {summary.top.map((deal) => (\n <TableRow key={deal.$id}>\n <TableCell>\n <AppLink href={routes.deal(deal.$id)}>\n {deal.title || deal.$id}\n </AppLink>\n </TableCell>\n <TableCell>{summary.stageName(deal)}</TableCell>\n <TableCell>{shortDate(deal.expectedCloseAtMs)}</TableCell>\n <TableCell align=\"right\">\n {money(\n Number(deal.amountCents ?? 0),\n String(deal.currency || currency),\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {/* What the ranking, and so the export, is drawn from when that is not everything. */}\n {dealWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Ranked within the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.`}\n </Typography>\n ) : null}\n </Section>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nPipelineCard.displayName = 'PipelineCard'\n\nexport default PipelineCard\n"],"names":["Aglyn","stuckDeals","money","AppLink","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","count","getAggregateFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","collectionCeiling","ReportBreakdown","ReportExport","plural","reportFilename","shortDate","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","currencyOfDeals","pipelineTotals","OPEN_DEAL_CEILING","PIPELINE_CEILING","TOP_DEALS","TOP_DEAL_COLUMNS","PipelineCard","props","report","scope","tokens","routes","nowMs","firestore","open","amountCents","then","snapshot","Number","data","cacheKey","dealWindow","pipelineWindow","dealsStatus","status","summary","byPipeline","Map","deal","rows","key","pipelineId","list","get","push","set","pipelines","filter","pipeline","isPipelineArchived","has","$id","map","totals","known","Set","orphans","stageName","dealStageById","find","stageId","name","top","sort","a","b","slice","orphanTotals","weightedCents","reduce","total","entry","currency","stuck","dealsRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","title","expectedCloseAtMs","Date","toISOString","toFixed","String","toUpperCase","disabled","length","spacing","useFlexGap","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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"}
@@ -121,6 +121,7 @@ import { conversionBySource } from "../../model/crm-reports.js";
121
121
  spacing: 2,
122
122
  children: [
123
123
  /*#__PURE__*/ _jsxs(Stack, {
124
+ useFlexGap: true,
124
125
  direction: "row",
125
126
  spacing: 3,
126
127
  sx: {
@@ -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'\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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"}
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 useFlexGap 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","useFlexGap","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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"}
@@ -190,6 +190,7 @@ import { localDayBounds } from "../../model/crm-reports.js";
190
190
  spacing: 2,
191
191
  children: [
192
192
  /*#__PURE__*/ _jsxs(Stack, {
193
+ useFlexGap: true,
193
194
  direction: "row",
194
195
  spacing: 3,
195
196
  sx: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/tasks-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 { 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 Button,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n documentId,\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 { 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 { localDayBounds } from '../../model/crm-reports'\n\n/**\n * How many open tasks the by-assignee table is grouped from.\n *\n * The tiles are counts the server takes; the table needs each task's\n * assignee and due state, so it reads the soonest-due thousand and says\n * when there were more.\n */\nconst OPEN_TASK_CEILING = 1000\n/** One `in` query's worth of member documents, for the assignee names. */\nconst ASSIGNEE_NAME_CEILING = 30\n\ntype TaskRow = Aglyn.CrmTask & { $id: string }\n\n/** The by-assignee table's columns, which are also the CSV's. */\nconst ASSIGNEE_COLUMNS = ['Assignee', 'Overdue', 'Today', 'Upcoming', 'No date', 'Open'] as const\n\ninterface AssigneeLoad {\n uid: string\n overdue: number\n today: number\n upcoming: number\n undated: number\n open: number\n}\n\nexport interface TasksCardProps {\n report: CrmReportScope\n}\n\n/**\n * The task load: open, overdue and due today, and who is carrying what\n * (AGL-2604).\n *\n * \"Today\" and \"overdue\" are decided on the LOCAL calendar day, the same day\n * `taskDueState` decides them on, so the count here and the chip on the\n * tasks list agree about the same task at 11 p.m. The counts are range\n * queries on `dueAtMs`, which the `(visibleTo, status, dueAtMs)` index\n * serves; a task with no due date is in the open count and in none of the\n * dated ones.\n *\n * The table reads open tasks ordered by due date. An ordered read admits\n * only documents that CARRY the field, so a task written without `dueAtMs`\n * at all — rather than with `null`, which sorts first — is counted in the\n * tile and absent from the table. The model types the field as optional,\n * so the table says what it is grouped from.\n */\nexport function TasksCard(props: TasksCardProps) {\n const { report } = props\n const { scope, tokens, nowMs, routes } = report\n const firestore = useFirestore()\n const day = useMemo(() => localDayBounds(nowMs), [nowMs])\n\n const openTasks = () =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n )\n\n const counts = useAggregateRead(\n () =>\n Promise.all([\n getCountFromServer(openTasks()),\n getCountFromServer(query(openTasks(), where('dueAtMs', '<', day.start))),\n getCountFromServer(\n query(\n openTasks(),\n where('dueAtMs', '>=', day.start),\n where('dueAtMs', '<', day.end),\n ),\n ),\n ]).then(([open, overdue, today]) => ({\n open: open.data().count,\n overdue: overdue.data().count,\n today: today.data().count,\n })),\n [firestore, scope, tokens, day],\n { cacheKey: reportCacheKey(report, 'tasks:counts') },\n )\n\n const taskWindow = useWindowRead<TaskRow>(\n () =>\n query(\n openTasks(),\n orderBy('dueAtMs', 'asc'),\n limit(OPEN_TASK_CEILING + 1),\n ),\n OPEN_TASK_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'tasks:open') },\n )\n const tasksStatus = taskWindow.status\n\n const load = useMemo(() => {\n const byAssignee = new Map<string, AssigneeLoad>()\n for (const task of taskWindow.rows) {\n const uid = task.assigneeUid || ''\n const row = byAssignee.get(uid) ?? {\n uid,\n overdue: 0,\n today: 0,\n upcoming: 0,\n undated: 0,\n open: 0,\n }\n const state = Aglyn.taskDueState(task, nowMs)\n if (state === 'overdue') row.overdue += 1\n else if (state === 'today') row.today += 1\n else if (state === 'upcoming') row.upcoming += 1\n else row.undated += 1\n row.open += 1\n byAssignee.set(uid, row)\n }\n return [...byAssignee.values()].sort(\n (a, b) => b.open - a.open || a.uid.localeCompare(b.uid),\n )\n }, [taskWindow, nowMs])\n\n /*\n * The assignees' names, from their member documents — one `in` query for\n * up to thirty, which is Firestore's cap on the operator and more assignees\n * than a tasks list has. Anyone past it, or a member document the reader\n * cannot open, shows as their id rather than blocking the table.\n */\n const uids = load.map((row) => row.uid).filter(Boolean).slice(0, ASSIGNEE_NAME_CEILING)\n const uidKey = uids.join(',')\n const names = useAggregateRead<Record<string, string>>(\n () =>\n uids.length\n ? getDocs(\n query(\n scopedCollection(firestore, scope, 'members'),\n where(documentId(), 'in', uids),\n ),\n ).then((snapshot) =>\n Object.fromEntries(\n snapshot.docs.map((member) => [\n member.id,\n String(member.get('displayName') || member.get('email') || member.id),\n ]),\n ),\n )\n : Promise.resolve({}),\n [firestore, scope, uidKey],\n { cacheKey: reportCacheKey(report, `tasks:names:${uidKey}`) },\n )\n\n const figures = counts.value\n const assigneeName = (row: AssigneeLoad): string =>\n row.uid ? names.value?.[row.uid] ?? row.uid : 'Unassigned'\n return (\n <CardDisplay\n header={'Tasks'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#tasks',\n excerpt:\n 'Open tasks, how many are overdue or due today, and the open ' +\n 'load per assignee. Today and overdue are decided on your calendar ' +\n 'day, the same way the tasks list decides them.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <ReportExport\n filename={reportFilename('tasks-by-assignee')}\n columns={ASSIGNEE_COLUMNS}\n rows={() =>\n load.map((row) => [\n assigneeName(row),\n row.overdue,\n row.today,\n row.upcoming,\n row.undated,\n row.open,\n ])\n }\n disabled={tasksStatus !== 'success' || !load.length}\n />\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('tasks')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open tasks'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open tasks'}\n value={figures ? figures.open.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Overdue'}\n value={figures ? figures.overdue.toLocaleString() : null}\n note={'due before today'}\n color={figures?.overdue ? 'error.main' : undefined}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Due today'}\n value={figures ? figures.today.toLocaleString() : null}\n note={'on your calendar day'}\n href={routes.section('tasks')}\n />\n </Stack>\n {counts.status === 'error' ? (\n <Alert severity=\"warning\">{'The task counts could not be read.'}</Alert>\n ) : null}\n <Section title={'Open tasks by assignee'}>\n {load.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {ASSIGNEE_COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {load.map((row) => (\n <TableRow key={row.uid || '$unassigned'}>\n <TableCell>{assigneeName(row)}</TableCell>\n <TableCell\n align=\"right\"\n sx={row.overdue ? { color: 'error.main' } : undefined}\n >\n {row.overdue.toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">{row.today.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.upcoming.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.undated.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.open.toLocaleString()}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {tasksStatus === 'loading' ? 'Reading…' : 'No open tasks.'}\n </Typography>\n )}\n {/* What the table, and so the export, is grouped from when that is not everything. */}\n {taskWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Grouped from the ${OPEN_TASK_CEILING.toLocaleString()} soonest-due open tasks; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nTasksCard.displayName = 'TasksCard'\n\nexport default TasksCard\n"],"names":["Aglyn","AppLink","CardDisplay","Section","ScrollTable","Alert","Button","Stack","TableBody","TableCell","TableHead","TableRow","Typography","documentId","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","localDayBounds","OPEN_TASK_CEILING","ASSIGNEE_NAME_CEILING","ASSIGNEE_COLUMNS","TasksCard","props","report","scope","tokens","nowMs","routes","firestore","day","openTasks","CRM_COLLECTIONS","tasks","counts","Promise","all","start","end","then","open","overdue","today","data","count","cacheKey","taskWindow","tasksStatus","status","load","byAssignee","Map","task","rows","uid","assigneeUid","row","get","upcoming","undated","state","taskDueState","set","values","sort","a","b","localeCompare","uids","map","filter","Boolean","slice","uidKey","join","names","length","snapshot","Object","fromEntries","docs","member","id","String","resolve","figures","value","assigneeName","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","filename","columns","disabled","component","componentVariant","nativeButton","href","section","size","color","sx","flexWrap","label","toLocaleString","note","undefined","severity","title","column","index","align","variant","truncated","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/tasks-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 { 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 Button,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n documentId,\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 { 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 { localDayBounds } from '../../model/crm-reports'\n\n/**\n * How many open tasks the by-assignee table is grouped from.\n *\n * The tiles are counts the server takes; the table needs each task's\n * assignee and due state, so it reads the soonest-due thousand and says\n * when there were more.\n */\nconst OPEN_TASK_CEILING = 1000\n/** One `in` query's worth of member documents, for the assignee names. */\nconst ASSIGNEE_NAME_CEILING = 30\n\ntype TaskRow = Aglyn.CrmTask & { $id: string }\n\n/** The by-assignee table's columns, which are also the CSV's. */\nconst ASSIGNEE_COLUMNS = ['Assignee', 'Overdue', 'Today', 'Upcoming', 'No date', 'Open'] as const\n\ninterface AssigneeLoad {\n uid: string\n overdue: number\n today: number\n upcoming: number\n undated: number\n open: number\n}\n\nexport interface TasksCardProps {\n report: CrmReportScope\n}\n\n/**\n * The task load: open, overdue and due today, and who is carrying what\n * (AGL-2604).\n *\n * \"Today\" and \"overdue\" are decided on the LOCAL calendar day, the same day\n * `taskDueState` decides them on, so the count here and the chip on the\n * tasks list agree about the same task at 11 p.m. The counts are range\n * queries on `dueAtMs`, which the `(visibleTo, status, dueAtMs)` index\n * serves; a task with no due date is in the open count and in none of the\n * dated ones.\n *\n * The table reads open tasks ordered by due date. An ordered read admits\n * only documents that CARRY the field, so a task written without `dueAtMs`\n * at all — rather than with `null`, which sorts first — is counted in the\n * tile and absent from the table. The model types the field as optional,\n * so the table says what it is grouped from.\n */\nexport function TasksCard(props: TasksCardProps) {\n const { report } = props\n const { scope, tokens, nowMs, routes } = report\n const firestore = useFirestore()\n const day = useMemo(() => localDayBounds(nowMs), [nowMs])\n\n const openTasks = () =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n )\n\n const counts = useAggregateRead(\n () =>\n Promise.all([\n getCountFromServer(openTasks()),\n getCountFromServer(query(openTasks(), where('dueAtMs', '<', day.start))),\n getCountFromServer(\n query(\n openTasks(),\n where('dueAtMs', '>=', day.start),\n where('dueAtMs', '<', day.end),\n ),\n ),\n ]).then(([open, overdue, today]) => ({\n open: open.data().count,\n overdue: overdue.data().count,\n today: today.data().count,\n })),\n [firestore, scope, tokens, day],\n { cacheKey: reportCacheKey(report, 'tasks:counts') },\n )\n\n const taskWindow = useWindowRead<TaskRow>(\n () =>\n query(\n openTasks(),\n orderBy('dueAtMs', 'asc'),\n limit(OPEN_TASK_CEILING + 1),\n ),\n OPEN_TASK_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'tasks:open') },\n )\n const tasksStatus = taskWindow.status\n\n const load = useMemo(() => {\n const byAssignee = new Map<string, AssigneeLoad>()\n for (const task of taskWindow.rows) {\n const uid = task.assigneeUid || ''\n const row = byAssignee.get(uid) ?? {\n uid,\n overdue: 0,\n today: 0,\n upcoming: 0,\n undated: 0,\n open: 0,\n }\n const state = Aglyn.taskDueState(task, nowMs)\n if (state === 'overdue') row.overdue += 1\n else if (state === 'today') row.today += 1\n else if (state === 'upcoming') row.upcoming += 1\n else row.undated += 1\n row.open += 1\n byAssignee.set(uid, row)\n }\n return [...byAssignee.values()].sort(\n (a, b) => b.open - a.open || a.uid.localeCompare(b.uid),\n )\n }, [taskWindow, nowMs])\n\n /*\n * The assignees' names, from their member documents — one `in` query for\n * up to thirty, which is Firestore's cap on the operator and more assignees\n * than a tasks list has. Anyone past it, or a member document the reader\n * cannot open, shows as their id rather than blocking the table.\n */\n const uids = load.map((row) => row.uid).filter(Boolean).slice(0, ASSIGNEE_NAME_CEILING)\n const uidKey = uids.join(',')\n const names = useAggregateRead<Record<string, string>>(\n () =>\n uids.length\n ? getDocs(\n query(\n scopedCollection(firestore, scope, 'members'),\n where(documentId(), 'in', uids),\n ),\n ).then((snapshot) =>\n Object.fromEntries(\n snapshot.docs.map((member) => [\n member.id,\n String(member.get('displayName') || member.get('email') || member.id),\n ]),\n ),\n )\n : Promise.resolve({}),\n [firestore, scope, uidKey],\n { cacheKey: reportCacheKey(report, `tasks:names:${uidKey}`) },\n )\n\n const figures = counts.value\n const assigneeName = (row: AssigneeLoad): string =>\n row.uid ? names.value?.[row.uid] ?? row.uid : 'Unassigned'\n return (\n <CardDisplay\n header={'Tasks'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#tasks',\n excerpt:\n 'Open tasks, how many are overdue or due today, and the open ' +\n 'load per assignee. Today and overdue are decided on your calendar ' +\n 'day, the same way the tasks list decides them.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <ReportExport\n filename={reportFilename('tasks-by-assignee')}\n columns={ASSIGNEE_COLUMNS}\n rows={() =>\n load.map((row) => [\n assigneeName(row),\n row.overdue,\n row.today,\n row.upcoming,\n row.undated,\n row.open,\n ])\n }\n disabled={tasksStatus !== 'success' || !load.length}\n />\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('tasks')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open tasks'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open tasks'}\n value={figures ? figures.open.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Overdue'}\n value={figures ? figures.overdue.toLocaleString() : null}\n note={'due before today'}\n color={figures?.overdue ? 'error.main' : undefined}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Due today'}\n value={figures ? figures.today.toLocaleString() : null}\n note={'on your calendar day'}\n href={routes.section('tasks')}\n />\n </Stack>\n {counts.status === 'error' ? (\n <Alert severity=\"warning\">{'The task counts could not be read.'}</Alert>\n ) : null}\n <Section title={'Open tasks by assignee'}>\n {load.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {ASSIGNEE_COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {load.map((row) => (\n <TableRow key={row.uid || '$unassigned'}>\n <TableCell>{assigneeName(row)}</TableCell>\n <TableCell\n align=\"right\"\n sx={row.overdue ? { color: 'error.main' } : undefined}\n >\n {row.overdue.toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">{row.today.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.upcoming.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.undated.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.open.toLocaleString()}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {tasksStatus === 'loading' ? 'Reading…' : 'No open tasks.'}\n </Typography>\n )}\n {/* What the table, and so the export, is grouped from when that is not everything. */}\n {taskWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Grouped from the ${OPEN_TASK_CEILING.toLocaleString()} soonest-due open tasks; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nTasksCard.displayName = 'TasksCard'\n\nexport default TasksCard\n"],"names":["Aglyn","AppLink","CardDisplay","Section","ScrollTable","Alert","Button","Stack","TableBody","TableCell","TableHead","TableRow","Typography","documentId","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","localDayBounds","OPEN_TASK_CEILING","ASSIGNEE_NAME_CEILING","ASSIGNEE_COLUMNS","TasksCard","props","report","scope","tokens","nowMs","routes","firestore","day","openTasks","CRM_COLLECTIONS","tasks","counts","Promise","all","start","end","then","open","overdue","today","data","count","cacheKey","taskWindow","tasksStatus","status","load","byAssignee","Map","task","rows","uid","assigneeUid","row","get","upcoming","undated","state","taskDueState","set","values","sort","a","b","localeCompare","uids","map","filter","Boolean","slice","uidKey","join","names","length","snapshot","Object","fromEntries","docs","member","id","String","resolve","figures","value","assigneeName","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","filename","columns","disabled","component","componentVariant","nativeButton","href","section","size","color","useFlexGap","sx","flexWrap","label","toLocaleString","note","undefined","severity","title","column","index","align","variant","truncated","displayName"],"mappings":"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"}
@@ -162,6 +162,7 @@ import { CRM_REPORT_PERIOD_LABELS, CRM_REPORT_PERIODS, crmReportRange } from "..
162
162
  spacing: 2,
163
163
  children: [
164
164
  /*#__PURE__*/ _jsxs(Stack, {
165
+ useFlexGap: true,
165
166
  direction: "row",
166
167
  spacing: 2,
167
168
  sx: {
@@ -176,6 +177,7 @@ import { CRM_REPORT_PERIOD_LABELS, CRM_REPORT_PERIODS, crmReportRange } from "..
176
177
  children: 'Reports'
177
178
  }),
178
179
  /*#__PURE__*/ _jsxs(Stack, {
180
+ useFlexGap: true,
179
181
  direction: "row",
180
182
  spacing: 1,
181
183
  sx: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/reports-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { mdiRefresh } from '@aglyn/shared-data-mdi'\nimport {\n MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { CardColumns } from '@aglyn/shared-ui-jsx/components/card-columns'\nimport {\n Button,\n Stack,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport { getCountFromServer, query } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { crmRoutes } from '../model/crm-routes'\nimport { ActivityCard } from './reports/activity-card'\nimport { ClosedDealsCard } from './reports/closed-deals-card'\nimport { ContactsMixCard } from './reports/contacts-mix-card'\nimport { ContactsTrendCard } from './reports/contacts-trend-card'\nimport { ForecastCard } from './reports/forecast-card'\nimport { LeadFunnelCard } from './reports/lead-funnel-card'\nimport { LeadSourceCard } from './reports/lead-source-card'\nimport { PipelineCard } from './reports/pipeline-card'\nimport { SourceConversionCard } from './reports/source-conversion-card'\nimport { WonLostByOwnerCard } from './reports/won-lost-by-owner-card'\nimport {\n type CrmReportScope,\n reportCacheKey,\n reportCachePrefix,\n scopedCollection,\n visibleToClause,\n} from './reports/report-scope'\nimport { TasksCard } from './reports/tasks-card'\nimport { invalidateAggregateReads, useAggregateRead } from './reports/use-aggregate-read'\nimport {\n CRM_REPORT_PERIOD_LABELS,\n CRM_REPORT_PERIODS,\n type CrmReportPeriod,\n crmReportRange,\n} from '../model/crm-reports'\n\n/**\n * `/crm/reports` — the CRM in aggregate (AGL-2604, AGL-2624).\n *\n * Nine cards, each reading its own collection: contacts over time, contacts\n * by source and stage, which sources convert, this site's lead funnel, the\n * open pipeline, what closed, who logged what, the forecast by close month\n * (AGL-2620, over the pipeline card's own window), and the task load. This\n * section resolves what they share and hands it down — see `CrmReportScope`\n * — so that every card counts the same reader's records over the same\n * clock. The lead funnel is the one card handed the site as well: leads live\n * under the host, not the org.\n *\n * ## The period is a control, and the reads follow it\n *\n * Nothing on this page reads a year on mount. The picker names the period,\n * the period sizes every read (one count per week of it, the deals closed\n * within it), and the default is thirty days. The three cards that are\n * stocks rather than flows — the open pipeline, its forecast, the open\n * tasks — ignore the period because \"what is open\" has no period.\n *\n * ## Scoped like the list\n *\n * Every query carries `visibleTo array-contains-any` over the group's\n * tokens, the predicate the rules evaluate, so a report can never count a\n * record its reader could not open — and the total here is the reader's\n * total, not the org's billable audience, which is the head-count the\n * contacts list keeps for the quota.\n *\n * ## Read once a minute, not once an arrival\n *\n * Every read on this page is keyed by the scope and the period and kept\n * for `AGGREGATE_READ_TTL_MS` — see `useAggregateRead` — so leaving to open\n * a deal the report named and coming straight back costs nothing. Refresh\n * forgets this reader's answers for this org and anchors the period again,\n * which is what makes every card ask afresh.\n */\nexport function ContactsReportsSection(props: ConsolePluginPageProps) {\n const { hostId, org, basePath } = props\n const firestore = useFirestore()\n // The org data root — null until the org lookup settles, and the page\n // reads nothing until it does — and the controller this page is viewed\n // as, resolved the way every CRM surface resolves them so the facet a\n // stage is read from here is the facet the contacts list edits it in.\n const { scope: dataScope, consentGroup, visibleTo } = useCrmScope({ hostId, org })\n // `null` at the organization level (AGL-2630): no clause, and every card\n // totals every site; the holder is then each contact's own.\n const tokens = useMemo(() => (visibleTo ? [...visibleTo] : null), [visibleTo])\n const groupId = consentGroup?.groupId ?? null\n const orgRecord = (org ?? null) as Record<string, unknown> | null\n /*\n * The period AND the moment it was chosen, as one value: the range is\n * anchored when the reader picks, not re-derived from a moving `Date.now()`\n * per render, which would rebuild every query on every pass.\n */\n const [view, setView] = useState<{\n period: CrmReportPeriod\n nowMs: number\n }>(() => ({ period: '30d', nowMs: Date.now() }))\n const range = useMemo(\n () => crmReportRange(view.period, view.nowMs),\n [view],\n )\n const routes = useMemo(() => crmRoutes(basePath ?? ''), [basePath])\n const refresh = useCallback(() => {\n if (dataScope) {\n invalidateAggregateReads(reportCachePrefix({ scope: dataScope, tokens, groupId }))\n }\n // A new anchor is what changes every card's dependencies, so each read\n // runs again and — its key forgotten — reaches the server.\n setView((current) => ({ period: current.period, nowMs: Date.now() }))\n }, [dataScope, tokens, groupId])\n\n const report = useMemo<CrmReportScope | null>(\n () =>\n dataScope\n ? {\n scope: dataScope,\n tokens,\n groupId,\n org: orgRecord,\n period: view.period,\n range,\n nowMs: view.nowMs,\n routes,\n }\n : null,\n [dataScope, tokens, groupId, orgRecord, view, range, routes],\n )\n\n /**\n * Every contact this reader may see, counted once for the two cards that\n * quote it — the tile, and the sample notice that compares the read to it.\n */\n const totalContacts = useAggregateRead<number>(\n () =>\n dataScope\n ? getCountFromServer(\n query(\n scopedCollection(firestore, dataScope, 'contacts'),\n ...visibleToClause(tokens),\n ),\n ).then((snapshot) => snapshot.data().count)\n : null,\n // The anchor is a dependency so Refresh re-asks; the key, which omits\n // it, is what answers a reopened section from memory.\n [firestore, dataScope, tokens, view.nowMs],\n report ? { cacheKey: reportCacheKey(report, 'contacts:total') } : {},\n )\n\n return (\n <Stack spacing={2}>\n <Stack\n direction=\"row\"\n spacing={2}\n sx={{\n alignItems: 'center',\n justifyContent: 'space-between',\n flexWrap: 'wrap',\n }}\n >\n <Typography variant=\"h6\" component=\"h2\">\n {'Reports'}\n </Typography>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}>\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={view.period}\n onChange={(_event, next) => {\n if (next) {\n setView({ period: next as CrmReportPeriod, nowMs: Date.now() })\n }\n }}\n aria-label=\"Report period\"\n >\n {CRM_REPORT_PERIODS.map((period) => (\n <ToggleButton key={period} value={period}>\n {CRM_REPORT_PERIOD_LABELS[period]}\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiRefresh.path} size={0.8} />}\n disabled={!report}\n onClick={refresh}\n >\n {'Refresh'}\n </Button>\n </Stack>\n </Stack>\n {report ? (\n <>\n {/*\n The same masonry the billing and staff pages use: columns as tall\n as their own contents, so a short card beside a tall one leaves no\n hole. Measured live before this changed — 271, 272 and 111 pixels\n of dead space down one report, and an odd card stranded with an\n empty column beside it.\n */}\n <CardColumns\n spacing={2}\n items={[\n {\n key: 'trend',\n children: (\n <ContactsTrendCard report={report} totalContacts={totalContacts} />\n ),\n },\n {\n key: 'mix',\n children: (\n <ContactsMixCard report={report} totalContacts={totalContacts} />\n ),\n },\n { key: 'source-conversion', children: <SourceConversionCard report={report} /> },\n { key: 'lead-funnel', children: <LeadFunnelCard report={report} hostId={hostId} /> },\n { key: 'lead-sources', children: <LeadSourceCard report={report} hostId={hostId} /> },\n { key: 'pipeline', children: <PipelineCard report={report} /> },\n {\n key: 'closed',\n /*\n One column item carrying two cards, because multicol picks\n its own break: two consecutive items are only USUALLY\n adjacent, and a card that is a footnote to the one above it\n cannot be left to that. Won and lost by owner asks a second\n question of the very rows Won and lost already read — the\n same window under the same `closed:won` and `closed:lost`\n keys — so it is read as that card's breakdown, and\n `break-inside: avoid` keeps the pair whole in one column.\n */\n children: (\n <Stack spacing={2}>\n <ClosedDealsCard report={report} />\n <WonLostByOwnerCard report={report} />\n </Stack>\n ),\n },\n ]}\n />\n {/*\n Outside the columns: multicol cannot span, and these three read\n across the full width.\n */}\n <Stack spacing={2}>\n <ActivityCard report={report} />\n <ForecastCard report={report} />\n <TasksCard report={report} />\n </Stack>\n </>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n )}\n </Stack>\n )\n}\nContactsReportsSection.displayName = 'ContactsReportsSection'\n\nexport default ContactsReportsSection\n"],"names":["mdiRefresh","MdiIcon","CardColumns","Button","Stack","ToggleButton","ToggleButtonGroup","Typography","getCountFromServer","query","useCallback","useMemo","useState","useFirestore","useCrmScope","crmRoutes","ActivityCard","ClosedDealsCard","ContactsMixCard","ContactsTrendCard","ForecastCard","LeadFunnelCard","LeadSourceCard","PipelineCard","SourceConversionCard","WonLostByOwnerCard","reportCacheKey","reportCachePrefix","scopedCollection","visibleToClause","TasksCard","invalidateAggregateReads","useAggregateRead","CRM_REPORT_PERIOD_LABELS","CRM_REPORT_PERIODS","crmReportRange","ContactsReportsSection","props","hostId","org","basePath","firestore","scope","dataScope","consentGroup","visibleTo","tokens","groupId","orgRecord","view","setView","period","nowMs","Date","now","range","routes","refresh","current","report","totalContacts","then","snapshot","data","count","cacheKey","spacing","direction","sx","alignItems","justifyContent","flexWrap","variant","component","rowGap","exclusive","size","color","value","onChange","_event","next","aria-label","map","startIcon","path","disabled","onClick","items","key","children","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/reports-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { mdiRefresh } from '@aglyn/shared-data-mdi'\nimport {\n MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { CardColumns } from '@aglyn/shared-ui-jsx/components/card-columns'\nimport {\n Button,\n Stack,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport { getCountFromServer, query } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { crmRoutes } from '../model/crm-routes'\nimport { ActivityCard } from './reports/activity-card'\nimport { ClosedDealsCard } from './reports/closed-deals-card'\nimport { ContactsMixCard } from './reports/contacts-mix-card'\nimport { ContactsTrendCard } from './reports/contacts-trend-card'\nimport { ForecastCard } from './reports/forecast-card'\nimport { LeadFunnelCard } from './reports/lead-funnel-card'\nimport { LeadSourceCard } from './reports/lead-source-card'\nimport { PipelineCard } from './reports/pipeline-card'\nimport { SourceConversionCard } from './reports/source-conversion-card'\nimport { WonLostByOwnerCard } from './reports/won-lost-by-owner-card'\nimport {\n type CrmReportScope,\n reportCacheKey,\n reportCachePrefix,\n scopedCollection,\n visibleToClause,\n} from './reports/report-scope'\nimport { TasksCard } from './reports/tasks-card'\nimport { invalidateAggregateReads, useAggregateRead } from './reports/use-aggregate-read'\nimport {\n CRM_REPORT_PERIOD_LABELS,\n CRM_REPORT_PERIODS,\n type CrmReportPeriod,\n crmReportRange,\n} from '../model/crm-reports'\n\n/**\n * `/crm/reports` — the CRM in aggregate (AGL-2604, AGL-2624).\n *\n * Nine cards, each reading its own collection: contacts over time, contacts\n * by source and stage, which sources convert, this site's lead funnel, the\n * open pipeline, what closed, who logged what, the forecast by close month\n * (AGL-2620, over the pipeline card's own window), and the task load. This\n * section resolves what they share and hands it down — see `CrmReportScope`\n * — so that every card counts the same reader's records over the same\n * clock. The lead funnel is the one card handed the site as well: leads live\n * under the host, not the org.\n *\n * ## The period is a control, and the reads follow it\n *\n * Nothing on this page reads a year on mount. The picker names the period,\n * the period sizes every read (one count per week of it, the deals closed\n * within it), and the default is thirty days. The three cards that are\n * stocks rather than flows — the open pipeline, its forecast, the open\n * tasks — ignore the period because \"what is open\" has no period.\n *\n * ## Scoped like the list\n *\n * Every query carries `visibleTo array-contains-any` over the group's\n * tokens, the predicate the rules evaluate, so a report can never count a\n * record its reader could not open — and the total here is the reader's\n * total, not the org's billable audience, which is the head-count the\n * contacts list keeps for the quota.\n *\n * ## Read once a minute, not once an arrival\n *\n * Every read on this page is keyed by the scope and the period and kept\n * for `AGGREGATE_READ_TTL_MS` — see `useAggregateRead` — so leaving to open\n * a deal the report named and coming straight back costs nothing. Refresh\n * forgets this reader's answers for this org and anchors the period again,\n * which is what makes every card ask afresh.\n */\nexport function ContactsReportsSection(props: ConsolePluginPageProps) {\n const { hostId, org, basePath } = props\n const firestore = useFirestore()\n // The org data root — null until the org lookup settles, and the page\n // reads nothing until it does — and the controller this page is viewed\n // as, resolved the way every CRM surface resolves them so the facet a\n // stage is read from here is the facet the contacts list edits it in.\n const { scope: dataScope, consentGroup, visibleTo } = useCrmScope({ hostId, org })\n // `null` at the organization level (AGL-2630): no clause, and every card\n // totals every site; the holder is then each contact's own.\n const tokens = useMemo(() => (visibleTo ? [...visibleTo] : null), [visibleTo])\n const groupId = consentGroup?.groupId ?? null\n const orgRecord = (org ?? null) as Record<string, unknown> | null\n /*\n * The period AND the moment it was chosen, as one value: the range is\n * anchored when the reader picks, not re-derived from a moving `Date.now()`\n * per render, which would rebuild every query on every pass.\n */\n const [view, setView] = useState<{\n period: CrmReportPeriod\n nowMs: number\n }>(() => ({ period: '30d', nowMs: Date.now() }))\n const range = useMemo(\n () => crmReportRange(view.period, view.nowMs),\n [view],\n )\n const routes = useMemo(() => crmRoutes(basePath ?? ''), [basePath])\n const refresh = useCallback(() => {\n if (dataScope) {\n invalidateAggregateReads(reportCachePrefix({ scope: dataScope, tokens, groupId }))\n }\n // A new anchor is what changes every card's dependencies, so each read\n // runs again and — its key forgotten — reaches the server.\n setView((current) => ({ period: current.period, nowMs: Date.now() }))\n }, [dataScope, tokens, groupId])\n\n const report = useMemo<CrmReportScope | null>(\n () =>\n dataScope\n ? {\n scope: dataScope,\n tokens,\n groupId,\n org: orgRecord,\n period: view.period,\n range,\n nowMs: view.nowMs,\n routes,\n }\n : null,\n [dataScope, tokens, groupId, orgRecord, view, range, routes],\n )\n\n /**\n * Every contact this reader may see, counted once for the two cards that\n * quote it — the tile, and the sample notice that compares the read to it.\n */\n const totalContacts = useAggregateRead<number>(\n () =>\n dataScope\n ? getCountFromServer(\n query(\n scopedCollection(firestore, dataScope, 'contacts'),\n ...visibleToClause(tokens),\n ),\n ).then((snapshot) => snapshot.data().count)\n : null,\n // The anchor is a dependency so Refresh re-asks; the key, which omits\n // it, is what answers a reopened section from memory.\n [firestore, dataScope, tokens, view.nowMs],\n report ? { cacheKey: reportCacheKey(report, 'contacts:total') } : {},\n )\n\n return (\n <Stack spacing={2}>\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={2}\n sx={{\n alignItems: 'center',\n justifyContent: 'space-between',\n flexWrap: 'wrap',\n }}\n >\n <Typography variant=\"h6\" component=\"h2\">\n {'Reports'}\n </Typography>\n <Stack useFlexGap direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}>\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={view.period}\n onChange={(_event, next) => {\n if (next) {\n setView({ period: next as CrmReportPeriod, nowMs: Date.now() })\n }\n }}\n aria-label=\"Report period\"\n >\n {CRM_REPORT_PERIODS.map((period) => (\n <ToggleButton key={period} value={period}>\n {CRM_REPORT_PERIOD_LABELS[period]}\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiRefresh.path} size={0.8} />}\n disabled={!report}\n onClick={refresh}\n >\n {'Refresh'}\n </Button>\n </Stack>\n </Stack>\n {report ? (\n <>\n {/*\n The same masonry the billing and staff pages use: columns as tall\n as their own contents, so a short card beside a tall one leaves no\n hole. Measured live before this changed — 271, 272 and 111 pixels\n of dead space down one report, and an odd card stranded with an\n empty column beside it.\n */}\n <CardColumns\n spacing={2}\n items={[\n {\n key: 'trend',\n children: (\n <ContactsTrendCard report={report} totalContacts={totalContacts} />\n ),\n },\n {\n key: 'mix',\n children: (\n <ContactsMixCard report={report} totalContacts={totalContacts} />\n ),\n },\n { key: 'source-conversion', children: <SourceConversionCard report={report} /> },\n { key: 'lead-funnel', children: <LeadFunnelCard report={report} hostId={hostId} /> },\n { key: 'lead-sources', children: <LeadSourceCard report={report} hostId={hostId} /> },\n { key: 'pipeline', children: <PipelineCard report={report} /> },\n {\n key: 'closed',\n /*\n One column item carrying two cards, because multicol picks\n its own break: two consecutive items are only USUALLY\n adjacent, and a card that is a footnote to the one above it\n cannot be left to that. Won and lost by owner asks a second\n question of the very rows Won and lost already read — the\n same window under the same `closed:won` and `closed:lost`\n keys — so it is read as that card's breakdown, and\n `break-inside: avoid` keeps the pair whole in one column.\n */\n children: (\n <Stack spacing={2}>\n <ClosedDealsCard report={report} />\n <WonLostByOwnerCard report={report} />\n </Stack>\n ),\n },\n ]}\n />\n {/*\n Outside the columns: multicol cannot span, and these three read\n across the full width.\n */}\n <Stack spacing={2}>\n <ActivityCard report={report} />\n <ForecastCard report={report} />\n <TasksCard report={report} />\n </Stack>\n </>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n )}\n </Stack>\n )\n}\nContactsReportsSection.displayName = 'ContactsReportsSection'\n\nexport default ContactsReportsSection\n"],"names":["mdiRefresh","MdiIcon","CardColumns","Button","Stack","ToggleButton","ToggleButtonGroup","Typography","getCountFromServer","query","useCallback","useMemo","useState","useFirestore","useCrmScope","crmRoutes","ActivityCard","ClosedDealsCard","ContactsMixCard","ContactsTrendCard","ForecastCard","LeadFunnelCard","LeadSourceCard","PipelineCard","SourceConversionCard","WonLostByOwnerCard","reportCacheKey","reportCachePrefix","scopedCollection","visibleToClause","TasksCard","invalidateAggregateReads","useAggregateRead","CRM_REPORT_PERIOD_LABELS","CRM_REPORT_PERIODS","crmReportRange","ContactsReportsSection","props","hostId","org","basePath","firestore","scope","dataScope","consentGroup","visibleTo","tokens","groupId","orgRecord","view","setView","period","nowMs","Date","now","range","routes","refresh","current","report","totalContacts","then","snapshot","data","count","cacheKey","spacing","useFlexGap","direction","sx","alignItems","justifyContent","flexWrap","variant","component","rowGap","exclusive","size","color","value","onChange","_event","next","aria-label","map","startIcon","path","disabled","onClick","items","key","children","displayName"],"mappings":"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"}
@@ -77,6 +77,7 @@ import { bucketTasksByCalendarDay, CALENDAR_DAY_LABELS, CALENDAR_WEEK_DAYS, shif
77
77
  spacing: 1.5,
78
78
  children: [
79
79
  /*#__PURE__*/ _jsxs(Stack, {
80
+ useFlexGap: true,
80
81
  direction: "row",
81
82
  spacing: 1,
82
83
  sx: {