@aglyn/plugins-crm 1.0.0-beta.199 → 1.0.0-beta.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/activity-list.d.ts +36 -5
  3. package/src/lib/components/activity-list.js +159 -137
  4. package/src/lib/components/activity-list.js.map +1 -1
  5. package/src/lib/components/activity-timeline.d.ts +84 -0
  6. package/src/lib/components/activity-timeline.js +224 -0
  7. package/src/lib/components/activity-timeline.js.map +1 -0
  8. package/src/lib/components/companies-section.d.ts +6 -6
  9. package/src/lib/components/companies-section.js +54 -84
  10. package/src/lib/components/companies-section.js.map +1 -1
  11. package/src/lib/components/company-edit-drawer.js +17 -4
  12. package/src/lib/components/company-edit-drawer.js.map +1 -1
  13. package/src/lib/components/company-picker.js +6 -2
  14. package/src/lib/components/company-picker.js.map +1 -1
  15. package/src/lib/components/company-properties-card.js +53 -36
  16. package/src/lib/components/company-properties-card.js.map +1 -1
  17. package/src/lib/components/contact-associations-card.d.ts +3 -2
  18. package/src/lib/components/contact-associations-card.js +12 -12
  19. package/src/lib/components/contact-associations-card.js.map +1 -1
  20. package/src/lib/components/contact-detail-page.d.ts +4 -3
  21. package/src/lib/components/contact-detail-page.js +18 -12
  22. package/src/lib/components/contact-detail-page.js.map +1 -1
  23. package/src/lib/components/contact-list-columns.d.ts +3 -5
  24. package/src/lib/components/contact-list-columns.js +6 -8
  25. package/src/lib/components/contact-list-columns.js.map +1 -1
  26. package/src/lib/components/contact-properties-card.d.ts +21 -4
  27. package/src/lib/components/contact-properties-card.js +129 -29
  28. package/src/lib/components/contact-properties-card.js.map +1 -1
  29. package/src/lib/components/contact-timeline-card.d.ts +12 -6
  30. package/src/lib/components/contact-timeline-card.js +132 -174
  31. package/src/lib/components/contact-timeline-card.js.map +1 -1
  32. package/src/lib/components/contacts-bulk-bar.js +10 -1
  33. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  34. package/src/lib/components/contacts-section.js +282 -331
  35. package/src/lib/components/contacts-section.js.map +1 -1
  36. package/src/lib/components/crm-email-state-chip.js +4 -1
  37. package/src/lib/components/crm-email-state-chip.js.map +1 -1
  38. package/src/lib/components/crm-next-activity-column.d.ts +4 -2
  39. package/src/lib/components/crm-next-activity-column.js +6 -3
  40. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  41. package/src/lib/components/deal-edit-drawer.js +8 -4
  42. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  43. package/src/lib/components/deal-products-card.js +15 -13
  44. package/src/lib/components/deal-products-card.js.map +1 -1
  45. package/src/lib/components/deal-properties-card.js +7 -0
  46. package/src/lib/components/deal-properties-card.js.map +1 -1
  47. package/src/lib/components/deal-stage-card.js +31 -29
  48. package/src/lib/components/deal-stage-card.js.map +1 -1
  49. package/src/lib/components/deals-section.d.ts +2 -2
  50. package/src/lib/components/deals-section.js +67 -97
  51. package/src/lib/components/deals-section.js.map +1 -1
  52. package/src/lib/components/fields-section.d.ts +4 -0
  53. package/src/lib/components/fields-section.js +368 -154
  54. package/src/lib/components/fields-section.js.map +1 -1
  55. package/src/lib/components/form-contact-fields-card.js +24 -20
  56. package/src/lib/components/form-contact-fields-card.js.map +1 -1
  57. package/src/lib/components/lead-campaigns-card.d.ts +1 -1
  58. package/src/lib/components/lead-campaigns-card.js +43 -38
  59. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  60. package/src/lib/components/lead-properties-card.d.ts +14 -6
  61. package/src/lib/components/lead-properties-card.js +323 -311
  62. package/src/lib/components/lead-properties-card.js.map +1 -1
  63. package/src/lib/components/lead-surfaces-note.js +13 -2
  64. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  65. package/src/lib/components/leads-bulk-bar.js +16 -3
  66. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  67. package/src/lib/components/leads-section.d.ts +2 -1
  68. package/src/lib/components/leads-section.js +110 -148
  69. package/src/lib/components/leads-section.js.map +1 -1
  70. package/src/lib/components/linked-deals-card.js +12 -10
  71. package/src/lib/components/linked-deals-card.js.map +1 -1
  72. package/src/lib/components/recent-activity-feed.d.ts +12 -8
  73. package/src/lib/components/recent-activity-feed.js +54 -29
  74. package/src/lib/components/recent-activity-feed.js.map +1 -1
  75. package/src/lib/components/record-activity-card.d.ts +4 -2
  76. package/src/lib/components/record-activity-card.js +29 -11
  77. package/src/lib/components/record-activity-card.js.map +1 -1
  78. package/src/lib/components/reports/activity-card.js +18 -12
  79. package/src/lib/components/reports/activity-card.js.map +1 -1
  80. package/src/lib/components/reports/lead-funnel-card.js +47 -37
  81. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  82. package/src/lib/components/reports/pipeline-card.js +22 -16
  83. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  84. package/src/lib/components/reports/report-export.d.ts +4 -11
  85. package/src/lib/components/reports/report-export.js +16 -33
  86. package/src/lib/components/reports/report-export.js.map +1 -1
  87. package/src/lib/components/reports/source-conversion-card.js +18 -12
  88. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  89. package/src/lib/components/reports/tasks-card.js +35 -25
  90. package/src/lib/components/reports/tasks-card.js.map +1 -1
  91. package/src/lib/components/tasks-section.js +151 -144
  92. package/src/lib/components/tasks-section.js.map +1 -1
  93. package/src/lib/constants/company-filters.d.ts +13 -0
  94. package/src/lib/constants/company-filters.js +47 -17
  95. package/src/lib/constants/company-filters.js.map +1 -1
  96. package/src/lib/constants/contact-filters.d.ts +61 -10
  97. package/src/lib/constants/contact-filters.js +373 -141
  98. package/src/lib/constants/contact-filters.js.map +1 -1
  99. package/src/lib/constants/deal-filters.d.ts +44 -0
  100. package/src/lib/constants/deal-filters.js +78 -0
  101. package/src/lib/constants/deal-filters.js.map +1 -0
  102. package/src/lib/constants/field-list-query.d.ts +23 -0
  103. package/src/lib/constants/field-list-query.js +80 -0
  104. package/src/lib/constants/field-list-query.js.map +1 -0
  105. package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
  106. package/src/lib/hooks/use-crm-list-query.js +182 -0
  107. package/src/lib/hooks/use-crm-list-query.js.map +1 -0
  108. package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
  109. package/src/lib/hooks/use-crm-tasks.js +44 -42
  110. package/src/lib/hooks/use-crm-tasks.js.map +1 -1
  111. package/src/lib/hooks/use-deals.d.ts +2 -34
  112. package/src/lib/hooks/use-deals.js +3 -62
  113. package/src/lib/hooks/use-deals.js.map +1 -1
  114. package/src/lib/model/crm-list-query.d.ts +91 -0
  115. package/src/lib/model/crm-list-query.js +182 -0
  116. package/src/lib/model/crm-list-query.js.map +1 -0
  117. package/src/lib/model/lead-filters.d.ts +50 -44
  118. package/src/lib/model/lead-filters.js +194 -88
  119. package/src/lib/model/lead-filters.js.map +1 -1
  120. package/src/lib/model/list-fields-api.d.ts +26 -0
  121. package/src/lib/model/list-fields-api.js +47 -0
  122. package/src/lib/model/list-fields-api.js.map +1 -0
  123. package/src/lib/model/list-fields.d.ts +47 -0
  124. package/src/lib/model/list-fields.js +39 -0
  125. package/src/lib/model/list-fields.js.map +1 -0
  126. package/src/lib/model/task-views.d.ts +15 -10
  127. package/src/lib/model/task-views.js +68 -9
  128. package/src/lib/model/task-views.js.map +1 -1
  129. package/src/lib/server/capture-contact.js +7 -5
  130. package/src/lib/server/capture-contact.js.map +1 -1
  131. package/src/lib/server/companies-import.js +3 -3
  132. package/src/lib/server/companies-import.js.map +1 -1
  133. package/src/lib/server/company-delete.js +3 -1
  134. package/src/lib/server/company-delete.js.map +1 -1
  135. package/src/lib/server/consent-group-participant.js +4 -0
  136. package/src/lib/server/consent-group-participant.js.map +1 -1
  137. package/src/lib/server/contact-update.js +14 -8
  138. package/src/lib/server/contact-update.js.map +1 -1
  139. package/src/lib/server/contacts-import.js +4 -2
  140. package/src/lib/server/contacts-import.js.map +1 -1
  141. package/src/lib/server/deals-import.js +4 -2
  142. package/src/lib/server/deals-import.js.map +1 -1
  143. package/src/lib/server/erase-person.js +2 -1
  144. package/src/lib/server/erase-person.js.map +1 -1
  145. package/src/lib/server/lead-create.js +4 -1
  146. package/src/lib/server/lead-create.js.map +1 -1
  147. package/src/lib/server/lead-source-values.js +9 -5
  148. package/src/lib/server/lead-source-values.js.map +1 -1
  149. package/src/lib/server/leads-import.js +4 -1
  150. package/src/lib/server/leads-import.js.map +1 -1
  151. package/src/lib/server/list-fields-routes.d.ts +29 -0
  152. package/src/lib/server/list-fields-routes.js +85 -0
  153. package/src/lib/server/list-fields-routes.js.map +1 -0
  154. package/src/lib/server/record-email-state.js +5 -2
  155. package/src/lib/server/record-email-state.js.map +1 -1
  156. package/src/lib/server/record-timeline.js +3 -2
  157. package/src/lib/server/record-timeline.js.map +1 -1
  158. package/src/lib/server/task-routes.js +6 -3
  159. package/src/lib/server/task-routes.js.map +1 -1
  160. package/src/lib/server/tasks-import.js +5 -2
  161. package/src/lib/server/tasks-import.js.map +1 -1
  162. package/src/lib/server.js +10 -1
  163. package/src/lib/server.js.map +1 -1
  164. package/src/lib/components/contact-custom-fields-card.d.ts +0 -39
  165. package/src/lib/components/contact-custom-fields-card.js +0 -204
  166. package/src/lib/components/contact-custom-fields-card.js.map +0 -1
  167. package/src/lib/hooks/use-org-leads.d.ts +0 -50
  168. package/src/lib/hooks/use-org-leads.js +0 -104
  169. package/src/lib/hooks/use-org-leads.js.map +0 -1
@@ -161,6 +161,23 @@ const TOP_DEALS = 10;
161
161
  }),
162
162
  contentGutterX: true,
163
163
  contentGutterY: true,
164
+ HeaderProps: {
165
+ action: /*#__PURE__*/ _jsx(ReportExport, {
166
+ filename: reportFilename('top-open-deals'),
167
+ columns: TOP_DEAL_COLUMNS,
168
+ rows: ()=>summary.top.map((deal)=>{
169
+ var _deal_amountCents;
170
+ return [
171
+ deal.title || deal.$id,
172
+ summary.stageName(deal),
173
+ typeof deal.expectedCloseAtMs === 'number' && deal.expectedCloseAtMs > 0 ? new Date(deal.expectedCloseAtMs).toISOString().slice(0, 10) : '',
174
+ (Number((_deal_amountCents = deal.amountCents) != null ? _deal_amountCents : 0) / 100).toFixed(2),
175
+ String(deal.currency || currency).toUpperCase()
176
+ ];
177
+ }),
178
+ disabled: !dealsRead || !summary.top.length
179
+ })
180
+ },
164
181
  children: /*#__PURE__*/ _jsxs(Stack, {
165
182
  spacing: 2,
166
183
  children: [
@@ -310,22 +327,11 @@ const TOP_DEALS = 10;
310
327
  })
311
328
  ]
312
329
  }),
313
- /*#__PURE__*/ _jsx(ReportExport, {
314
- filename: reportFilename('top-open-deals'),
315
- columns: TOP_DEAL_COLUMNS,
316
- rows: ()=>summary.top.map((deal)=>{
317
- var _deal_amountCents;
318
- return [
319
- deal.title || deal.$id,
320
- summary.stageName(deal),
321
- typeof deal.expectedCloseAtMs === 'number' && deal.expectedCloseAtMs > 0 ? new Date(deal.expectedCloseAtMs).toISOString().slice(0, 10) : '',
322
- (Number((_deal_amountCents = deal.amountCents) != null ? _deal_amountCents : 0) / 100).toFixed(2),
323
- String(deal.currency || currency).toUpperCase()
324
- ];
325
- }),
326
- disabled: !dealsRead,
327
- caption: dealWindow.truncated ? `Ranked within the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.` : undefined
328
- })
330
+ dealWindow.truncated ? /*#__PURE__*/ _jsx(Typography, {
331
+ variant: "caption",
332
+ color: "text.secondary",
333
+ children: `Ranked within the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.`
334
+ }) : null
329
335
  ]
330
336
  }) : null
331
337
  ]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/pipeline-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { collectionCeiling } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename, shortDate } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\n\n/**\n * How many open deals the by-stage chart, the forecast and the top-deals\n * table are read from.\n *\n * The count and the face value of the pipeline are server aggregates and\n * never meet this bound. The forecast cannot be: it weights each deal by\n * ITS stage's odds, which is a join the server will not do, so it reads the\n * deals and says so when there were more than this.\n */\nconst OPEN_DEAL_CEILING = 1000\n/** More pipelines than this is a shape nobody has asked for; the probe says if it happens. */\nconst PIPELINE_CEILING = 20\nconst TOP_DEALS = 10\n\n/** The top-deals table's columns, which are also the CSV's — the amount split from its currency there. */\nconst TOP_DEAL_COLUMNS = ['Deal', 'Stage', 'Expected close', 'Amount', 'Currency'] as const\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\ntype PipelineRow = Aglyn.CrmPipeline & { $id: string }\n\nexport interface PipelineCardProps {\n report: CrmReportScope\n}\n\n/**\n * The open pipeline: how many deals, what they add up to, what they are\n * worth at their stage odds, where they sit, and the largest of them\n * (AGL-2604).\n *\n * Not period-scoped, deliberately. A pipeline is a stock, not a flow — what\n * is open NOW — and a \"pipeline over the last 30 days\" would be a report on\n * deals created in a window, which is a different question with a different\n * answer. The period picker governs the cards that measure flows.\n */\nexport function PipelineCard(props: PipelineCardProps) {\n const { report } = props\n const { scope, tokens, routes, nowMs } = report\n const firestore = useFirestore()\n\n const open = useAggregateRead(\n () =>\n getAggregateFromServer(\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n ),\n { count: count(), amountCents: sum('amountCents') },\n ).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:open') },\n )\n\n /**\n * The open deals, most recently updated first — the order the\n * `(visibleTo, status, updatedAt)` index the deals list already uses can\n * serve, so the report adds no index for this read.\n */\n const dealWindow = useWindowRead<DealRow>(\n () =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n orderBy('updatedAt', 'desc'),\n limit(OPEN_DEAL_CEILING + 1),\n ),\n OPEN_DEAL_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:deals') },\n )\n const pipelineWindow = useWindowRead<PipelineRow>(\n () =>\n collectionCeiling(\n query(\n scopedCollection(firestore, scope, 'pipelines'),\n ...visibleToClause(tokens),\n ),\n PIPELINE_CEILING,\n ),\n PIPELINE_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:pipelines') },\n )\n const dealsStatus = dealWindow.status\n\n const summary = useMemo(() => {\n const byPipeline = new Map<string, DealRow[]>()\n for (const deal of dealWindow.rows) {\n const key = deal.pipelineId ?? ''\n const list = byPipeline.get(key) ?? []\n list.push(deal)\n byPipeline.set(key, list)\n }\n // Every active pipeline is charted even with nothing open; an archived\n // one only while a deal still sits open in it — the forecast card's\n // rule, so the two cards name the same pipelines.\n const pipelines = pipelineWindow.rows\n .filter((pipeline) => !Aglyn.isPipelineArchived(pipeline) || byPipeline.has(pipeline.$id))\n .map((pipeline) => ({\n pipeline,\n totals: Aglyn.pipelineTotals(byPipeline.get(pipeline.$id) ?? [], pipeline),\n }))\n const known = new Set(pipelineWindow.rows.map((pipeline) => pipeline.$id))\n const orphans = dealWindow.rows.filter(\n (deal) => !known.has(deal.pipelineId ?? ''),\n )\n const stageName = (deal: DealRow): string =>\n Aglyn.dealStageById(\n pipelineWindow.rows.find((pipeline) => pipeline.$id === deal.pipelineId),\n deal.stageId,\n )?.name ?? deal.stageId\n const top = [...dealWindow.rows]\n .sort(\n (a, b) => Number(b.amountCents ?? 0) - Number(a.amountCents ?? 0),\n )\n .slice(0, TOP_DEALS)\n return {\n pipelines,\n orphanTotals: Aglyn.pipelineTotals(orphans, null),\n weightedCents: pipelines.reduce(\n (total, entry) => total + entry.totals.weightedCents,\n 0,\n ),\n top,\n stageName,\n currency: Aglyn.currencyOfDeals(dealWindow.rows),\n }\n }, [dealWindow, pipelineWindow])\n // Open deals with nothing scheduled (AGL-2661), off the same window.\n const stuck = useMemo(() => Aglyn.stuckDeals(dealWindow.rows), [dealWindow.rows])\n\n const currency = summary.currency.currency\n const dealsRead = dealsStatus === 'success'\n\n return (\n <CardDisplay\n header={'Pipeline'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#pipeline',\n excerpt:\n 'Every open deal: how many, what they add up to, and what they ' +\n 'are worth once each is weighted by the odds of its stage. Not ' +\n 'tied to the period — a pipeline is what is open now.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open deals'}\n value={open.value ? open.value.count.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Pipeline value'}\n value={open.value ? money(open.value.amountCents, currency) : null}\n note={'face value of every open deal'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Weighted forecast'}\n value={dealsRead ? money(summary.weightedCents, currency) : null}\n note={\n dealWindow.truncated\n ? `from the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated`\n : 'each deal at its stage odds'\n }\n />\n {/*\n Open deals with no open task against them (AGL-2661): the ones\n nobody has a next step for. Painted as a warning above zero,\n because a deal with nothing planned is a deal going nowhere; the\n tile opens the deals list, whose \"No next activity\" chip lists\n them.\n */}\n <ReportStatTile\n label={'Stuck deals'}\n value={dealsRead ? stuck.count.toLocaleString() : null}\n note={'open, with no next activity'}\n riseIsGood={false}\n color={stuck.count > 0 ? 'warning.main' : undefined}\n href={routes.section('deals')}\n />\n </Stack>\n {open.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The deal totals could not be read.'}\n </Alert>\n ) : null}\n {summary.currency.mixed ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Deals are in more than one currency; the totals add them as numbers and show ${currency.toUpperCase()}, the most common.`}\n </Typography>\n ) : null}\n {dealsRead && !summary.pipelines.length && !dealWindow.rows.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No open deals yet.'}\n </Typography>\n ) : null}\n {summary.pipelines.map(({ pipeline, totals }) => (\n <Section key={pipeline.$id} title={pipeline.name || 'Pipeline'}>\n <ReportBreakdown\n rows={[\n ...totals.stages.map((row) => ({\n key: row.stage.id,\n label: row.stage.name,\n value: row.amountCents,\n display: money(row.amountCents, currency),\n note: `${plural(row.count, 'deal')} · ${money(row.weightedCents, currency)} weighted`,\n })),\n ...(totals.unplaced.count\n ? [\n {\n key: '$unplaced',\n label: 'Stage no longer in this pipeline',\n value: totals.unplaced.amountCents,\n display: money(totals.unplaced.amountCents, currency),\n note: `${plural(totals.unplaced.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]\n : []),\n ]}\n emptyText={'This pipeline has no open stages.'}\n />\n </Section>\n ))}\n {summary.orphanTotals.count ? (\n <Section title={'Deals in no pipeline'}>\n <ReportBreakdown\n rows={[\n {\n key: '$orphans',\n label: 'No pipeline',\n value: summary.orphanTotals.amountCents,\n display: money(summary.orphanTotals.amountCents, currency),\n note: `${plural(summary.orphanTotals.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]}\n emptyText={''}\n />\n </Section>\n ) : null}\n {pipelineWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Only the first ${PIPELINE_CEILING} pipelines are charted.`}\n </Typography>\n ) : null}\n {summary.top.length ? (\n <Section title={'Top open deals'}>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Deal'}</TableCell>\n <TableCell>{'Stage'}</TableCell>\n <TableCell>{'Expected close'}</TableCell>\n <TableCell align=\"right\">{'Amount'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {summary.top.map((deal) => (\n <TableRow key={deal.$id}>\n <TableCell>\n <AppLink href={routes.deal(deal.$id)}>\n {deal.title || deal.$id}\n </AppLink>\n </TableCell>\n <TableCell>{summary.stageName(deal)}</TableCell>\n <TableCell>{shortDate(deal.expectedCloseAtMs)}</TableCell>\n <TableCell align=\"right\">\n {money(\n Number(deal.amountCents ?? 0),\n String(deal.currency || currency),\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n <ReportExport\n filename={reportFilename('top-open-deals')}\n columns={TOP_DEAL_COLUMNS}\n rows={() =>\n summary.top.map((deal) => [\n deal.title || deal.$id,\n summary.stageName(deal),\n typeof deal.expectedCloseAtMs === 'number' && deal.expectedCloseAtMs > 0\n ? new Date(deal.expectedCloseAtMs).toISOString().slice(0, 10)\n : '',\n (Number(deal.amountCents ?? 0) / 100).toFixed(2),\n String(deal.currency || currency).toUpperCase(),\n ])\n }\n disabled={!dealsRead}\n caption={\n dealWindow.truncated\n ? `Ranked within the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.`\n : undefined\n }\n />\n </Section>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nPipelineCard.displayName = 'PipelineCard'\n\nexport default PipelineCard\n"],"names":["Aglyn","money","AppLink","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","count","getAggregateFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","collectionCeiling","ReportBreakdown","ReportExport","plural","reportFilename","shortDate","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","OPEN_DEAL_CEILING","PIPELINE_CEILING","TOP_DEALS","TOP_DEAL_COLUMNS","PipelineCard","props","report","scope","tokens","routes","nowMs","firestore","open","amountCents","then","snapshot","Number","data","cacheKey","dealWindow","pipelineWindow","dealsStatus","status","summary","byPipeline","Map","deal","rows","key","pipelineId","list","get","push","set","pipelines","filter","pipeline","isPipelineArchived","has","$id","map","totals","pipelineTotals","known","Set","orphans","stageName","dealStageById","find","stageId","name","top","sort","a","b","slice","orphanTotals","weightedCents","reduce","total","entry","currency","currencyOfDeals","stuck","stuckDeals","dealsRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","direction","sx","flexWrap","label","value","toLocaleString","note","href","section","truncated","riseIsGood","color","undefined","severity","mixed","variant","toUpperCase","length","title","stages","row","stage","id","display","unplaced","emptyText","size","align","expectedCloseAtMs","String","filename","columns","Date","toISOString","toFixed","disabled","caption","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/pipeline-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { collectionCeiling } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename, shortDate } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\n\n/**\n * How many open deals the by-stage chart, the forecast and the top-deals\n * table are read from.\n *\n * The count and the face value of the pipeline are server aggregates and\n * never meet this bound. The forecast cannot be: it weights each deal by\n * ITS stage's odds, which is a join the server will not do, so it reads the\n * deals and says so when there were more than this.\n */\nconst OPEN_DEAL_CEILING = 1000\n/** More pipelines than this is a shape nobody has asked for; the probe says if it happens. */\nconst PIPELINE_CEILING = 20\nconst TOP_DEALS = 10\n\n/** The top-deals table's columns, which are also the CSV's — the amount split from its currency there. */\nconst TOP_DEAL_COLUMNS = ['Deal', 'Stage', 'Expected close', 'Amount', 'Currency'] as const\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\ntype PipelineRow = Aglyn.CrmPipeline & { $id: string }\n\nexport interface PipelineCardProps {\n report: CrmReportScope\n}\n\n/**\n * The open pipeline: how many deals, what they add up to, what they are\n * worth at their stage odds, where they sit, and the largest of them\n * (AGL-2604).\n *\n * Not period-scoped, deliberately. A pipeline is a stock, not a flow — what\n * is open NOW — and a \"pipeline over the last 30 days\" would be a report on\n * deals created in a window, which is a different question with a different\n * answer. The period picker governs the cards that measure flows.\n */\nexport function PipelineCard(props: PipelineCardProps) {\n const { report } = props\n const { scope, tokens, routes, nowMs } = report\n const firestore = useFirestore()\n\n const open = useAggregateRead(\n () =>\n getAggregateFromServer(\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n ),\n { count: count(), amountCents: sum('amountCents') },\n ).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:open') },\n )\n\n /**\n * The open deals, most recently updated first — the order the\n * `(visibleTo, status, updatedAt)` index the deals list already uses can\n * serve, so the report adds no index for this read.\n */\n const dealWindow = useWindowRead<DealRow>(\n () =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n orderBy('updatedAt', 'desc'),\n limit(OPEN_DEAL_CEILING + 1),\n ),\n OPEN_DEAL_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:deals') },\n )\n const pipelineWindow = useWindowRead<PipelineRow>(\n () =>\n collectionCeiling(\n query(\n scopedCollection(firestore, scope, 'pipelines'),\n ...visibleToClause(tokens),\n ),\n PIPELINE_CEILING,\n ),\n PIPELINE_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'pipeline:pipelines') },\n )\n const dealsStatus = dealWindow.status\n\n const summary = useMemo(() => {\n const byPipeline = new Map<string, DealRow[]>()\n for (const deal of dealWindow.rows) {\n const key = deal.pipelineId ?? ''\n const list = byPipeline.get(key) ?? []\n list.push(deal)\n byPipeline.set(key, list)\n }\n // Every active pipeline is charted even with nothing open; an archived\n // one only while a deal still sits open in it — the forecast card's\n // rule, so the two cards name the same pipelines.\n const pipelines = pipelineWindow.rows\n .filter((pipeline) => !Aglyn.isPipelineArchived(pipeline) || byPipeline.has(pipeline.$id))\n .map((pipeline) => ({\n pipeline,\n totals: Aglyn.pipelineTotals(byPipeline.get(pipeline.$id) ?? [], pipeline),\n }))\n const known = new Set(pipelineWindow.rows.map((pipeline) => pipeline.$id))\n const orphans = dealWindow.rows.filter(\n (deal) => !known.has(deal.pipelineId ?? ''),\n )\n const stageName = (deal: DealRow): string =>\n Aglyn.dealStageById(\n pipelineWindow.rows.find((pipeline) => pipeline.$id === deal.pipelineId),\n deal.stageId,\n )?.name ?? deal.stageId\n const top = [...dealWindow.rows]\n .sort(\n (a, b) => Number(b.amountCents ?? 0) - Number(a.amountCents ?? 0),\n )\n .slice(0, TOP_DEALS)\n return {\n pipelines,\n orphanTotals: Aglyn.pipelineTotals(orphans, null),\n weightedCents: pipelines.reduce(\n (total, entry) => total + entry.totals.weightedCents,\n 0,\n ),\n top,\n stageName,\n currency: Aglyn.currencyOfDeals(dealWindow.rows),\n }\n }, [dealWindow, pipelineWindow])\n // Open deals with nothing scheduled (AGL-2661), off the same window.\n const stuck = useMemo(() => Aglyn.stuckDeals(dealWindow.rows), [dealWindow.rows])\n\n const currency = summary.currency.currency\n const dealsRead = dealsStatus === 'success'\n\n return (\n <CardDisplay\n header={'Pipeline'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#pipeline',\n excerpt:\n 'Every open deal: how many, what they add up to, and what they ' +\n 'are worth once each is weighted by the odds of its stage. Not ' +\n 'tied to the period — a pipeline is what is open now.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('top-open-deals')}\n columns={TOP_DEAL_COLUMNS}\n rows={() =>\n summary.top.map((deal) => [\n deal.title || deal.$id,\n summary.stageName(deal),\n typeof deal.expectedCloseAtMs === 'number' && deal.expectedCloseAtMs > 0\n ? new Date(deal.expectedCloseAtMs).toISOString().slice(0, 10)\n : '',\n (Number(deal.amountCents ?? 0) / 100).toFixed(2),\n String(deal.currency || currency).toUpperCase(),\n ])\n }\n disabled={!dealsRead || !summary.top.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open deals'}\n value={open.value ? open.value.count.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Pipeline value'}\n value={open.value ? money(open.value.amountCents, currency) : null}\n note={'face value of every open deal'}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Weighted forecast'}\n value={dealsRead ? money(summary.weightedCents, currency) : null}\n note={\n dealWindow.truncated\n ? `from the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated`\n : 'each deal at its stage odds'\n }\n />\n {/*\n Open deals with no open task against them (AGL-2661): the ones\n nobody has a next step for. Painted as a warning above zero,\n because a deal with nothing planned is a deal going nowhere; the\n tile opens the deals list, whose \"No next activity\" chip lists\n them.\n */}\n <ReportStatTile\n label={'Stuck deals'}\n value={dealsRead ? stuck.count.toLocaleString() : null}\n note={'open, with no next activity'}\n riseIsGood={false}\n color={stuck.count > 0 ? 'warning.main' : undefined}\n href={routes.section('deals')}\n />\n </Stack>\n {open.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The deal totals could not be read.'}\n </Alert>\n ) : null}\n {summary.currency.mixed ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Deals are in more than one currency; the totals add them as numbers and show ${currency.toUpperCase()}, the most common.`}\n </Typography>\n ) : null}\n {dealsRead && !summary.pipelines.length && !dealWindow.rows.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No open deals yet.'}\n </Typography>\n ) : null}\n {summary.pipelines.map(({ pipeline, totals }) => (\n <Section key={pipeline.$id} title={pipeline.name || 'Pipeline'}>\n <ReportBreakdown\n rows={[\n ...totals.stages.map((row) => ({\n key: row.stage.id,\n label: row.stage.name,\n value: row.amountCents,\n display: money(row.amountCents, currency),\n note: `${plural(row.count, 'deal')} · ${money(row.weightedCents, currency)} weighted`,\n })),\n ...(totals.unplaced.count\n ? [\n {\n key: '$unplaced',\n label: 'Stage no longer in this pipeline',\n value: totals.unplaced.amountCents,\n display: money(totals.unplaced.amountCents, currency),\n note: `${plural(totals.unplaced.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]\n : []),\n ]}\n emptyText={'This pipeline has no open stages.'}\n />\n </Section>\n ))}\n {summary.orphanTotals.count ? (\n <Section title={'Deals in no pipeline'}>\n <ReportBreakdown\n rows={[\n {\n key: '$orphans',\n label: 'No pipeline',\n value: summary.orphanTotals.amountCents,\n display: money(summary.orphanTotals.amountCents, currency),\n note: `${plural(summary.orphanTotals.count, 'deal')} · not forecast`,\n color: 'warning.main',\n },\n ]}\n emptyText={''}\n />\n </Section>\n ) : null}\n {pipelineWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Only the first ${PIPELINE_CEILING} pipelines are charted.`}\n </Typography>\n ) : null}\n {summary.top.length ? (\n <Section title={'Top open deals'}>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Deal'}</TableCell>\n <TableCell>{'Stage'}</TableCell>\n <TableCell>{'Expected close'}</TableCell>\n <TableCell align=\"right\">{'Amount'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {summary.top.map((deal) => (\n <TableRow key={deal.$id}>\n <TableCell>\n <AppLink href={routes.deal(deal.$id)}>\n {deal.title || deal.$id}\n </AppLink>\n </TableCell>\n <TableCell>{summary.stageName(deal)}</TableCell>\n <TableCell>{shortDate(deal.expectedCloseAtMs)}</TableCell>\n <TableCell align=\"right\">\n {money(\n Number(deal.amountCents ?? 0),\n String(deal.currency || currency),\n )}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {/* What the ranking, and so the export, is drawn from when that is not everything. */}\n {dealWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Ranked within the ${OPEN_DEAL_CEILING.toLocaleString()} most recently updated open deals.`}\n </Typography>\n ) : null}\n </Section>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nPipelineCard.displayName = 'PipelineCard'\n\nexport default PipelineCard\n"],"names":["Aglyn","money","AppLink","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","count","getAggregateFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","collectionCeiling","ReportBreakdown","ReportExport","plural","reportFilename","shortDate","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","OPEN_DEAL_CEILING","PIPELINE_CEILING","TOP_DEALS","TOP_DEAL_COLUMNS","PipelineCard","props","report","scope","tokens","routes","nowMs","firestore","open","amountCents","then","snapshot","Number","data","cacheKey","dealWindow","pipelineWindow","dealsStatus","status","summary","byPipeline","Map","deal","rows","key","pipelineId","list","get","push","set","pipelines","filter","pipeline","isPipelineArchived","has","$id","map","totals","pipelineTotals","known","Set","orphans","stageName","dealStageById","find","stageId","name","top","sort","a","b","slice","orphanTotals","weightedCents","reduce","total","entry","currency","currencyOfDeals","stuck","stuckDeals","dealsRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","title","expectedCloseAtMs","Date","toISOString","toFixed","String","toUpperCase","disabled","length","spacing","direction","sx","flexWrap","label","value","toLocaleString","note","href","section","truncated","riseIsGood","color","undefined","severity","mixed","variant","stages","row","stage","id","display","unplaced","emptyText","size","align","displayName"],"mappings":"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"}
@@ -11,24 +11,17 @@ export interface ReportExportProps {
11
11
  rows: () => ReadonlyArray<ReadonlyArray<unknown>>;
12
12
  /** Nothing to write yet — the window is still reading, or came back empty. */
13
13
  disabled?: boolean;
14
- /**
15
- * What the table is grouped from, when that is not everything — the
16
- * card's own truncation notice, drawn beside the button so the file and
17
- * its limits are read together.
18
- */
19
- caption?: string;
20
14
  }
21
15
  /**
22
- * Export CSV for one report table (AGL-2624).
16
+ * Export CSV for one report table (AGL-2624), in its card's header.
23
17
  *
24
18
  * Client-side, from the window the card has already loaded: a report table
25
19
  * is at most a thousand grouped rows, and the file is those rows exactly as
26
20
  * drawn — the same names, the same counts, the same order — so a reader can
27
21
  * check the export against the screen. It is NOT a fresh read, so when the
28
- * card's window was full the caption beside the button says so, the way
29
- * the card says so beneath the table; an export that quietly wrote a sample
30
- * as if it were the whole would be the one thing the on-screen caption
31
- * exists to prevent.
22
+ * card's window was full the card says so in a caption beneath the table;
23
+ * an export that quietly wrote a sample as if it were the whole would be
24
+ * the one thing that caption exists to prevent.
32
25
  */
33
26
  export declare function ReportExport(props: ReportExportProps): import("react").JSX.Element;
34
27
  export declare namespace ReportExport {
@@ -14,50 +14,33 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
+ import { jsx as _jsx } from "react/jsx-runtime";
18
18
  import { csvDocument } from "@aglyn/aglyn";
19
19
  import { mdiDownloadOutline } from "@aglyn/shared-data-mdi";
20
20
  import { MdiIcon } from "@aglyn/shared-ui-jsx";
21
- import { Button, Stack, Typography } from "@mui/material";
21
+ import { Button } from "@mui/material";
22
22
  import { downloadTextFile } from "../../model/contacts-csv.js";
23
23
  /**
24
- * Export CSV for one report table (AGL-2624).
24
+ * Export CSV for one report table (AGL-2624), in its card's header.
25
25
  *
26
26
  * Client-side, from the window the card has already loaded: a report table
27
27
  * is at most a thousand grouped rows, and the file is those rows exactly as
28
28
  * drawn — the same names, the same counts, the same order — so a reader can
29
29
  * check the export against the screen. It is NOT a fresh read, so when the
30
- * card's window was full the caption beside the button says so, the way
31
- * the card says so beneath the table; an export that quietly wrote a sample
32
- * as if it were the whole would be the one thing the on-screen caption
33
- * exists to prevent.
30
+ * card's window was full the card says so in a caption beneath the table;
31
+ * an export that quietly wrote a sample as if it were the whole would be
32
+ * the one thing that caption exists to prevent.
34
33
  */ export function ReportExport(props) {
35
- const { filename, columns, rows, disabled = false, caption } = props;
36
- return /*#__PURE__*/ _jsxs(Stack, {
37
- direction: "row",
38
- spacing: 1.5,
39
- sx: {
40
- alignItems: 'center',
41
- flexWrap: 'wrap',
42
- rowGap: 0.5
43
- },
44
- children: [
45
- /*#__PURE__*/ _jsx(Button, {
46
- size: "small",
47
- startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
48
- path: mdiDownloadOutline.path,
49
- size: 0.8
50
- }),
51
- disabled: disabled,
52
- onClick: ()=>downloadTextFile(filename, 'text/csv', csvDocument(columns, rows())),
53
- children: 'Export CSV'
54
- }),
55
- caption ? /*#__PURE__*/ _jsx(Typography, {
56
- variant: "caption",
57
- color: "text.secondary",
58
- children: caption
59
- }) : null
60
- ]
34
+ const { filename, columns, rows, disabled = false } = props;
35
+ return /*#__PURE__*/ _jsx(Button, {
36
+ size: "small",
37
+ startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
38
+ path: mdiDownloadOutline.path,
39
+ size: 0.8
40
+ }),
41
+ disabled: disabled,
42
+ onClick: ()=>downloadTextFile(filename, 'text/csv', csvDocument(columns, rows())),
43
+ children: 'Export CSV'
61
44
  });
62
45
  }
63
46
  ReportExport.displayName = 'ReportExport';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/report-export.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 { csvDocument } from '@aglyn/aglyn'\nimport { mdiDownloadOutline } from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { Button, Stack, Typography } from '@mui/material'\nimport { downloadTextFile } from '../../model/contacts-csv'\n\nexport interface ReportExportProps {\n /** The file's name — see `reportFilename`. */\n filename: string\n /** The header row, in the table's column order. */\n columns: readonly string[]\n /**\n * The rows, one array per line in the header's order, built when the\n * button is pressed rather than on every render: a table of a thousand\n * rows is serialized once, on demand, and never while nobody asked.\n */\n rows: () => ReadonlyArray<ReadonlyArray<unknown>>\n /** Nothing to write yet — the window is still reading, or came back empty. */\n disabled?: boolean\n /**\n * What the table is grouped from, when that is not everything — the\n * card's own truncation notice, drawn beside the button so the file and\n * its limits are read together.\n */\n caption?: string\n}\n\n/**\n * Export CSV for one report table (AGL-2624).\n *\n * Client-side, from the window the card has already loaded: a report table\n * is at most a thousand grouped rows, and the file is those rows exactly as\n * drawn — the same names, the same counts, the same order — so a reader can\n * check the export against the screen. It is NOT a fresh read, so when the\n * card's window was full the caption beside the button says so, the way\n * the card says so beneath the table; an export that quietly wrote a sample\n * as if it were the whole would be the one thing the on-screen caption\n * exists to prevent.\n */\nexport function ReportExport(props: ReportExportProps) {\n const { filename, columns, rows, disabled = false, caption } = props\n return (\n <Stack\n direction=\"row\"\n spacing={1.5}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiDownloadOutline.path} size={0.8} />}\n disabled={disabled}\n onClick={() =>\n downloadTextFile(filename, 'text/csv', csvDocument(columns, rows()))\n }\n >\n {'Export CSV'}\n </Button>\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Stack>\n )\n}\nReportExport.displayName = 'ReportExport'\n\nexport default ReportExport\n"],"names":["csvDocument","mdiDownloadOutline","MdiIcon","Button","Stack","Typography","downloadTextFile","ReportExport","props","filename","columns","rows","disabled","caption","direction","spacing","sx","alignItems","flexWrap","rowGap","size","startIcon","path","onClick","variant","color","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,WAAW,QAAQ,eAAc;AAC1C,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,OAAO,QAAQ,uBAAsB;AAC9C,SAASC,MAAM,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACzD,SAASC,gBAAgB,QAAQ,8BAA0B;AAuB3D;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,aAAaC,KAAwB;IACnD,MAAM,EAAEC,QAAQ,EAAEC,OAAO,EAAEC,IAAI,EAAEC,WAAW,KAAK,EAAEC,OAAO,EAAE,GAAGL;IAC/D,qBACE,MAACJ;QACCU,WAAU;QACVC,SAAS;QACTC,IAAI;YAAEC,YAAY;YAAUC,UAAU;YAAQC,QAAQ;QAAI;;0BAE1D,KAAChB;gBACCiB,MAAK;gBACLC,yBAAW,KAACnB;oBAAQoB,MAAMrB,mBAAmBqB,IAAI;oBAAEF,MAAM;;gBACzDR,UAAUA;gBACVW,SAAS,IACPjB,iBAAiBG,UAAU,YAAYT,YAAYU,SAASC;0BAG7D;;YAEFE,wBACC,KAACR;gBAAWmB,SAAQ;gBAAUC,OAAM;0BACjCZ;iBAED;;;AAGV;AACAN,aAAamB,WAAW,GAAG;AAE3B,eAAenB,aAAY"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/report-export.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 { csvDocument } from '@aglyn/aglyn'\nimport { mdiDownloadOutline } from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { Button } from '@mui/material'\nimport { downloadTextFile } from '../../model/contacts-csv'\n\nexport interface ReportExportProps {\n /** The file's name — see `reportFilename`. */\n filename: string\n /** The header row, in the table's column order. */\n columns: readonly string[]\n /**\n * The rows, one array per line in the header's order, built when the\n * button is pressed rather than on every render: a table of a thousand\n * rows is serialized once, on demand, and never while nobody asked.\n */\n rows: () => ReadonlyArray<ReadonlyArray<unknown>>\n /** Nothing to write yet — the window is still reading, or came back empty. */\n disabled?: boolean\n}\n\n/**\n * Export CSV for one report table (AGL-2624), in its card's header.\n *\n * Client-side, from the window the card has already loaded: a report table\n * is at most a thousand grouped rows, and the file is those rows exactly as\n * drawn — the same names, the same counts, the same order — so a reader can\n * check the export against the screen. It is NOT a fresh read, so when the\n * card's window was full the card says so in a caption beneath the table;\n * an export that quietly wrote a sample as if it were the whole would be\n * the one thing that caption exists to prevent.\n */\nexport function ReportExport(props: ReportExportProps) {\n const { filename, columns, rows, disabled = false } = props\n return (\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiDownloadOutline.path} size={0.8} />}\n disabled={disabled}\n onClick={() =>\n downloadTextFile(filename, 'text/csv', csvDocument(columns, rows()))\n }\n >\n {'Export CSV'}\n </Button>\n )\n}\nReportExport.displayName = 'ReportExport'\n\nexport default ReportExport\n"],"names":["csvDocument","mdiDownloadOutline","MdiIcon","Button","downloadTextFile","ReportExport","props","filename","columns","rows","disabled","size","startIcon","path","onClick","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,WAAW,QAAQ,eAAc;AAC1C,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,OAAO,QAAQ,uBAAsB;AAC9C,SAASC,MAAM,QAAQ,gBAAe;AACtC,SAASC,gBAAgB,QAAQ,8BAA0B;AAiB3D;;;;;;;;;;CAUC,GACD,OAAO,SAASC,aAAaC,KAAwB;IACnD,MAAM,EAAEC,QAAQ,EAAEC,OAAO,EAAEC,IAAI,EAAEC,WAAW,KAAK,EAAE,GAAGJ;IACtD,qBACE,KAACH;QACCQ,MAAK;QACLC,yBAAW,KAACV;YAAQW,MAAMZ,mBAAmBY,IAAI;YAAEF,MAAM;;QACzDD,UAAUA;QACVI,SAAS,IACPV,iBAAiBG,UAAU,YAAYP,YAAYQ,SAASC;kBAG7D;;AAGP;AACAJ,aAAaU,WAAW,GAAG;AAE3B,eAAeV,aAAY"}
@@ -103,6 +103,19 @@ import { useAggregateRead, useWindowRead } from "./use-aggregate-read.js";
103
103
  }),
104
104
  contentGutterX: true,
105
105
  contentGutterY: true,
106
+ HeaderProps: {
107
+ action: /*#__PURE__*/ _jsx(ReportExport, {
108
+ filename: reportFilename('conversion-by-source', period),
109
+ columns: COLUMNS,
110
+ rows: ()=>conversion.rows.map((row)=>[
111
+ labelOf(row.source),
112
+ row.captured,
113
+ row.customers,
114
+ percent(row.rate)
115
+ ]),
116
+ disabled: !read || !conversion.rows.length
117
+ })
118
+ },
106
119
  children: /*#__PURE__*/ _jsxs(Stack, {
107
120
  spacing: 2,
108
121
  children: [
@@ -181,18 +194,11 @@ import { useAggregateRead, useWindowRead } from "./use-aggregate-read.js";
181
194
  color: "text.secondary",
182
195
  children: 'A person captured two ways counts under both.' + (conversion.unsourced ? ` ${conversion.unsourced.toLocaleString()} with no source on this site are counted in the tiles only.` : '')
183
196
  }) : null,
184
- /*#__PURE__*/ _jsx(ReportExport, {
185
- filename: reportFilename('conversion-by-source', period),
186
- columns: COLUMNS,
187
- rows: ()=>conversion.rows.map((row)=>[
188
- labelOf(row.source),
189
- row.captured,
190
- row.customers,
191
- percent(row.rate)
192
- ]),
193
- disabled: !read || !conversion.rows.length,
194
- caption: caption
195
- })
197
+ caption ? /*#__PURE__*/ _jsx(Typography, {
198
+ variant: "caption",
199
+ color: "text.secondary",
200
+ children: caption
201
+ }) : null
196
202
  ]
197
203
  })
198
204
  ]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/source-conversion-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n getCountFromServer,\n limit,\n orderBy,\n query,\n Timestamp,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\n\n/**\n * How many of the period's contacts the by-source table is read from.\n *\n * The captured count is a server aggregate and never meets this bound;\n * the table needs each contact's sources and stage off this holder's\n * facet, which no aggregate can read, so it reads the period's newest\n * thousand and says when there were more — the same bound the sources\n * mix keeps, over the period rather than all time.\n */\nconst CONTACT_CEILING = 1000\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = ['Source', 'Captured', 'Customers', 'Conversion'] as const\n\nexport interface SourceConversionCardProps {\n report: CrmReportScope\n}\n\n/**\n * Conversion by source (AGL-2624): of the people captured through each\n * door in the period, how many are customers now.\n *\n * A COHORT by capture date, not a count of purchases in the period: the\n * question is \"how well does each source convert\", and the honest\n * population for it is the people that source brought in, followed to\n * where they stand today. A form lead captured on day one and buying on\n * day twenty is a conversion the form earned; a customer captured last\n * year who bought again this month is not evidence about this period's\n * doors. So the window is the period's contacts by `createdAt` — the\n * `(visibleTo, createdAt)` index the trend card counts through — and\n * \"customer\" is read off the facet: the stage at `customer` or past it,\n * or an order on the books whatever the stage says.\n *\n * Through THIS group's facet, like every by-source figure: a source is\n * which of this business's doors the person came through, and a stage is\n * where this business's team put them.\n */\nexport function SourceConversionCard(props: SourceConversionCardProps) {\n const { report } = props\n const { scope, tokens, groupId, range, period, routes } = report\n const firestore = useFirestore()\n\n const capturedInPeriod = () =>\n query(\n scopedCollection(firestore, scope, 'contacts'),\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(range.from)),\n where('createdAt', '<', Timestamp.fromMillis(range.to)),\n )\n\n const captured = useAggregateRead<number>(\n () => getCountFromServer(capturedInPeriod()).then((snapshot) => snapshot.data().count),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:captured') },\n )\n const window = useWindowRead<Record<string, unknown>>(\n () =>\n query(\n capturedInPeriod(),\n orderBy('createdAt', 'desc'),\n limit(CONTACT_CEILING + 1),\n ),\n CONTACT_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:window') },\n )\n const status = window.status\n\n const conversion = useMemo(\n () =>\n Aglyn.conversionBySource(\n window.rows.map((row) => Aglyn.readContactFacet(row, groupId)),\n ),\n [window, groupId],\n )\n const read = status === 'success'\n const rate = read\n ? measuredRate(conversion.customers, conversion.total, 'captured')\n : null\n const labelOf = (source: Aglyn.ContactSource): string =>\n Aglyn.CONTACT_SOURCE_LABELS[source] ?? source\n const sampled =\n read &&\n (window.truncated ||\n (captured.value !== null && captured.value > window.rows.length))\n const caption = sampled\n ? `Read from the ${window.rows.length.toLocaleString()} most recently captured contacts` +\n (captured.value !== null ? ` of ${captured.value.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n\n return (\n <CardDisplay\n header={'Conversion by source'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#conversion-by-source',\n excerpt:\n 'Of the people each capture surface brought in during the ' +\n 'period, how many are customers now — at the customer stage or ' +\n 'past it, or with an order on the books. Read through this ' +\n 'site’s own view of each person.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Captured'}\n value={captured.value !== null ? captured.value.toLocaleString() : null}\n note={'in the period, counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Now customers'}\n value={read ? conversion.customers.toLocaleString() : null}\n note={'of those captured'}\n />\n <ReportStatTile\n label={'Conversion'}\n value={rate ? percent(rate.value) : null}\n note={\n rate\n ? `${rate.numerator.toLocaleString()} of ${rate.denominator.toLocaleString()} ${rate.denominatorLabel}`\n : read\n ? 'nobody captured in the period'\n : undefined\n }\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{'The period’s contacts could not be read.'}</Alert>\n ) : null}\n <Section title={'By source'}>\n {conversion.rows.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {conversion.rows.map((row) => (\n <TableRow key={row.source}>\n <TableCell>{labelOf(row.source)}</TableCell>\n <TableCell align=\"right\">{row.captured.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.customers.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{percent(row.rate)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {status === 'loading' ? 'Reading…' : 'Nobody captured in this period.'}\n </Typography>\n )}\n {conversion.rows.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'A person captured two ways counts under both.' +\n (conversion.unsourced\n ? ` ${conversion.unsourced.toLocaleString()} with no source on this site are counted in the tiles only.`\n : '')}\n </Typography>\n ) : null}\n <ReportExport\n filename={reportFilename('conversion-by-source', period)}\n columns={COLUMNS}\n rows={() =>\n conversion.rows.map((row) => [\n labelOf(row.source),\n row.captured,\n row.customers,\n percent(row.rate),\n ])\n }\n disabled={!read || !conversion.rows.length}\n caption={caption}\n />\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nSourceConversionCard.displayName = 'SourceConversionCard'\n\nexport default SourceConversionCard\n"],"names":["Aglyn","CardDisplay","measuredRate","percent","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","Timestamp","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","CONTACT_CEILING","COLUMNS","SourceConversionCard","props","report","scope","tokens","groupId","range","period","routes","firestore","capturedInPeriod","fromMillis","from","to","captured","then","snapshot","data","count","cacheKey","window","status","conversion","conversionBySource","rows","map","row","readContactFacet","read","rate","customers","total","labelOf","source","CONTACT_SOURCE_LABELS","sampled","truncated","value","length","caption","toLocaleString","undefined","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","direction","sx","flexWrap","label","note","href","section","numerator","denominator","denominatorLabel","severity","title","size","column","index","align","variant","color","unsourced","filename","columns","disabled","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/source-conversion-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n getCountFromServer,\n limit,\n orderBy,\n query,\n Timestamp,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\n\n/**\n * How many of the period's contacts the by-source table is read from.\n *\n * The captured count is a server aggregate and never meets this bound;\n * the table needs each contact's sources and stage off this holder's\n * facet, which no aggregate can read, so it reads the period's newest\n * thousand and says when there were more — the same bound the sources\n * mix keeps, over the period rather than all time.\n */\nconst CONTACT_CEILING = 1000\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = ['Source', 'Captured', 'Customers', 'Conversion'] as const\n\nexport interface SourceConversionCardProps {\n report: CrmReportScope\n}\n\n/**\n * Conversion by source (AGL-2624): of the people captured through each\n * door in the period, how many are customers now.\n *\n * A COHORT by capture date, not a count of purchases in the period: the\n * question is \"how well does each source convert\", and the honest\n * population for it is the people that source brought in, followed to\n * where they stand today. A form lead captured on day one and buying on\n * day twenty is a conversion the form earned; a customer captured last\n * year who bought again this month is not evidence about this period's\n * doors. So the window is the period's contacts by `createdAt` — the\n * `(visibleTo, createdAt)` index the trend card counts through — and\n * \"customer\" is read off the facet: the stage at `customer` or past it,\n * or an order on the books whatever the stage says.\n *\n * Through THIS group's facet, like every by-source figure: a source is\n * which of this business's doors the person came through, and a stage is\n * where this business's team put them.\n */\nexport function SourceConversionCard(props: SourceConversionCardProps) {\n const { report } = props\n const { scope, tokens, groupId, range, period, routes } = report\n const firestore = useFirestore()\n\n const capturedInPeriod = () =>\n query(\n scopedCollection(firestore, scope, 'contacts'),\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(range.from)),\n where('createdAt', '<', Timestamp.fromMillis(range.to)),\n )\n\n const captured = useAggregateRead<number>(\n () => getCountFromServer(capturedInPeriod()).then((snapshot) => snapshot.data().count),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:captured') },\n )\n const window = useWindowRead<Record<string, unknown>>(\n () =>\n query(\n capturedInPeriod(),\n orderBy('createdAt', 'desc'),\n limit(CONTACT_CEILING + 1),\n ),\n CONTACT_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:window') },\n )\n const status = window.status\n\n const conversion = useMemo(\n () =>\n Aglyn.conversionBySource(\n window.rows.map((row) => Aglyn.readContactFacet(row, groupId)),\n ),\n [window, groupId],\n )\n const read = status === 'success'\n const rate = read\n ? measuredRate(conversion.customers, conversion.total, 'captured')\n : null\n const labelOf = (source: Aglyn.ContactSource): string =>\n Aglyn.CONTACT_SOURCE_LABELS[source] ?? source\n const sampled =\n read &&\n (window.truncated ||\n (captured.value !== null && captured.value > window.rows.length))\n const caption = sampled\n ? `Read from the ${window.rows.length.toLocaleString()} most recently captured contacts` +\n (captured.value !== null ? ` of ${captured.value.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n\n return (\n <CardDisplay\n header={'Conversion by source'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#conversion-by-source',\n excerpt:\n 'Of the people each capture surface brought in during the ' +\n 'period, how many are customers now — at the customer stage or ' +\n 'past it, or with an order on the books. Read through this ' +\n 'site’s own view of each person.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('conversion-by-source', period)}\n columns={COLUMNS}\n rows={() =>\n conversion.rows.map((row) => [\n labelOf(row.source),\n row.captured,\n row.customers,\n percent(row.rate),\n ])\n }\n disabled={!read || !conversion.rows.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Captured'}\n value={captured.value !== null ? captured.value.toLocaleString() : null}\n note={'in the period, counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Now customers'}\n value={read ? conversion.customers.toLocaleString() : null}\n note={'of those captured'}\n />\n <ReportStatTile\n label={'Conversion'}\n value={rate ? percent(rate.value) : null}\n note={\n rate\n ? `${rate.numerator.toLocaleString()} of ${rate.denominator.toLocaleString()} ${rate.denominatorLabel}`\n : read\n ? 'nobody captured in the period'\n : undefined\n }\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{'The period’s contacts could not be read.'}</Alert>\n ) : null}\n <Section title={'By source'}>\n {conversion.rows.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {conversion.rows.map((row) => (\n <TableRow key={row.source}>\n <TableCell>{labelOf(row.source)}</TableCell>\n <TableCell align=\"right\">{row.captured.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.customers.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{percent(row.rate)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {status === 'loading' ? 'Reading…' : 'Nobody captured in this period.'}\n </Typography>\n )}\n {conversion.rows.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'A person captured two ways counts under both.' +\n (conversion.unsourced\n ? ` ${conversion.unsourced.toLocaleString()} with no source on this site are counted in the tiles only.`\n : '')}\n </Typography>\n ) : null}\n {/* What the table, and so the export, is drawn from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nSourceConversionCard.displayName = 'SourceConversionCard'\n\nexport default SourceConversionCard\n"],"names":["Aglyn","CardDisplay","measuredRate","percent","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","Timestamp","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","CONTACT_CEILING","COLUMNS","SourceConversionCard","props","report","scope","tokens","groupId","range","period","routes","firestore","capturedInPeriod","fromMillis","from","to","captured","then","snapshot","data","count","cacheKey","window","status","conversion","conversionBySource","rows","map","row","readContactFacet","read","rate","customers","total","labelOf","source","CONTACT_SOURCE_LABELS","sampled","truncated","value","length","caption","toLocaleString","undefined","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","disabled","spacing","direction","sx","flexWrap","label","note","href","section","numerator","denominator","denominatorLabel","severity","title","size","column","index","align","variant","color","unsourced","displayName"],"mappings":"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"}
@@ -154,17 +154,36 @@ import { useAggregateRead, useWindowRead } from "./use-aggregate-read.js";
154
154
  contentGutterX: true,
155
155
  contentGutterY: true,
156
156
  HeaderProps: {
157
- action: /*#__PURE__*/ _jsx(Button, _extends({
158
- component: AppLink
159
- }, {
160
- componentVariant: 'naked',
161
- nativeButton: false
162
- }, {
163
- href: routes.section('tasks'),
164
- size: "small",
165
- color: "primary",
166
- children: 'Open tasks'
167
- }))
157
+ action: /*#__PURE__*/ _jsxs(Stack, {
158
+ direction: "row",
159
+ spacing: 1,
160
+ children: [
161
+ /*#__PURE__*/ _jsx(ReportExport, {
162
+ filename: reportFilename('tasks-by-assignee'),
163
+ columns: ASSIGNEE_COLUMNS,
164
+ rows: ()=>load.map((row)=>[
165
+ assigneeName(row),
166
+ row.overdue,
167
+ row.today,
168
+ row.upcoming,
169
+ row.undated,
170
+ row.open
171
+ ]),
172
+ disabled: tasksStatus !== 'success' || !load.length
173
+ }),
174
+ /*#__PURE__*/ _jsx(Button, _extends({
175
+ component: AppLink
176
+ }, {
177
+ componentVariant: 'naked',
178
+ nativeButton: false
179
+ }, {
180
+ href: routes.section('tasks'),
181
+ size: "small",
182
+ color: "primary",
183
+ children: 'Open tasks'
184
+ }))
185
+ ]
186
+ })
168
187
  },
169
188
  children: /*#__PURE__*/ _jsxs(Stack, {
170
189
  spacing: 2,
@@ -253,20 +272,11 @@ import { useAggregateRead, useWindowRead } from "./use-aggregate-read.js";
253
272
  color: "text.secondary",
254
273
  children: tasksStatus === 'loading' ? 'Reading…' : 'No open tasks.'
255
274
  }),
256
- /*#__PURE__*/ _jsx(ReportExport, {
257
- filename: reportFilename('tasks-by-assignee'),
258
- columns: ASSIGNEE_COLUMNS,
259
- rows: ()=>load.map((row)=>[
260
- assigneeName(row),
261
- row.overdue,
262
- row.today,
263
- row.upcoming,
264
- row.undated,
265
- row.open
266
- ]),
267
- disabled: tasksStatus !== 'success' || !load.length,
268
- caption: taskWindow.truncated ? `Grouped from the ${OPEN_TASK_CEILING.toLocaleString()} soonest-due open tasks; the tiles are counted on the server.` : undefined
269
- })
275
+ taskWindow.truncated ? /*#__PURE__*/ _jsx(Typography, {
276
+ variant: "caption",
277
+ color: "text.secondary",
278
+ children: `Grouped from the ${OPEN_TASK_CEILING.toLocaleString()} soonest-due open tasks; the tiles are counted on the server.`
279
+ }) : null
270
280
  ]
271
281
  })
272
282
  ]
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/tasks-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Button,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n documentId,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\n\n/**\n * How many open tasks the by-assignee table is grouped from.\n *\n * The tiles are counts the server takes; the table needs each task's\n * assignee and due state, so it reads the soonest-due thousand and says\n * when there were more.\n */\nconst OPEN_TASK_CEILING = 1000\n/** One `in` query's worth of member documents, for the assignee names. */\nconst ASSIGNEE_NAME_CEILING = 30\n\ntype TaskRow = Aglyn.CrmTask & { $id: string }\n\n/** The by-assignee table's columns, which are also the CSV's. */\nconst ASSIGNEE_COLUMNS = ['Assignee', 'Overdue', 'Today', 'Upcoming', 'No date', 'Open'] as const\n\ninterface AssigneeLoad {\n uid: string\n overdue: number\n today: number\n upcoming: number\n undated: number\n open: number\n}\n\nexport interface TasksCardProps {\n report: CrmReportScope\n}\n\n/**\n * The task load: open, overdue and due today, and who is carrying what\n * (AGL-2604).\n *\n * \"Today\" and \"overdue\" are decided on the LOCAL calendar day, the same day\n * `taskDueState` decides them on, so the count here and the chip on the\n * tasks list agree about the same task at 11 p.m. The counts are range\n * queries on `dueAtMs`, which the `(visibleTo, status, dueAtMs)` index\n * serves; a task with no due date is in the open count and in none of the\n * dated ones.\n *\n * The table reads open tasks ordered by due date. An ordered read admits\n * only documents that CARRY the field, so a task written without `dueAtMs`\n * at all — rather than with `null`, which sorts first — is counted in the\n * tile and absent from the table. The model types the field as optional,\n * so the table says what it is grouped from.\n */\nexport function TasksCard(props: TasksCardProps) {\n const { report } = props\n const { scope, tokens, nowMs, routes } = report\n const firestore = useFirestore()\n const day = useMemo(() => Aglyn.localDayBounds(nowMs), [nowMs])\n\n const openTasks = () =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n )\n\n const counts = useAggregateRead(\n () =>\n Promise.all([\n getCountFromServer(openTasks()),\n getCountFromServer(query(openTasks(), where('dueAtMs', '<', day.start))),\n getCountFromServer(\n query(\n openTasks(),\n where('dueAtMs', '>=', day.start),\n where('dueAtMs', '<', day.end),\n ),\n ),\n ]).then(([open, overdue, today]) => ({\n open: open.data().count,\n overdue: overdue.data().count,\n today: today.data().count,\n })),\n [firestore, scope, tokens, day],\n { cacheKey: reportCacheKey(report, 'tasks:counts') },\n )\n\n const taskWindow = useWindowRead<TaskRow>(\n () =>\n query(\n openTasks(),\n orderBy('dueAtMs', 'asc'),\n limit(OPEN_TASK_CEILING + 1),\n ),\n OPEN_TASK_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'tasks:open') },\n )\n const tasksStatus = taskWindow.status\n\n const load = useMemo(() => {\n const byAssignee = new Map<string, AssigneeLoad>()\n for (const task of taskWindow.rows) {\n const uid = task.assigneeUid || ''\n const row = byAssignee.get(uid) ?? {\n uid,\n overdue: 0,\n today: 0,\n upcoming: 0,\n undated: 0,\n open: 0,\n }\n const state = Aglyn.taskDueState(task, nowMs)\n if (state === 'overdue') row.overdue += 1\n else if (state === 'today') row.today += 1\n else if (state === 'upcoming') row.upcoming += 1\n else row.undated += 1\n row.open += 1\n byAssignee.set(uid, row)\n }\n return [...byAssignee.values()].sort(\n (a, b) => b.open - a.open || a.uid.localeCompare(b.uid),\n )\n }, [taskWindow, nowMs])\n\n /*\n * The assignees' names, from their member documents — one `in` query for\n * up to thirty, which is Firestore's cap on the operator and more assignees\n * than a tasks list has. Anyone past it, or a member document the reader\n * cannot open, shows as their id rather than blocking the table.\n */\n const uids = load.map((row) => row.uid).filter(Boolean).slice(0, ASSIGNEE_NAME_CEILING)\n const uidKey = uids.join(',')\n const names = useAggregateRead<Record<string, string>>(\n () =>\n uids.length\n ? getDocs(\n query(\n scopedCollection(firestore, scope, 'members'),\n where(documentId(), 'in', uids),\n ),\n ).then((snapshot) =>\n Object.fromEntries(\n snapshot.docs.map((member) => [\n member.id,\n String(member.get('displayName') || member.get('email') || member.id),\n ]),\n ),\n )\n : Promise.resolve({}),\n [firestore, scope, uidKey],\n { cacheKey: reportCacheKey(report, `tasks:names:${uidKey}`) },\n )\n\n const figures = counts.value\n const assigneeName = (row: AssigneeLoad): string =>\n row.uid ? names.value?.[row.uid] ?? row.uid : 'Unassigned'\n return (\n <CardDisplay\n header={'Tasks'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#tasks',\n excerpt:\n 'Open tasks, how many are overdue or due today, and the open ' +\n 'load per assignee. Today and overdue are decided on your calendar ' +\n 'day, the same way the tasks list decides them.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('tasks')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open tasks'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open tasks'}\n value={figures ? figures.open.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Overdue'}\n value={figures ? figures.overdue.toLocaleString() : null}\n note={'due before today'}\n color={figures?.overdue ? 'error.main' : undefined}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Due today'}\n value={figures ? figures.today.toLocaleString() : null}\n note={'on your calendar day'}\n href={routes.section('tasks')}\n />\n </Stack>\n {counts.status === 'error' ? (\n <Alert severity=\"warning\">{'The task counts could not be read.'}</Alert>\n ) : null}\n <Section title={'Open tasks by assignee'}>\n {load.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {ASSIGNEE_COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {load.map((row) => (\n <TableRow key={row.uid || '$unassigned'}>\n <TableCell>{assigneeName(row)}</TableCell>\n <TableCell\n align=\"right\"\n sx={row.overdue ? { color: 'error.main' } : undefined}\n >\n {row.overdue.toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">{row.today.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.upcoming.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.undated.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.open.toLocaleString()}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {tasksStatus === 'loading' ? 'Reading…' : 'No open tasks.'}\n </Typography>\n )}\n <ReportExport\n filename={reportFilename('tasks-by-assignee')}\n columns={ASSIGNEE_COLUMNS}\n rows={() =>\n load.map((row) => [\n assigneeName(row),\n row.overdue,\n row.today,\n row.upcoming,\n row.undated,\n row.open,\n ])\n }\n disabled={tasksStatus !== 'success' || !load.length}\n caption={\n taskWindow.truncated\n ? `Grouped from the ${OPEN_TASK_CEILING.toLocaleString()} soonest-due open tasks; the tiles are counted on the server.`\n : undefined\n }\n />\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nTasksCard.displayName = 'TasksCard'\n\nexport default TasksCard\n"],"names":["Aglyn","AppLink","CardDisplay","Section","ScrollTable","Alert","Button","Stack","TableBody","TableCell","TableHead","TableRow","Typography","documentId","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","OPEN_TASK_CEILING","ASSIGNEE_NAME_CEILING","ASSIGNEE_COLUMNS","TasksCard","props","report","scope","tokens","nowMs","routes","firestore","day","localDayBounds","openTasks","CRM_COLLECTIONS","tasks","counts","Promise","all","start","end","then","open","overdue","today","data","count","cacheKey","taskWindow","tasksStatus","status","load","byAssignee","Map","task","rows","uid","assigneeUid","row","get","upcoming","undated","state","taskDueState","set","values","sort","a","b","localeCompare","uids","map","filter","Boolean","slice","uidKey","join","names","length","snapshot","Object","fromEntries","docs","member","id","String","resolve","figures","value","assigneeName","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","component","componentVariant","nativeButton","href","section","size","color","spacing","direction","sx","flexWrap","label","toLocaleString","note","undefined","severity","title","column","index","align","variant","filename","columns","disabled","caption","truncated","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/tasks-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Button,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n documentId,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\n\n/**\n * How many open tasks the by-assignee table is grouped from.\n *\n * The tiles are counts the server takes; the table needs each task's\n * assignee and due state, so it reads the soonest-due thousand and says\n * when there were more.\n */\nconst OPEN_TASK_CEILING = 1000\n/** One `in` query's worth of member documents, for the assignee names. */\nconst ASSIGNEE_NAME_CEILING = 30\n\ntype TaskRow = Aglyn.CrmTask & { $id: string }\n\n/** The by-assignee table's columns, which are also the CSV's. */\nconst ASSIGNEE_COLUMNS = ['Assignee', 'Overdue', 'Today', 'Upcoming', 'No date', 'Open'] as const\n\ninterface AssigneeLoad {\n uid: string\n overdue: number\n today: number\n upcoming: number\n undated: number\n open: number\n}\n\nexport interface TasksCardProps {\n report: CrmReportScope\n}\n\n/**\n * The task load: open, overdue and due today, and who is carrying what\n * (AGL-2604).\n *\n * \"Today\" and \"overdue\" are decided on the LOCAL calendar day, the same day\n * `taskDueState` decides them on, so the count here and the chip on the\n * tasks list agree about the same task at 11 p.m. The counts are range\n * queries on `dueAtMs`, which the `(visibleTo, status, dueAtMs)` index\n * serves; a task with no due date is in the open count and in none of the\n * dated ones.\n *\n * The table reads open tasks ordered by due date. An ordered read admits\n * only documents that CARRY the field, so a task written without `dueAtMs`\n * at all — rather than with `null`, which sorts first — is counted in the\n * tile and absent from the table. The model types the field as optional,\n * so the table says what it is grouped from.\n */\nexport function TasksCard(props: TasksCardProps) {\n const { report } = props\n const { scope, tokens, nowMs, routes } = report\n const firestore = useFirestore()\n const day = useMemo(() => Aglyn.localDayBounds(nowMs), [nowMs])\n\n const openTasks = () =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n )\n\n const counts = useAggregateRead(\n () =>\n Promise.all([\n getCountFromServer(openTasks()),\n getCountFromServer(query(openTasks(), where('dueAtMs', '<', day.start))),\n getCountFromServer(\n query(\n openTasks(),\n where('dueAtMs', '>=', day.start),\n where('dueAtMs', '<', day.end),\n ),\n ),\n ]).then(([open, overdue, today]) => ({\n open: open.data().count,\n overdue: overdue.data().count,\n today: today.data().count,\n })),\n [firestore, scope, tokens, day],\n { cacheKey: reportCacheKey(report, 'tasks:counts') },\n )\n\n const taskWindow = useWindowRead<TaskRow>(\n () =>\n query(\n openTasks(),\n orderBy('dueAtMs', 'asc'),\n limit(OPEN_TASK_CEILING + 1),\n ),\n OPEN_TASK_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'tasks:open') },\n )\n const tasksStatus = taskWindow.status\n\n const load = useMemo(() => {\n const byAssignee = new Map<string, AssigneeLoad>()\n for (const task of taskWindow.rows) {\n const uid = task.assigneeUid || ''\n const row = byAssignee.get(uid) ?? {\n uid,\n overdue: 0,\n today: 0,\n upcoming: 0,\n undated: 0,\n open: 0,\n }\n const state = Aglyn.taskDueState(task, nowMs)\n if (state === 'overdue') row.overdue += 1\n else if (state === 'today') row.today += 1\n else if (state === 'upcoming') row.upcoming += 1\n else row.undated += 1\n row.open += 1\n byAssignee.set(uid, row)\n }\n return [...byAssignee.values()].sort(\n (a, b) => b.open - a.open || a.uid.localeCompare(b.uid),\n )\n }, [taskWindow, nowMs])\n\n /*\n * The assignees' names, from their member documents — one `in` query for\n * up to thirty, which is Firestore's cap on the operator and more assignees\n * than a tasks list has. Anyone past it, or a member document the reader\n * cannot open, shows as their id rather than blocking the table.\n */\n const uids = load.map((row) => row.uid).filter(Boolean).slice(0, ASSIGNEE_NAME_CEILING)\n const uidKey = uids.join(',')\n const names = useAggregateRead<Record<string, string>>(\n () =>\n uids.length\n ? getDocs(\n query(\n scopedCollection(firestore, scope, 'members'),\n where(documentId(), 'in', uids),\n ),\n ).then((snapshot) =>\n Object.fromEntries(\n snapshot.docs.map((member) => [\n member.id,\n String(member.get('displayName') || member.get('email') || member.id),\n ]),\n ),\n )\n : Promise.resolve({}),\n [firestore, scope, uidKey],\n { cacheKey: reportCacheKey(report, `tasks:names:${uidKey}`) },\n )\n\n const figures = counts.value\n const assigneeName = (row: AssigneeLoad): string =>\n row.uid ? names.value?.[row.uid] ?? row.uid : 'Unassigned'\n return (\n <CardDisplay\n header={'Tasks'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#tasks',\n excerpt:\n 'Open tasks, how many are overdue or due today, and the open ' +\n 'load per assignee. Today and overdue are decided on your calendar ' +\n 'day, the same way the tasks list decides them.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <ReportExport\n filename={reportFilename('tasks-by-assignee')}\n columns={ASSIGNEE_COLUMNS}\n rows={() =>\n load.map((row) => [\n assigneeName(row),\n row.overdue,\n row.today,\n row.upcoming,\n row.undated,\n row.open,\n ])\n }\n disabled={tasksStatus !== 'success' || !load.length}\n />\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('tasks')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open tasks'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open tasks'}\n value={figures ? figures.open.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Overdue'}\n value={figures ? figures.overdue.toLocaleString() : null}\n note={'due before today'}\n color={figures?.overdue ? 'error.main' : undefined}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Due today'}\n value={figures ? figures.today.toLocaleString() : null}\n note={'on your calendar day'}\n href={routes.section('tasks')}\n />\n </Stack>\n {counts.status === 'error' ? (\n <Alert severity=\"warning\">{'The task counts could not be read.'}</Alert>\n ) : null}\n <Section title={'Open tasks by assignee'}>\n {load.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {ASSIGNEE_COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {load.map((row) => (\n <TableRow key={row.uid || '$unassigned'}>\n <TableCell>{assigneeName(row)}</TableCell>\n <TableCell\n align=\"right\"\n sx={row.overdue ? { color: 'error.main' } : undefined}\n >\n {row.overdue.toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">{row.today.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.upcoming.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.undated.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.open.toLocaleString()}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {tasksStatus === 'loading' ? 'Reading…' : 'No open tasks.'}\n </Typography>\n )}\n {/* What the table, and so the export, is grouped from when that is not everything. */}\n {taskWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Grouped from the ${OPEN_TASK_CEILING.toLocaleString()} soonest-due open tasks; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nTasksCard.displayName = 'TasksCard'\n\nexport default TasksCard\n"],"names":["Aglyn","AppLink","CardDisplay","Section","ScrollTable","Alert","Button","Stack","TableBody","TableCell","TableHead","TableRow","Typography","documentId","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","OPEN_TASK_CEILING","ASSIGNEE_NAME_CEILING","ASSIGNEE_COLUMNS","TasksCard","props","report","scope","tokens","nowMs","routes","firestore","day","localDayBounds","openTasks","CRM_COLLECTIONS","tasks","counts","Promise","all","start","end","then","open","overdue","today","data","count","cacheKey","taskWindow","tasksStatus","status","load","byAssignee","Map","task","rows","uid","assigneeUid","row","get","upcoming","undated","state","taskDueState","set","values","sort","a","b","localeCompare","uids","map","filter","Boolean","slice","uidKey","join","names","length","snapshot","Object","fromEntries","docs","member","id","String","resolve","figures","value","assigneeName","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","filename","columns","disabled","component","componentVariant","nativeButton","href","section","size","color","sx","flexWrap","label","toLocaleString","note","undefined","severity","title","column","index","align","variant","truncated","displayName"],"mappings":"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"}