@aglyn/plugins-crm 1.0.0-beta.230 → 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.
- package/package.json +14 -13
- package/src/lib/components/activity-timeline.js +1 -0
- package/src/lib/components/activity-timeline.js.map +1 -1
- package/src/lib/components/assignment-rule-drawer.js +1 -0
- package/src/lib/components/assignment-rule-drawer.js.map +1 -1
- package/src/lib/components/contact-associations-card.js +1 -0
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-duplicates-card.js +1 -0
- package/src/lib/components/contact-duplicates-card.js.map +1 -1
- package/src/lib/components/contact-field-drawer.js +1 -0
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/components/contact-known-by-card.js +1 -0
- package/src/lib/components/contact-known-by-card.js.map +1 -1
- package/src/lib/components/contact-timeline-card.js +1 -0
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/crm-glance-card.js +1 -0
- package/src/lib/components/crm-glance-card.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.d.ts +1 -13
- package/src/lib/components/crm-next-activity-column.js +1 -19
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/crm-record-header.js +2 -0
- package/src/lib/components/crm-record-header.js.map +1 -1
- package/src/lib/components/deals-section.js +1 -0
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.js +4 -0
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-history-card.js +1 -0
- package/src/lib/components/lead-history-card.js.map +1 -1
- package/src/lib/components/lead-status-options.d.ts +2 -14
- package/src/lib/components/lead-status-options.js +1 -13
- package/src/lib/components/lead-status-options.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +1 -0
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.d.ts +1 -2
- package/src/lib/components/lead-unqualify-dialog.js +2 -1
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/linked-deals-card.js +1 -0
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/picklist-values-card.js +1 -0
- package/src/lib/components/picklist-values-card.js.map +1 -1
- package/src/lib/components/recipes-card.js +1 -0
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/reports/activity-card.js +1 -0
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/components/reports/closed-deals-card.js +1 -0
- package/src/lib/components/reports/closed-deals-card.js.map +1 -1
- package/src/lib/components/reports/contacts-trend-card.js +1 -0
- package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +1 -0
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/lead-source-card.js +1 -0
- package/src/lib/components/reports/lead-source-card.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +1 -0
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +1 -0
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/reports/tasks-card.js +1 -0
- package/src/lib/components/reports/tasks-card.js.map +1 -1
- package/src/lib/components/reports-section.js +2 -0
- package/src/lib/components/reports-section.js.map +1 -1
- package/src/lib/components/settings-section.d.ts +18 -0
- package/src/lib/components/settings-section.js +97 -1
- package/src/lib/components/settings-section.js.map +1 -1
- package/src/lib/components/tasks-calendar.js +1 -0
- package/src/lib/components/tasks-calendar.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +2 -2
- package/src/lib/constants/company-filters.js +1 -1
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +3 -3
- package/src/lib/constants/contact-filters.js +2 -1
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +3 -3
- package/src/lib/constants/deal-filters.js +1 -1
- package/src/lib/constants/deal-filters.js.map +1 -1
- package/src/lib/constants/field-list-query.d.ts +1 -1
- package/src/lib/constants/field-list-query.js +1 -1
- package/src/lib/constants/field-list-query.js.map +1 -1
- package/src/lib/declarations.server.js +9 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/contacts-list-seed.d.ts +1 -1
- package/src/lib/model/contacts-list-seed.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +4 -4
- package/src/lib/model/crm-list-query.js +2 -2
- package/src/lib/model/crm-list-query.js.map +1 -1
- package/src/lib/model/crm-next-activity.d.ts +13 -0
- package/src/lib/model/crm-next-activity.js +19 -0
- package/src/lib/model/crm-next-activity.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +6 -5
- package/src/lib/model/lead-filters.js +3 -2
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/lead-status-choices.d.ts +32 -0
- package/src/lib/model/lead-status-choices.js +31 -0
- package/src/lib/model/lead-status-choices.js.map +1 -0
- package/src/lib/model/task-views.d.ts +2 -2
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/person-records.d.ts +27 -0
- package/src/lib/server/person-records.js +127 -4
- package/src/lib/server/person-records.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/closed-deals-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n getCountFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportBarChart } from './report-bar-chart'\nimport { weekLabel } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { bucketByWeek, currencyOfDeals } from '../../model/crm-reports'\n\n/**\n * How many closed deals of each outcome the weekly chart reads.\n *\n * The tiles are server aggregates and never meet this bound; the chart\n * needs each deal's `closedAtMs` to place it in a week, so it reads them,\n * most recent first, and says when the period held more.\n */\nconst CLOSED_DEAL_CEILING = 500\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\n\nexport interface ClosedDealsCardProps {\n report: CrmReportScope\n}\n\n/**\n * Deals won and lost in the period, and the win rate over what closed\n * (AGL-2604).\n *\n * Won and lost are TWO queries rather than one over `status in [won,\n * lost]`: each is an equality the `(visibleTo, status, closedAtMs)` index\n * serves directly, and a card that drew one series from a merged read\n * would have to split it again by hand. The range is on `closedAtMs`, the\n * moment the deal was closed — not `updatedAt`, which a note added last\n * week would move.\n */\nexport function ClosedDealsCard(props: ClosedDealsCardProps) {\n const { report } = props\n const { scope, tokens, range, routes } = report\n const firestore = useFirestore()\n\n const closedInPeriod = (status: Aglyn.CrmDealStatus) =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', status),\n where('closedAtMs', '>=', range.from),\n where('closedAtMs', '<', range.to),\n )\n\n const closed = useAggregateRead(\n () =>\n Promise.all([\n getAggregateFromServer(closedInPeriod('won'), {\n count: count(),\n amountCents: sum('amountCents'),\n }).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n getCountFromServer(closedInPeriod('lost')).then(\n (snapshot) => snapshot.data().count,\n ),\n ]).then(([won, lost]) => ({ won, lost })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:totals') },\n )\n\n const wonWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('won'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:won') },\n )\n const lostWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('lost'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:lost') },\n )\n const wonStatus = wonWindow.status\n const lostStatus = lostWindow.status\n\n const chart = useMemo(() => {\n const wonWeeks = bucketByWeek(\n wonWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n const lostWeeks = bucketByWeek(\n lostWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n return {\n labels: wonWeeks.map((week) => weekLabel(week.start)),\n won: wonWeeks.map((week) => week.items.length),\n wonCents: wonWeeks.map((week) =>\n week.items.reduce(\n (total, deal) => total + Math.max(0, Number(deal.amountCents ?? 0)),\n 0,\n ),\n ),\n lost: lostWeeks.map((week) => week.items.length),\n currency: currencyOfDeals(wonWindow.rows).currency,\n }\n }, [wonWindow, lostWindow, range])\n\n const figures = closed.value\n const winRate = figures\n ? measuredRate(figures.won.count, figures.won.count + figures.lost, 'closed')\n : null\n const chartRead = wonStatus === 'success' && lostStatus === 'success'\n\n return (\n <CardDisplay\n header={'Won and lost'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#won-and-lost',\n excerpt:\n 'Deals closed in the period, by outcome and by week, with the ' +\n 'value of what was won and the win rate over everything that ' +\n 'closed. Placed by the moment each deal was closed.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Won'}\n value={figures ? figures.won.count.toLocaleString() : null}\n note={\n figures\n ? `${money(figures.won.amountCents, chart.currency)} closed won`\n : undefined\n }\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Lost'}\n value={figures ? figures.lost.toLocaleString() : null}\n note={'in the period'}\n riseIsGood={false}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Win rate'}\n value={winRate ? percent(winRate.value) : null}\n note={\n winRate\n ? `${winRate.numerator.toLocaleString()} of ${winRate.denominator.toLocaleString()} ${winRate.denominatorLabel}`\n : figures\n ? 'nothing closed in the period'\n : undefined\n }\n />\n </Stack>\n {closed.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The closed-deal totals could not be read.'}\n </Alert>\n ) : null}\n <Section title={'Won vs lost per week'}>\n <ReportBarChart\n labels={chart.labels}\n series={[\n { name: 'Won', color: 'success.main', values: chart.won },\n { name: 'Lost', color: 'error.main', values: chart.lost },\n ]}\n format={(value, seriesIndex, barIndex) =>\n seriesIndex === 0\n ? `Won: ${value.toLocaleString()} · ${money(chart.wonCents[barIndex] ?? 0, chart.currency)}`\n : `Lost: ${value.toLocaleString()}`\n }\n emptyText={chartRead ? 'Nothing closed in this period.' : 'Reading…'}\n />\n {wonWindow.truncated || lostWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`The chart places the ${CLOSED_DEAL_CEILING} most recently closed of each outcome; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nClosedDealsCard.displayName = 'ClosedDealsCard'\n\nexport default ClosedDealsCard\n"],"names":["Aglyn","money","CardDisplay","measuredRate","percent","Section","Alert","Stack","Typography","count","getAggregateFromServer","getCountFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","ReportBarChart","weekLabel","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","bucketByWeek","currencyOfDeals","CLOSED_DEAL_CEILING","ClosedDealsCard","props","report","scope","tokens","range","routes","firestore","closedInPeriod","status","from","to","closed","Promise","all","amountCents","then","snapshot","Number","data","won","lost","cacheKey","wonWindow","lostWindow","wonStatus","lostStatus","chart","wonWeeks","rows","deal","closedAtMs","lostWeeks","labels","map","week","start","items","length","wonCents","reduce","total","Math","max","currency","figures","value","winRate","chartRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","direction","sx","flexWrap","label","toLocaleString","note","undefined","href","section","riseIsGood","numerator","denominator","denominatorLabel","severity","title","series","name","color","values","format","seriesIndex","barIndex","emptyText","truncated","variant","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/closed-deals-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n getCountFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportBarChart } from './report-bar-chart'\nimport { weekLabel } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { bucketByWeek, currencyOfDeals } from '../../model/crm-reports'\n\n/**\n * How many closed deals of each outcome the weekly chart reads.\n *\n * The tiles are server aggregates and never meet this bound; the chart\n * needs each deal's `closedAtMs` to place it in a week, so it reads them,\n * most recent first, and says when the period held more.\n */\nconst CLOSED_DEAL_CEILING = 500\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\n\nexport interface ClosedDealsCardProps {\n report: CrmReportScope\n}\n\n/**\n * Deals won and lost in the period, and the win rate over what closed\n * (AGL-2604).\n *\n * Won and lost are TWO queries rather than one over `status in [won,\n * lost]`: each is an equality the `(visibleTo, status, closedAtMs)` index\n * serves directly, and a card that drew one series from a merged read\n * would have to split it again by hand. The range is on `closedAtMs`, the\n * moment the deal was closed — not `updatedAt`, which a note added last\n * week would move.\n */\nexport function ClosedDealsCard(props: ClosedDealsCardProps) {\n const { report } = props\n const { scope, tokens, range, routes } = report\n const firestore = useFirestore()\n\n const closedInPeriod = (status: Aglyn.CrmDealStatus) =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', status),\n where('closedAtMs', '>=', range.from),\n where('closedAtMs', '<', range.to),\n )\n\n const closed = useAggregateRead(\n () =>\n Promise.all([\n getAggregateFromServer(closedInPeriod('won'), {\n count: count(),\n amountCents: sum('amountCents'),\n }).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n getCountFromServer(closedInPeriod('lost')).then(\n (snapshot) => snapshot.data().count,\n ),\n ]).then(([won, lost]) => ({ won, lost })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:totals') },\n )\n\n const wonWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('won'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:won') },\n )\n const lostWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('lost'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:lost') },\n )\n const wonStatus = wonWindow.status\n const lostStatus = lostWindow.status\n\n const chart = useMemo(() => {\n const wonWeeks = bucketByWeek(\n wonWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n const lostWeeks = bucketByWeek(\n lostWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n return {\n labels: wonWeeks.map((week) => weekLabel(week.start)),\n won: wonWeeks.map((week) => week.items.length),\n wonCents: wonWeeks.map((week) =>\n week.items.reduce(\n (total, deal) => total + Math.max(0, Number(deal.amountCents ?? 0)),\n 0,\n ),\n ),\n lost: lostWeeks.map((week) => week.items.length),\n currency: currencyOfDeals(wonWindow.rows).currency,\n }\n }, [wonWindow, lostWindow, range])\n\n const figures = closed.value\n const winRate = figures\n ? measuredRate(figures.won.count, figures.won.count + figures.lost, 'closed')\n : null\n const chartRead = wonStatus === 'success' && lostStatus === 'success'\n\n return (\n <CardDisplay\n header={'Won and lost'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#won-and-lost',\n excerpt:\n 'Deals closed in the period, by outcome and by week, with the ' +\n 'value of what was won and the win rate over everything that ' +\n 'closed. Placed by the moment each deal was closed.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Won'}\n value={figures ? figures.won.count.toLocaleString() : null}\n note={\n figures\n ? `${money(figures.won.amountCents, chart.currency)} closed won`\n : undefined\n }\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Lost'}\n value={figures ? figures.lost.toLocaleString() : null}\n note={'in the period'}\n riseIsGood={false}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Win rate'}\n value={winRate ? percent(winRate.value) : null}\n note={\n winRate\n ? `${winRate.numerator.toLocaleString()} of ${winRate.denominator.toLocaleString()} ${winRate.denominatorLabel}`\n : figures\n ? 'nothing closed in the period'\n : undefined\n }\n />\n </Stack>\n {closed.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The closed-deal totals could not be read.'}\n </Alert>\n ) : null}\n <Section title={'Won vs lost per week'}>\n <ReportBarChart\n labels={chart.labels}\n series={[\n { name: 'Won', color: 'success.main', values: chart.won },\n { name: 'Lost', color: 'error.main', values: chart.lost },\n ]}\n format={(value, seriesIndex, barIndex) =>\n seriesIndex === 0\n ? `Won: ${value.toLocaleString()} · ${money(chart.wonCents[barIndex] ?? 0, chart.currency)}`\n : `Lost: ${value.toLocaleString()}`\n }\n emptyText={chartRead ? 'Nothing closed in this period.' : 'Reading…'}\n />\n {wonWindow.truncated || lostWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`The chart places the ${CLOSED_DEAL_CEILING} most recently closed of each outcome; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nClosedDealsCard.displayName = 'ClosedDealsCard'\n\nexport default ClosedDealsCard\n"],"names":["Aglyn","money","CardDisplay","measuredRate","percent","Section","Alert","Stack","Typography","count","getAggregateFromServer","getCountFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","ReportBarChart","weekLabel","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","bucketByWeek","currencyOfDeals","CLOSED_DEAL_CEILING","ClosedDealsCard","props","report","scope","tokens","range","routes","firestore","closedInPeriod","status","from","to","closed","Promise","all","amountCents","then","snapshot","Number","data","won","lost","cacheKey","wonWindow","lostWindow","wonStatus","lostStatus","chart","wonWeeks","rows","deal","closedAtMs","lostWeeks","labels","map","week","start","items","length","wonCents","reduce","total","Math","max","currency","figures","value","winRate","chartRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","useFlexGap","direction","sx","flexWrap","label","toLocaleString","note","undefined","href","section","riseIsGood","numerator","denominator","denominatorLabel","severity","title","series","name","color","values","format","seriesIndex","barIndex","emptyText","truncated","variant","displayName"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/contacts-trend-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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport { getCountFromServer, query, Timestamp, where } from 'firebase/firestore'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportBarChart } from './report-bar-chart'\nimport { weekLabel } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { type AggregateRead, useAggregateRead } from './use-aggregate-read'\nimport { deltaPercent, weekBuckets } from '../../model/crm-reports'\n\nexport interface ContactsTrendCardProps {\n report: CrmReportScope\n /** The section's one count of every contact this reader may see. */\n totalContacts: AggregateRead<number>\n}\n\ninterface ContactsTrend {\n /** New contacts in the period — the sum of the weeks. */\n current: number\n /** New contacts in the period before it. */\n previous: number\n weekly: Array<{ start: number; end: number; count: number }>\n}\n\n/**\n * New contacts over the period, against the period before, week by week\n * (AGL-2604).\n *\n * ## Every number here is a server count\n *\n * One `getCountFromServer` per week of the period and one for the previous\n * period — fourteen reads for ninety days, billed as index entries rather\n * than documents — and NO document is downloaded to be counted. The period\n * figure is the sum of its weeks rather than a fifteenth read, so the tile\n * and the bars cannot disagree. The contacts list's head-count learned the\n * same lesson (AGL-1706): a count taken from a capped listener saturates at\n * the cap, and the org this page is for is the one with more contacts than\n * any listener should carry.\n *\n * The range is on `createdAt`, the timestamp every contact writer stamps —\n * the server upsert with `serverTimestamp()`, the console's add-by-hand with\n * a `Date` — so \"new\" means first captured, not last touched.\n */\nexport function ContactsTrendCard(props: ContactsTrendCardProps) {\n const { report, totalContacts } = props\n const { scope, tokens, range, routes } = report\n const firestore = useFirestore()\n\n const trend = useAggregateRead<ContactsTrend>(() => {\n const contacts = scopedCollection(firestore, scope, 'contacts')\n const createdBetween = (from: number, to: number) =>\n getCountFromServer(\n query(\n contacts,\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(from)),\n where('createdAt', '<', Timestamp.fromMillis(to)),\n ),\n ).then((snapshot) => snapshot.data().count)\n const buckets = weekBuckets(range.from, range.to)\n return Promise.all([\n createdBetween(range.previousFrom, range.previousTo),\n ...buckets.map((bucket) => createdBetween(bucket.start, bucket.end)),\n ]).then(([previous, ...weekly]) => ({\n previous,\n current: weekly.reduce((sum, count) => sum + count, 0),\n weekly: buckets.map((bucket, index) => ({\n ...bucket,\n count: weekly[index] ?? 0,\n })),\n }))\n }, [firestore, scope, tokens, range], { cacheKey: reportCacheKey(report, 'contacts:trend') })\n\n const figures = trend.value\n return (\n <CardDisplay\n header={'Contacts'}\n help={pluginDocsHelp('crmReports', {\n anchor: '#contacts',\n excerpt:\n 'New contacts in the period against the period before it, week ' +\n 'by week, and how many contacts this site can see in all. Counted ' +\n 'on the server by when each contact was first captured.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'New contacts'}\n value={figures ? figures.current.toLocaleString() : null}\n deltaPct={figures ? deltaPercent(figures.current, figures.previous) : null}\n note={\n figures\n ? figures.previous\n ? `${figures.previous.toLocaleString()} in the previous period`\n : 'none in the previous period'\n : undefined\n }\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Total contacts'}\n value={\n totalContacts.value === null\n ? null\n : totalContacts.value.toLocaleString()\n }\n note={'visible to this site'}\n href={routes.section('contacts')}\n />\n </Stack>\n {trend.status === 'error' || totalContacts.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The contact counts could not be read. The rest of the page is unaffected.'}\n </Alert>\n ) : null}\n <Section title={'New contacts per week'}>\n <ReportBarChart\n labels={(figures?.weekly ?? []).map((week) => weekLabel(week.start))}\n series={[\n {\n name: 'New contacts',\n color: 'primary.main',\n values: (figures?.weekly ?? []).map((week) => week.count),\n },\n ]}\n emptyText={\n trend.status === 'loading' ? 'Counting…' : 'Nothing in this period.'\n }\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Each bar is seven days from the start of the period; the last ' +\n 'may be shorter.'}\n </Typography>\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nContactsTrendCard.displayName = 'ContactsTrendCard'\n\nexport default ContactsTrendCard\n"],"names":["pluginDocsHelp","CardDisplay","Section","Alert","Stack","Typography","getCountFromServer","query","Timestamp","where","useFirestore","ReportBarChart","weekLabel","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","deltaPercent","weekBuckets","ContactsTrendCard","props","report","totalContacts","scope","tokens","range","routes","firestore","trend","contacts","createdBetween","from","to","fromMillis","then","snapshot","data","count","buckets","Promise","all","previousFrom","previousTo","map","bucket","start","end","previous","weekly","current","reduce","sum","index","cacheKey","figures","value","header","help","anchor","excerpt","contentGutterX","contentGutterY","spacing","direction","sx","flexWrap","label","toLocaleString","deltaPct","note","undefined","href","section","status","severity","title","labels","week","series","name","color","values","emptyText","variant","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/contacts-trend-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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport { getCountFromServer, query, Timestamp, where } from 'firebase/firestore'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportBarChart } from './report-bar-chart'\nimport { weekLabel } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { type AggregateRead, useAggregateRead } from './use-aggregate-read'\nimport { deltaPercent, weekBuckets } from '../../model/crm-reports'\n\nexport interface ContactsTrendCardProps {\n report: CrmReportScope\n /** The section's one count of every contact this reader may see. */\n totalContacts: AggregateRead<number>\n}\n\ninterface ContactsTrend {\n /** New contacts in the period — the sum of the weeks. */\n current: number\n /** New contacts in the period before it. */\n previous: number\n weekly: Array<{ start: number; end: number; count: number }>\n}\n\n/**\n * New contacts over the period, against the period before, week by week\n * (AGL-2604).\n *\n * ## Every number here is a server count\n *\n * One `getCountFromServer` per week of the period and one for the previous\n * period — fourteen reads for ninety days, billed as index entries rather\n * than documents — and NO document is downloaded to be counted. The period\n * figure is the sum of its weeks rather than a fifteenth read, so the tile\n * and the bars cannot disagree. The contacts list's head-count learned the\n * same lesson (AGL-1706): a count taken from a capped listener saturates at\n * the cap, and the org this page is for is the one with more contacts than\n * any listener should carry.\n *\n * The range is on `createdAt`, the timestamp every contact writer stamps —\n * the server upsert with `serverTimestamp()`, the console's add-by-hand with\n * a `Date` — so \"new\" means first captured, not last touched.\n */\nexport function ContactsTrendCard(props: ContactsTrendCardProps) {\n const { report, totalContacts } = props\n const { scope, tokens, range, routes } = report\n const firestore = useFirestore()\n\n const trend = useAggregateRead<ContactsTrend>(() => {\n const contacts = scopedCollection(firestore, scope, 'contacts')\n const createdBetween = (from: number, to: number) =>\n getCountFromServer(\n query(\n contacts,\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(from)),\n where('createdAt', '<', Timestamp.fromMillis(to)),\n ),\n ).then((snapshot) => snapshot.data().count)\n const buckets = weekBuckets(range.from, range.to)\n return Promise.all([\n createdBetween(range.previousFrom, range.previousTo),\n ...buckets.map((bucket) => createdBetween(bucket.start, bucket.end)),\n ]).then(([previous, ...weekly]) => ({\n previous,\n current: weekly.reduce((sum, count) => sum + count, 0),\n weekly: buckets.map((bucket, index) => ({\n ...bucket,\n count: weekly[index] ?? 0,\n })),\n }))\n }, [firestore, scope, tokens, range], { cacheKey: reportCacheKey(report, 'contacts:trend') })\n\n const figures = trend.value\n return (\n <CardDisplay\n header={'Contacts'}\n help={pluginDocsHelp('crmReports', {\n anchor: '#contacts',\n excerpt:\n 'New contacts in the period against the period before it, week ' +\n 'by week, and how many contacts this site can see in all. Counted ' +\n 'on the server by when each contact was first captured.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'New contacts'}\n value={figures ? figures.current.toLocaleString() : null}\n deltaPct={figures ? deltaPercent(figures.current, figures.previous) : null}\n note={\n figures\n ? figures.previous\n ? `${figures.previous.toLocaleString()} in the previous period`\n : 'none in the previous period'\n : undefined\n }\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Total contacts'}\n value={\n totalContacts.value === null\n ? null\n : totalContacts.value.toLocaleString()\n }\n note={'visible to this site'}\n href={routes.section('contacts')}\n />\n </Stack>\n {trend.status === 'error' || totalContacts.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The contact counts could not be read. The rest of the page is unaffected.'}\n </Alert>\n ) : null}\n <Section title={'New contacts per week'}>\n <ReportBarChart\n labels={(figures?.weekly ?? []).map((week) => weekLabel(week.start))}\n series={[\n {\n name: 'New contacts',\n color: 'primary.main',\n values: (figures?.weekly ?? []).map((week) => week.count),\n },\n ]}\n emptyText={\n trend.status === 'loading' ? 'Counting…' : 'Nothing in this period.'\n }\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Each bar is seven days from the start of the period; the last ' +\n 'may be shorter.'}\n </Typography>\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nContactsTrendCard.displayName = 'ContactsTrendCard'\n\nexport default ContactsTrendCard\n"],"names":["pluginDocsHelp","CardDisplay","Section","Alert","Stack","Typography","getCountFromServer","query","Timestamp","where","useFirestore","ReportBarChart","weekLabel","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","deltaPercent","weekBuckets","ContactsTrendCard","props","report","totalContacts","scope","tokens","range","routes","firestore","trend","contacts","createdBetween","from","to","fromMillis","then","snapshot","data","count","buckets","Promise","all","previousFrom","previousTo","map","bucket","start","end","previous","weekly","current","reduce","sum","index","cacheKey","figures","value","header","help","anchor","excerpt","contentGutterX","contentGutterY","spacing","useFlexGap","direction","sx","flexWrap","label","toLocaleString","deltaPct","note","undefined","href","section","status","severity","title","labels","week","series","name","color","values","emptyText","variant","displayName"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/lead-funnel-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport {\n collection,\n type Firestore,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../../hooks/use-crm-org-mount'\nimport { useLeadStatusPicklist } from '../../hooks/use-lead-status-picklist'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename } from './report-format'\nimport { type CrmReportScope, reportCacheKey } from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport {\n type AggregateRead,\n useAggregateRead,\n useWindowRead,\n type WindowRead,\n} from './use-aggregate-read'\nimport { deltaPercent, leadFunnel } from '../../model/crm-reports'\n\n/**\n * How many of the period's leads the funnel is placed from — the Leads\n * section's own window, over the period rather than over everything.\n *\n * The captured count is a server aggregate and never meets this bound; the\n * funnel needs each lead's status and reason, so it reads the period's\n * newest two hundred and says when there were more. At the organization\n * level the bound is PER SITE (AGL-2634): each site's window is its own,\n * and a site with a thousand leads must not crowd out a site with ten.\n */\nexport const LEAD_CEILING = 200\n/** How many reasons the bar list draws; the rest are counted beneath it. */\nconst REASONS_SHOWN = 8\n\n/** The CSV's columns: one file for both the statuses and the reasons. */\nconst COLUMNS = ['Kind', 'Label', 'Count', 'Share'] as const\n\ntype LeadRow = Record<string, unknown> & Aglyn.CrmLeadFields & { $id: string }\n\n/** The period's captured count, and the previous period's for the delta. */\ninterface CapturedFigures {\n current: number\n previous: number\n}\n\nexport interface LeadFunnelCardProps {\n report: CrmReportScope\n /**\n * The site whose leads are placed. A lead lives under its host, not the\n * org — `hosts/{hostId}/leads`, private by path, no `visibleTo` — so the\n * report's org scope cannot reach it and the card is told the site. At\n * the organization level `null` (AGL-2630): the card then reads every\n * site the mount lists, one window each (AGL-2634).\n */\n hostId: string | null\n}\n\nconst LEAD_FUNNEL_HELP = Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#lead-funnel',\n excerpt:\n 'Of the leads this site captured in the period, how many are ' +\n 'still new, being worked, qualified or unqualified — and the ' +\n 'reasons the unqualified ones were closed. Placed by when each ' +\n 'lead was first seen.',\n})\n\n/**\n * The leads a scope may see, first seen in a range (AGL-3275).\n *\n * `visibleTo` is the site's tokens under a site, and `null` at the\n * organization level where an org-wide member reads without a clause. The\n * range stays a single-field one; with the scope clause it composes with the\n * `visibleTo` + `firstSeenAtMs` entries in `cloud/firebase-firestore.indexes.json`\n * — the descending one for the window, the ascending one for the counts,\n * which name no order (AGL-3443).\n */\nexport function leadsBetween(\n firestore: Firestore,\n orgId: string,\n visibleTo: readonly string[] | null,\n from: number,\n to: number,\n) {\n return query(\n collection(firestore, 'orgs', orgId, 'leads'),\n ...(visibleTo ? [where('visibleTo', 'array-contains-any', [...visibleTo])] : []),\n where('firstSeenAtMs', '>=', from),\n where('firstSeenAtMs', '<', to),\n )\n}\n\nconst countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n/**\n * The lead funnel, at whichever level the report is mounted (AGL-2624,\n * AGL-2630, AGL-2634).\n *\n * Under a site: that site. At the organization level: every site the\n * mount lists, one window each, totaled — the org hub's reports total\n * every site, and the leads are no exception now that the fan-out is\n * bounded by the org's site list. With no sites to read the card says so\n * instead of guessing.\n */\nexport function LeadFunnelCard(props: LeadFunnelCardProps) {\n const { report, hostId } = props\n const mount = useCrmOrgMount()\n if (hostId) return <SiteLeadFunnelCard report={report} hostId={hostId} />\n if (mount && (!mount.hostsReady || mount.hosts.length > 0)) {\n return <OrgLeadFunnelCard report={report} />\n }\n return (\n <CardDisplay header={'Lead funnel'} help={LEAD_FUNNEL_HELP} contentGutterX contentGutterY>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mount\n ? 'Leads live under a site, and this organization has no sites yet.'\n : 'Leads live under a site, and this report is mounted under none.'}\n </Typography>\n </CardDisplay>\n )\n}\n\n/**\n * The lead funnel (AGL-2624): of the leads this site captured in the\n * period, how many are still new, being worked, qualified or unqualified —\n * and why the unqualified ones were closed.\n *\n * A cohort by first sighting, followed to where each lead stands now,\n * because that is the question a period asks of leads — \"what became of\n * the ones we got\" — and because a lead's status has no history on the\n * document: only its current value. The window is a range on\n * `firstSeenAtMs`, which the capture door stamps once on creation and\n * never again, so a returning visitor's second form is not a second lead\n * in a second period. A single-field range needs no composite index.\n *\n * Read directly off `hosts/{hostId}/leads` under the site's own rules —\n * any member of the site may read it — the way the Leads section reads\n * it, so the funnel and the list can never disagree about who may see a\n * lead.\n */\nfunction SiteLeadFunnelCard(props: { report: CrmReportScope; hostId: string }) {\n const { report, hostId } = props\n const { range } = report\n const firestore = useFirestore()\n\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], report.tokens, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:counts:${hostId}`) },\n )\n const window = useWindowRead<LeadRow>(\n () =>\n query(\n leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n LEAD_CEILING,\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:window:${hostId}`) },\n )\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n (figures !== null ? ` of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'This site’s leads could not be read.'}\n />\n )\n}\nSiteLeadFunnelCard.displayName = 'SiteLeadFunnelCard'\n\n/**\n * THE ORGANIZATION'S FUNNEL (AGL-2634, AGL-3275): every lead the org holds.\n *\n * This totaled one aggregate and one window PER SITE, because the sites'\n * lead collections were disjoint. They share one collection now, so a sum\n * would count a lead that two brands in a consent group both hold once for\n * each of them — and the merge that existed so a busy site could not crowd\n * out a quiet one has nothing left to balance. One aggregate and one\n * bounded read, unscoped, as an org-wide member's reads are.\n */\nfunction OrgLeadFunnelCard(props: { report: CrmReportScope }) {\n const { report } = props\n const { range } = report\n const firestore = useFirestore()\n const mount = useCrmOrgMount()\n const hostIds = useMemo(\n () => (mount?.hostsReady ? mount.hosts.map((host) => host.id) : null),\n [mount],\n )\n\n /*\n * ONE READ, not one per site (AGL-3275). The sites share a collection now,\n * so a lead two brands in a consent group both hold would be counted once\n * for each of them — and the org's funnel would report more leads than the\n * org has. `null` tokens is the org-wide read that carries no clause.\n */\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], null, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], null, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:counts:org') },\n )\n const merged = useAggregateRead<{ rows: LeadRow[]; truncated: boolean }>(\n () =>\n getDocs(\n query(\n leadsBetween(firestore, report.scope[1], null, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n ).then((snapshot) =>\n ceilingedWindow(\n snapshot.docs.map(\n (document) => ({ ...document.data(), $id: document.id }) as unknown as LeadRow,\n ),\n LEAD_CEILING,\n ),\n ),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:window:org') },\n )\n const window = useMemo<WindowRead<LeadRow>>(\n () => ({\n rows: merged.value?.rows ?? [],\n truncated: merged.value?.truncated ?? false,\n status: merged.status,\n }),\n [merged],\n )\n const sites = hostIds?.length ?? 0\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n subheader={`Every site (${sites.toLocaleString()})`}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n ` — at most ${LEAD_CEILING.toLocaleString()}` +\n (figures !== null ? ` — of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'The organization’s leads could not be read.'}\n />\n )\n}\nOrgLeadFunnelCard.displayName = 'OrgLeadFunnelCard'\n\n/**\n * The funnel as drawn, whichever level fed it: four tiles, the statuses,\n * the reasons, the export. Pure over the two reads.\n */\nfunction LeadFunnelBody(props: {\n report: CrmReportScope\n subheader?: string\n captured: AggregateRead<CapturedFigures>\n window: WindowRead<LeadRow>\n caption?: string\n errorText: string\n}) {\n const { report, subheader, captured, window, caption, errorText } = props\n const { period, routes } = report\n const status = window.status\n\n const funnel = useMemo(() => leadFunnel(window.rows), [window])\n // Each status counted by its MEANING, named as the org names it (AGL-3512).\n const { picklist: statuses } = useLeadStatusPicklist(report.scope[1])\n const statusName = (status: Aglyn.CrmLeadStatus) => Aglyn.crmLeadStatusLabelFor(statuses, status)\n const read = status === 'success'\n const figures = captured.value\n const share = (count: number, of: number): string | null =>\n of > 0 ? percent(count / of) : null\n const reasons = funnel.reasons.slice(0, REASONS_SHOWN)\n const moreReasons = funnel.reasons.length - reasons.length\n\n return (\n <CardDisplay\n header={'Lead funnel'}\n subheader={subheader}\n help={LEAD_FUNNEL_HELP}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <ReportExport\n filename={reportFilename('lead-funnel', period)}\n columns={COLUMNS}\n rows={() => [\n ...Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => [\n 'Status',\n statusName(leadStatus),\n funnel.byStatus[leadStatus],\n share(funnel.byStatus[leadStatus], funnel.total) ?? '',\n ]),\n ...funnel.reasons.map((reason) => [\n 'Unqualified reason',\n reason.label,\n reason.count,\n share(reason.count, funnel.byStatus.unqualified) ?? '',\n ]),\n ]}\n disabled={!read || !funnel.total}\n />\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('leads')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open leads'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Leads captured'}\n value={figures ? figures.current.toLocaleString() : null}\n deltaPct={figures ? deltaPercent(figures.current, figures.previous) : null}\n note={'first seen in the period'}\n href={routes.section('leads')}\n />\n <ReportStatTile\n label={statusName('qualified')}\n value={read ? funnel.byStatus.qualified.toLocaleString() : null}\n note={\n funnel.qualifiedRate === null\n ? read\n ? 'nobody captured in the period'\n : undefined\n : `${percent(funnel.qualifiedRate)} of those captured`\n }\n />\n <ReportStatTile\n label={statusName('unqualified')}\n value={read ? funnel.byStatus.unqualified.toLocaleString() : null}\n note={\n funnel.unqualifiedRate === null\n ? undefined\n : `${percent(funnel.unqualifiedRate)} of those captured`\n }\n riseIsGood={false}\n />\n <ReportStatTile\n label={'Still open'}\n value={read ? funnel.open.toLocaleString() : null}\n note={'new or being worked'}\n href={routes.section('leads')}\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{errorText}</Alert>\n ) : null}\n <Section title={'Where they stand'}>\n <ReportBreakdown\n rows={Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => ({\n key: leadStatus,\n label: statusName(leadStatus),\n value: funnel.byStatus[leadStatus],\n note: share(funnel.byStatus[leadStatus], funnel.total) ?? undefined,\n color:\n leadStatus === 'qualified'\n ? 'success.main'\n : leadStatus === 'unqualified'\n ? 'warning.main'\n : undefined,\n }))}\n emptyText={''}\n />\n {read && !funnel.total ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No leads captured in this period.'}\n </Typography>\n ) : null}\n </Section>\n <Section title={'Why leads were unqualified'}>\n <ReportBreakdown\n rows={reasons.map((reason) => ({\n key: reason.key,\n label: reason.label,\n value: reason.count,\n note: share(reason.count, funnel.byStatus.unqualified) ?? undefined,\n color: 'warning.main',\n }))}\n emptyText={\n status === 'loading' ? 'Reading…' : 'No lead was unqualified in this period.'\n }\n />\n {moreReasons > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`And ${plural(moreReasons, 'other reason')}, each given once or twice.`}\n </Typography>\n ) : null}\n </Section>\n {/* What the funnel, and so the export, is placed from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadFunnelBody.displayName = 'LeadFunnelBody'\nLeadFunnelCard.displayName = 'LeadFunnelCard'\n\nexport default LeadFunnelCard\n"],"names":["Aglyn","AppLink","CardDisplay","percent","Section","ceilingedWindow","Alert","Button","Stack","Typography","collection","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","useCrmOrgMount","useLeadStatusPicklist","ReportBreakdown","ReportExport","plural","reportFilename","reportCacheKey","ReportStatTile","useAggregateRead","useWindowRead","deltaPercent","leadFunnel","LEAD_CEILING","REASONS_SHOWN","COLUMNS","LEAD_FUNNEL_HELP","pluginDocsHelp","anchor","excerpt","leadsBetween","firestore","orgId","visibleTo","from","to","countOf","target","then","snapshot","data","count","LeadFunnelCard","props","report","hostId","mount","SiteLeadFunnelCard","hostsReady","hosts","length","OrgLeadFunnelCard","header","help","contentGutterX","contentGutterY","variant","color","range","captured","Promise","all","scope","tokens","previousFrom","previousTo","current","previous","cacheKey","window","figures","value","sampled","status","truncated","rows","LeadFunnelBody","caption","toLocaleString","undefined","errorText","displayName","hostIds","map","host","id","merged","docs","document","$id","sites","subheader","period","routes","funnel","picklist","statuses","statusName","crmLeadStatusLabelFor","read","share","of","reasons","slice","moreReasons","HeaderProps","action","direction","spacing","filename","columns","CRM_LEAD_STATUSES","leadStatus","byStatus","total","reason","label","unqualified","disabled","component","componentVariant","nativeButton","href","section","size","sx","flexWrap","deltaPct","note","qualified","qualifiedRate","unqualifiedRate","riseIsGood","open","severity","title","key","emptyText"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/lead-funnel-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport {\n collection,\n type Firestore,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../../hooks/use-crm-org-mount'\nimport { useLeadStatusPicklist } from '../../hooks/use-lead-status-picklist'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename } from './report-format'\nimport { type CrmReportScope, reportCacheKey } from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport {\n type AggregateRead,\n useAggregateRead,\n useWindowRead,\n type WindowRead,\n} from './use-aggregate-read'\nimport { deltaPercent, leadFunnel } from '../../model/crm-reports'\n\n/**\n * How many of the period's leads the funnel is placed from — the Leads\n * section's own window, over the period rather than over everything.\n *\n * The captured count is a server aggregate and never meets this bound; the\n * funnel needs each lead's status and reason, so it reads the period's\n * newest two hundred and says when there were more. At the organization\n * level the bound is PER SITE (AGL-2634): each site's window is its own,\n * and a site with a thousand leads must not crowd out a site with ten.\n */\nexport const LEAD_CEILING = 200\n/** How many reasons the bar list draws; the rest are counted beneath it. */\nconst REASONS_SHOWN = 8\n\n/** The CSV's columns: one file for both the statuses and the reasons. */\nconst COLUMNS = ['Kind', 'Label', 'Count', 'Share'] as const\n\ntype LeadRow = Record<string, unknown> & Aglyn.CrmLeadFields & { $id: string }\n\n/** The period's captured count, and the previous period's for the delta. */\ninterface CapturedFigures {\n current: number\n previous: number\n}\n\nexport interface LeadFunnelCardProps {\n report: CrmReportScope\n /**\n * The site whose leads are placed. A lead lives under its host, not the\n * org — `hosts/{hostId}/leads`, private by path, no `visibleTo` — so the\n * report's org scope cannot reach it and the card is told the site. At\n * the organization level `null` (AGL-2630): the card then reads every\n * site the mount lists, one window each (AGL-2634).\n */\n hostId: string | null\n}\n\nconst LEAD_FUNNEL_HELP = Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#lead-funnel',\n excerpt:\n 'Of the leads this site captured in the period, how many are ' +\n 'still new, being worked, qualified or unqualified — and the ' +\n 'reasons the unqualified ones were closed. Placed by when each ' +\n 'lead was first seen.',\n})\n\n/**\n * The leads a scope may see, first seen in a range (AGL-3275).\n *\n * `visibleTo` is the site's tokens under a site, and `null` at the\n * organization level where an org-wide member reads without a clause. The\n * range stays a single-field one; with the scope clause it composes with the\n * `visibleTo` + `firstSeenAtMs` entries in `cloud/firebase-firestore.indexes.json`\n * — the descending one for the window, the ascending one for the counts,\n * which name no order (AGL-3443).\n */\nexport function leadsBetween(\n firestore: Firestore,\n orgId: string,\n visibleTo: readonly string[] | null,\n from: number,\n to: number,\n) {\n return query(\n collection(firestore, 'orgs', orgId, 'leads'),\n ...(visibleTo ? [where('visibleTo', 'array-contains-any', [...visibleTo])] : []),\n where('firstSeenAtMs', '>=', from),\n where('firstSeenAtMs', '<', to),\n )\n}\n\nconst countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n/**\n * The lead funnel, at whichever level the report is mounted (AGL-2624,\n * AGL-2630, AGL-2634).\n *\n * Under a site: that site. At the organization level: every site the\n * mount lists, one window each, totaled — the org hub's reports total\n * every site, and the leads are no exception now that the fan-out is\n * bounded by the org's site list. With no sites to read the card says so\n * instead of guessing.\n */\nexport function LeadFunnelCard(props: LeadFunnelCardProps) {\n const { report, hostId } = props\n const mount = useCrmOrgMount()\n if (hostId) return <SiteLeadFunnelCard report={report} hostId={hostId} />\n if (mount && (!mount.hostsReady || mount.hosts.length > 0)) {\n return <OrgLeadFunnelCard report={report} />\n }\n return (\n <CardDisplay header={'Lead funnel'} help={LEAD_FUNNEL_HELP} contentGutterX contentGutterY>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mount\n ? 'Leads live under a site, and this organization has no sites yet.'\n : 'Leads live under a site, and this report is mounted under none.'}\n </Typography>\n </CardDisplay>\n )\n}\n\n/**\n * The lead funnel (AGL-2624): of the leads this site captured in the\n * period, how many are still new, being worked, qualified or unqualified —\n * and why the unqualified ones were closed.\n *\n * A cohort by first sighting, followed to where each lead stands now,\n * because that is the question a period asks of leads — \"what became of\n * the ones we got\" — and because a lead's status has no history on the\n * document: only its current value. The window is a range on\n * `firstSeenAtMs`, which the capture door stamps once on creation and\n * never again, so a returning visitor's second form is not a second lead\n * in a second period. A single-field range needs no composite index.\n *\n * Read directly off `hosts/{hostId}/leads` under the site's own rules —\n * any member of the site may read it — the way the Leads section reads\n * it, so the funnel and the list can never disagree about who may see a\n * lead.\n */\nfunction SiteLeadFunnelCard(props: { report: CrmReportScope; hostId: string }) {\n const { report, hostId } = props\n const { range } = report\n const firestore = useFirestore()\n\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], report.tokens, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:counts:${hostId}`) },\n )\n const window = useWindowRead<LeadRow>(\n () =>\n query(\n leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n LEAD_CEILING,\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:window:${hostId}`) },\n )\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n (figures !== null ? ` of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'This site’s leads could not be read.'}\n />\n )\n}\nSiteLeadFunnelCard.displayName = 'SiteLeadFunnelCard'\n\n/**\n * THE ORGANIZATION'S FUNNEL (AGL-2634, AGL-3275): every lead the org holds.\n *\n * This totaled one aggregate and one window PER SITE, because the sites'\n * lead collections were disjoint. They share one collection now, so a sum\n * would count a lead that two brands in a consent group both hold once for\n * each of them — and the merge that existed so a busy site could not crowd\n * out a quiet one has nothing left to balance. One aggregate and one\n * bounded read, unscoped, as an org-wide member's reads are.\n */\nfunction OrgLeadFunnelCard(props: { report: CrmReportScope }) {\n const { report } = props\n const { range } = report\n const firestore = useFirestore()\n const mount = useCrmOrgMount()\n const hostIds = useMemo(\n () => (mount?.hostsReady ? mount.hosts.map((host) => host.id) : null),\n [mount],\n )\n\n /*\n * ONE READ, not one per site (AGL-3275). The sites share a collection now,\n * so a lead two brands in a consent group both hold would be counted once\n * for each of them — and the org's funnel would report more leads than the\n * org has. `null` tokens is the org-wide read that carries no clause.\n */\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], null, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], null, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:counts:org') },\n )\n const merged = useAggregateRead<{ rows: LeadRow[]; truncated: boolean }>(\n () =>\n getDocs(\n query(\n leadsBetween(firestore, report.scope[1], null, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n ).then((snapshot) =>\n ceilingedWindow(\n snapshot.docs.map(\n (document) => ({ ...document.data(), $id: document.id }) as unknown as LeadRow,\n ),\n LEAD_CEILING,\n ),\n ),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:window:org') },\n )\n const window = useMemo<WindowRead<LeadRow>>(\n () => ({\n rows: merged.value?.rows ?? [],\n truncated: merged.value?.truncated ?? false,\n status: merged.status,\n }),\n [merged],\n )\n const sites = hostIds?.length ?? 0\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n subheader={`Every site (${sites.toLocaleString()})`}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n ` — at most ${LEAD_CEILING.toLocaleString()}` +\n (figures !== null ? ` — of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'The organization’s leads could not be read.'}\n />\n )\n}\nOrgLeadFunnelCard.displayName = 'OrgLeadFunnelCard'\n\n/**\n * The funnel as drawn, whichever level fed it: four tiles, the statuses,\n * the reasons, the export. Pure over the two reads.\n */\nfunction LeadFunnelBody(props: {\n report: CrmReportScope\n subheader?: string\n captured: AggregateRead<CapturedFigures>\n window: WindowRead<LeadRow>\n caption?: string\n errorText: string\n}) {\n const { report, subheader, captured, window, caption, errorText } = props\n const { period, routes } = report\n const status = window.status\n\n const funnel = useMemo(() => leadFunnel(window.rows), [window])\n // Each status counted by its MEANING, named as the org names it (AGL-3512).\n const { picklist: statuses } = useLeadStatusPicklist(report.scope[1])\n const statusName = (status: Aglyn.CrmLeadStatus) => Aglyn.crmLeadStatusLabelFor(statuses, status)\n const read = status === 'success'\n const figures = captured.value\n const share = (count: number, of: number): string | null =>\n of > 0 ? percent(count / of) : null\n const reasons = funnel.reasons.slice(0, REASONS_SHOWN)\n const moreReasons = funnel.reasons.length - reasons.length\n\n return (\n <CardDisplay\n header={'Lead funnel'}\n subheader={subheader}\n help={LEAD_FUNNEL_HELP}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <ReportExport\n filename={reportFilename('lead-funnel', period)}\n columns={COLUMNS}\n rows={() => [\n ...Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => [\n 'Status',\n statusName(leadStatus),\n funnel.byStatus[leadStatus],\n share(funnel.byStatus[leadStatus], funnel.total) ?? '',\n ]),\n ...funnel.reasons.map((reason) => [\n 'Unqualified reason',\n reason.label,\n reason.count,\n share(reason.count, funnel.byStatus.unqualified) ?? '',\n ]),\n ]}\n disabled={!read || !funnel.total}\n />\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('leads')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open leads'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Leads captured'}\n value={figures ? figures.current.toLocaleString() : null}\n deltaPct={figures ? deltaPercent(figures.current, figures.previous) : null}\n note={'first seen in the period'}\n href={routes.section('leads')}\n />\n <ReportStatTile\n label={statusName('qualified')}\n value={read ? funnel.byStatus.qualified.toLocaleString() : null}\n note={\n funnel.qualifiedRate === null\n ? read\n ? 'nobody captured in the period'\n : undefined\n : `${percent(funnel.qualifiedRate)} of those captured`\n }\n />\n <ReportStatTile\n label={statusName('unqualified')}\n value={read ? funnel.byStatus.unqualified.toLocaleString() : null}\n note={\n funnel.unqualifiedRate === null\n ? undefined\n : `${percent(funnel.unqualifiedRate)} of those captured`\n }\n riseIsGood={false}\n />\n <ReportStatTile\n label={'Still open'}\n value={read ? funnel.open.toLocaleString() : null}\n note={'new or being worked'}\n href={routes.section('leads')}\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{errorText}</Alert>\n ) : null}\n <Section title={'Where they stand'}>\n <ReportBreakdown\n rows={Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => ({\n key: leadStatus,\n label: statusName(leadStatus),\n value: funnel.byStatus[leadStatus],\n note: share(funnel.byStatus[leadStatus], funnel.total) ?? undefined,\n color:\n leadStatus === 'qualified'\n ? 'success.main'\n : leadStatus === 'unqualified'\n ? 'warning.main'\n : undefined,\n }))}\n emptyText={''}\n />\n {read && !funnel.total ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No leads captured in this period.'}\n </Typography>\n ) : null}\n </Section>\n <Section title={'Why leads were unqualified'}>\n <ReportBreakdown\n rows={reasons.map((reason) => ({\n key: reason.key,\n label: reason.label,\n value: reason.count,\n note: share(reason.count, funnel.byStatus.unqualified) ?? undefined,\n color: 'warning.main',\n }))}\n emptyText={\n status === 'loading' ? 'Reading…' : 'No lead was unqualified in this period.'\n }\n />\n {moreReasons > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`And ${plural(moreReasons, 'other reason')}, each given once or twice.`}\n </Typography>\n ) : null}\n </Section>\n {/* What the funnel, and so the export, is placed from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadFunnelBody.displayName = 'LeadFunnelBody'\nLeadFunnelCard.displayName = 'LeadFunnelCard'\n\nexport default LeadFunnelCard\n"],"names":["Aglyn","AppLink","CardDisplay","percent","Section","ceilingedWindow","Alert","Button","Stack","Typography","collection","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","useCrmOrgMount","useLeadStatusPicklist","ReportBreakdown","ReportExport","plural","reportFilename","reportCacheKey","ReportStatTile","useAggregateRead","useWindowRead","deltaPercent","leadFunnel","LEAD_CEILING","REASONS_SHOWN","COLUMNS","LEAD_FUNNEL_HELP","pluginDocsHelp","anchor","excerpt","leadsBetween","firestore","orgId","visibleTo","from","to","countOf","target","then","snapshot","data","count","LeadFunnelCard","props","report","hostId","mount","SiteLeadFunnelCard","hostsReady","hosts","length","OrgLeadFunnelCard","header","help","contentGutterX","contentGutterY","variant","color","range","captured","Promise","all","scope","tokens","previousFrom","previousTo","current","previous","cacheKey","window","figures","value","sampled","status","truncated","rows","LeadFunnelBody","caption","toLocaleString","undefined","errorText","displayName","hostIds","map","host","id","merged","docs","document","$id","sites","subheader","period","routes","funnel","picklist","statuses","statusName","crmLeadStatusLabelFor","read","share","of","reasons","slice","moreReasons","HeaderProps","action","direction","spacing","filename","columns","CRM_LEAD_STATUSES","leadStatus","byStatus","total","reason","label","unqualified","disabled","component","componentVariant","nativeButton","href","section","size","useFlexGap","sx","flexWrap","deltaPct","note","qualified","qualifiedRate","unqualifiedRate","riseIsGood","open","severity","title","key","emptyText"],"mappings":"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"}
|
|
@@ -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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"}
|
|
@@ -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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"}
|