@aglyn/plugins-crm 1.0.0-beta.231 → 1.0.0-beta.233
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/activity-timeline.js +1 -0
- package/src/lib/components/activity-timeline.js.map +1 -1
- package/src/lib/components/assignment-rule-drawer.js +1 -0
- package/src/lib/components/assignment-rule-drawer.js.map +1 -1
- package/src/lib/components/companies-section.js +43 -8
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +3 -1
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/contact-associations-card.js +1 -0
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-detail-page.js +6 -1
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-duplicates-card.js +1 -0
- package/src/lib/components/contact-duplicates-card.js.map +1 -1
- package/src/lib/components/contact-field-drawer.js +1 -0
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/components/contact-known-by-card.js +1 -0
- package/src/lib/components/contact-known-by-card.js.map +1 -1
- package/src/lib/components/contact-timeline-card.js +1 -0
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/contacts-section.js +37 -8
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-email-check.d.ts +14 -0
- package/src/lib/components/crm-email-check.js +38 -1
- package/src/lib/components/crm-email-check.js.map +1 -1
- package/src/lib/components/crm-glance-card.js +1 -0
- package/src/lib/components/crm-glance-card.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.d.ts +5 -4
- package/src/lib/components/crm-next-activity-column.js +5 -4
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/crm-record-header.js +2 -0
- package/src/lib/components/crm-record-header.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +3 -1
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deals-section.js +51 -6
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.js +4 -0
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-history-card.js +1 -0
- package/src/lib/components/lead-history-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +9 -3
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +1 -0
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/leads-section.js +61 -11
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/linked-deals-card.js +1 -0
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/picklist-values-card.js +1 -0
- package/src/lib/components/picklist-values-card.js.map +1 -1
- package/src/lib/components/recipes-card.js +1 -0
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/reports/activity-card.js +1 -0
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/components/reports/closed-deals-card.js +1 -0
- package/src/lib/components/reports/closed-deals-card.js.map +1 -1
- package/src/lib/components/reports/contacts-trend-card.js +1 -0
- package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +1 -0
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/lead-source-card.js +1 -0
- package/src/lib/components/reports/lead-source-card.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +1 -0
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +1 -0
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/reports/tasks-card.js +1 -0
- package/src/lib/components/reports/tasks-card.js.map +1 -1
- package/src/lib/components/reports-section.js +2 -0
- package/src/lib/components/reports-section.js.map +1 -1
- package/src/lib/components/tasks-calendar.js +1 -0
- package/src/lib/components/tasks-calendar.js.map +1 -1
- package/src/lib/components/tasks-section.js +61 -9
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/company-filters.js +40 -2
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +10 -1
- package/src/lib/constants/contact-filters.js +62 -2
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.js +78 -0
- package/src/lib/constants/deal-filters.js.map +1 -1
- package/src/lib/hooks/use-crm-column-sort.d.ts +67 -0
- package/src/lib/hooks/use-crm-column-sort.js +156 -0
- package/src/lib/hooks/use-crm-column-sort.js.map +1 -0
- package/src/lib/hooks/use-crm-tasks.d.ts +6 -1
- package/src/lib/hooks/use-crm-tasks.js +10 -8
- package/src/lib/hooks/use-crm-tasks.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +6 -0
- package/src/lib/model/crm-list-query.js +13 -0
- package/src/lib/model/crm-list-query.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +19 -2
- package/src/lib/model/lead-filters.js +112 -2
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/native-vocabulary.d.ts +41 -0
- package/src/lib/model/native-vocabulary.js +59 -0
- package/src/lib/model/native-vocabulary.js.map +1 -0
- package/src/lib/model/task-views.d.ts +15 -2
- package/src/lib/model/task-views.js +50 -4
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/email-check.d.ts +4 -1
- package/src/lib/server/email-check.js +18 -6
- package/src/lib/server/email-check.js.map +1 -1
- package/src/lib/transfer/tasks.js +2 -1
- package/src/lib/transfer/tasks.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/source-conversion-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n getCountFromServer,\n limit,\n orderBy,\n query,\n Timestamp,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { conversionBySource } from '../../model/crm-reports'\n\n/**\n * How many of the period's contacts the by-source table is read from.\n *\n * The captured count is a server aggregate and never meets this bound;\n * the table needs each contact's sources and stage off this holder's\n * facet, which no aggregate can read, so it reads the period's newest\n * thousand and says when there were more — the same bound the sources\n * mix keeps, over the period rather than all time.\n */\nconst CONTACT_CEILING = 1000\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = ['Source', 'Captured', 'Customers', 'Conversion'] as const\n\nexport interface SourceConversionCardProps {\n report: CrmReportScope\n}\n\n/**\n * Conversion by source (AGL-2624): of the people captured through each\n * door in the period, how many are customers now.\n *\n * A COHORT by capture date, not a count of purchases in the period: the\n * question is \"how well does each source convert\", and the honest\n * population for it is the people that source brought in, followed to\n * where they stand today. A form lead captured on day one and buying on\n * day twenty is a conversion the form earned; a customer captured last\n * year who bought again this month is not evidence about this period's\n * doors. So the window is the period's contacts by `createdAt` — the\n * `(visibleTo, createdAt)` index the trend card counts through — and\n * \"customer\" is read off the facet: the stage at `customer` or past it,\n * or an order on the books whatever the stage says.\n *\n * Through THIS group's facet, like every by-source figure: a source is\n * which of this business's doors the person came through, and a stage is\n * where this business's team put them.\n */\nexport function SourceConversionCard(props: SourceConversionCardProps) {\n const { report } = props\n const { scope, tokens, groupId, range, period, routes } = report\n const firestore = useFirestore()\n\n const capturedInPeriod = () =>\n query(\n scopedCollection(firestore, scope, 'contacts'),\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(range.from)),\n where('createdAt', '<', Timestamp.fromMillis(range.to)),\n )\n\n const captured = useAggregateRead<number>(\n () => getCountFromServer(capturedInPeriod()).then((snapshot) => snapshot.data().count),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:captured') },\n )\n const window = useWindowRead<Record<string, unknown>>(\n () =>\n query(\n capturedInPeriod(),\n orderBy('createdAt', 'desc'),\n limit(CONTACT_CEILING + 1),\n ),\n CONTACT_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:window') },\n )\n const status = window.status\n\n const conversion = useMemo(\n () =>\n conversionBySource(\n window.rows.map((row) => Aglyn.readContactFacet(row, groupId)),\n ),\n [window, groupId],\n )\n const read = status === 'success'\n const rate = read\n ? measuredRate(conversion.customers, conversion.total, 'captured')\n : null\n const labelOf = (source: Aglyn.ContactSource): string =>\n Aglyn.CONTACT_SOURCE_LABELS[source] ?? source\n const sampled =\n read &&\n (window.truncated ||\n (captured.value !== null && captured.value > window.rows.length))\n const caption = sampled\n ? `Read from the ${window.rows.length.toLocaleString()} most recently captured contacts` +\n (captured.value !== null ? ` of ${captured.value.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n\n return (\n <CardDisplay\n header={'Conversion by source'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#conversion-by-source',\n excerpt:\n 'Of the people each capture surface brought in during the ' +\n 'period, how many are customers now — at the customer stage or ' +\n 'past it, or with an order on the books. Read through this ' +\n 'site’s own view of each person.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('conversion-by-source', period)}\n columns={COLUMNS}\n rows={() =>\n conversion.rows.map((row) => [\n labelOf(row.source),\n row.captured,\n row.customers,\n percent(row.rate),\n ])\n }\n disabled={!read || !conversion.rows.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Captured'}\n value={captured.value !== null ? captured.value.toLocaleString() : null}\n note={'in the period, counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Now customers'}\n value={read ? conversion.customers.toLocaleString() : null}\n note={'of those captured'}\n />\n <ReportStatTile\n label={'Conversion'}\n value={rate ? percent(rate.value) : null}\n note={\n rate\n ? `${rate.numerator.toLocaleString()} of ${rate.denominator.toLocaleString()} ${rate.denominatorLabel}`\n : read\n ? 'nobody captured in the period'\n : undefined\n }\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{'The period’s contacts could not be read.'}</Alert>\n ) : null}\n <Section title={'By source'}>\n {conversion.rows.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {conversion.rows.map((row) => (\n <TableRow key={row.source}>\n <TableCell>{labelOf(row.source)}</TableCell>\n <TableCell align=\"right\">{row.captured.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.customers.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{percent(row.rate)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {status === 'loading' ? 'Reading…' : 'Nobody captured in this period.'}\n </Typography>\n )}\n {conversion.rows.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'A person captured two ways counts under both.' +\n (conversion.unsourced\n ? ` ${conversion.unsourced.toLocaleString()} with no source on this site are counted in the tiles only.`\n : '')}\n </Typography>\n ) : null}\n {/* What the table, and so the export, is drawn from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nSourceConversionCard.displayName = 'SourceConversionCard'\n\nexport default SourceConversionCard\n"],"names":["Aglyn","CardDisplay","measuredRate","percent","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","Timestamp","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","conversionBySource","CONTACT_CEILING","COLUMNS","SourceConversionCard","props","report","scope","tokens","groupId","range","period","routes","firestore","capturedInPeriod","fromMillis","from","to","captured","then","snapshot","data","count","cacheKey","window","status","conversion","rows","map","row","readContactFacet","read","rate","customers","total","labelOf","source","CONTACT_SOURCE_LABELS","sampled","truncated","value","length","caption","toLocaleString","undefined","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","disabled","spacing","direction","sx","flexWrap","label","note","href","section","numerator","denominator","denominatorLabel","severity","title","size","column","index","align","variant","color","unsourced","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/source-conversion-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n getCountFromServer,\n limit,\n orderBy,\n query,\n Timestamp,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { conversionBySource } from '../../model/crm-reports'\n\n/**\n * How many of the period's contacts the by-source table is read from.\n *\n * The captured count is a server aggregate and never meets this bound;\n * the table needs each contact's sources and stage off this holder's\n * facet, which no aggregate can read, so it reads the period's newest\n * thousand and says when there were more — the same bound the sources\n * mix keeps, over the period rather than all time.\n */\nconst CONTACT_CEILING = 1000\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = ['Source', 'Captured', 'Customers', 'Conversion'] as const\n\nexport interface SourceConversionCardProps {\n report: CrmReportScope\n}\n\n/**\n * Conversion by source (AGL-2624): of the people captured through each\n * door in the period, how many are customers now.\n *\n * A COHORT by capture date, not a count of purchases in the period: the\n * question is \"how well does each source convert\", and the honest\n * population for it is the people that source brought in, followed to\n * where they stand today. A form lead captured on day one and buying on\n * day twenty is a conversion the form earned; a customer captured last\n * year who bought again this month is not evidence about this period's\n * doors. So the window is the period's contacts by `createdAt` — the\n * `(visibleTo, createdAt)` index the trend card counts through — and\n * \"customer\" is read off the facet: the stage at `customer` or past it,\n * or an order on the books whatever the stage says.\n *\n * Through THIS group's facet, like every by-source figure: a source is\n * which of this business's doors the person came through, and a stage is\n * where this business's team put them.\n */\nexport function SourceConversionCard(props: SourceConversionCardProps) {\n const { report } = props\n const { scope, tokens, groupId, range, period, routes } = report\n const firestore = useFirestore()\n\n const capturedInPeriod = () =>\n query(\n scopedCollection(firestore, scope, 'contacts'),\n ...visibleToClause(tokens),\n where('createdAt', '>=', Timestamp.fromMillis(range.from)),\n where('createdAt', '<', Timestamp.fromMillis(range.to)),\n )\n\n const captured = useAggregateRead<number>(\n () => getCountFromServer(capturedInPeriod()).then((snapshot) => snapshot.data().count),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:captured') },\n )\n const window = useWindowRead<Record<string, unknown>>(\n () =>\n query(\n capturedInPeriod(),\n orderBy('createdAt', 'desc'),\n limit(CONTACT_CEILING + 1),\n ),\n CONTACT_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'sources:window') },\n )\n const status = window.status\n\n const conversion = useMemo(\n () =>\n conversionBySource(\n window.rows.map((row) => Aglyn.readContactFacet(row, groupId)),\n ),\n [window, groupId],\n )\n const read = status === 'success'\n const rate = read\n ? measuredRate(conversion.customers, conversion.total, 'captured')\n : null\n const labelOf = (source: Aglyn.ContactSource): string =>\n Aglyn.CONTACT_SOURCE_LABELS[source] ?? source\n const sampled =\n read &&\n (window.truncated ||\n (captured.value !== null && captured.value > window.rows.length))\n const caption = sampled\n ? `Read from the ${window.rows.length.toLocaleString()} most recently captured contacts` +\n (captured.value !== null ? ` of ${captured.value.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n\n return (\n <CardDisplay\n header={'Conversion by source'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#conversion-by-source',\n excerpt:\n 'Of the people each capture surface brought in during the ' +\n 'period, how many are customers now — at the customer stage or ' +\n 'past it, or with an order on the books. Read through this ' +\n 'site’s own view of each person.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('conversion-by-source', period)}\n columns={COLUMNS}\n rows={() =>\n conversion.rows.map((row) => [\n labelOf(row.source),\n row.captured,\n row.customers,\n percent(row.rate),\n ])\n }\n disabled={!read || !conversion.rows.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Captured'}\n value={captured.value !== null ? captured.value.toLocaleString() : null}\n note={'in the period, counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Now customers'}\n value={read ? conversion.customers.toLocaleString() : null}\n note={'of those captured'}\n />\n <ReportStatTile\n label={'Conversion'}\n value={rate ? percent(rate.value) : null}\n note={\n rate\n ? `${rate.numerator.toLocaleString()} of ${rate.denominator.toLocaleString()} ${rate.denominatorLabel}`\n : read\n ? 'nobody captured in the period'\n : undefined\n }\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{'The period’s contacts could not be read.'}</Alert>\n ) : null}\n <Section title={'By source'}>\n {conversion.rows.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {conversion.rows.map((row) => (\n <TableRow key={row.source}>\n <TableCell>{labelOf(row.source)}</TableCell>\n <TableCell align=\"right\">{row.captured.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.customers.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{percent(row.rate)}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {status === 'loading' ? 'Reading…' : 'Nobody captured in this period.'}\n </Typography>\n )}\n {conversion.rows.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'A person captured two ways counts under both.' +\n (conversion.unsourced\n ? ` ${conversion.unsourced.toLocaleString()} with no source on this site are counted in the tiles only.`\n : '')}\n </Typography>\n ) : null}\n {/* What the table, and so the export, is drawn from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nSourceConversionCard.displayName = 'SourceConversionCard'\n\nexport default SourceConversionCard\n"],"names":["Aglyn","CardDisplay","measuredRate","percent","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","Timestamp","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","conversionBySource","CONTACT_CEILING","COLUMNS","SourceConversionCard","props","report","scope","tokens","groupId","range","period","routes","firestore","capturedInPeriod","fromMillis","from","to","captured","then","snapshot","data","count","cacheKey","window","status","conversion","rows","map","row","readContactFacet","read","rate","customers","total","labelOf","source","CONTACT_SOURCE_LABELS","sampled","truncated","value","length","caption","toLocaleString","undefined","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","disabled","spacing","useFlexGap","direction","sx","flexWrap","label","note","href","section","numerator","denominator","denominatorLabel","severity","title","size","column","index","align","variant","color","unsourced","displayName"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/tasks-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Button,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n documentId,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { localDayBounds } from '../../model/crm-reports'\n\n/**\n * How many open tasks the by-assignee table is grouped from.\n *\n * The tiles are counts the server takes; the table needs each task's\n * assignee and due state, so it reads the soonest-due thousand and says\n * when there were more.\n */\nconst OPEN_TASK_CEILING = 1000\n/** One `in` query's worth of member documents, for the assignee names. */\nconst ASSIGNEE_NAME_CEILING = 30\n\ntype TaskRow = Aglyn.CrmTask & { $id: string }\n\n/** The by-assignee table's columns, which are also the CSV's. */\nconst ASSIGNEE_COLUMNS = ['Assignee', 'Overdue', 'Today', 'Upcoming', 'No date', 'Open'] as const\n\ninterface AssigneeLoad {\n uid: string\n overdue: number\n today: number\n upcoming: number\n undated: number\n open: number\n}\n\nexport interface TasksCardProps {\n report: CrmReportScope\n}\n\n/**\n * The task load: open, overdue and due today, and who is carrying what\n * (AGL-2604).\n *\n * \"Today\" and \"overdue\" are decided on the LOCAL calendar day, the same day\n * `taskDueState` decides them on, so the count here and the chip on the\n * tasks list agree about the same task at 11 p.m. The counts are range\n * queries on `dueAtMs`, which the `(visibleTo, status, dueAtMs)` index\n * serves; a task with no due date is in the open count and in none of the\n * dated ones.\n *\n * The table reads open tasks ordered by due date. An ordered read admits\n * only documents that CARRY the field, so a task written without `dueAtMs`\n * at all — rather than with `null`, which sorts first — is counted in the\n * tile and absent from the table. The model types the field as optional,\n * so the table says what it is grouped from.\n */\nexport function TasksCard(props: TasksCardProps) {\n const { report } = props\n const { scope, tokens, nowMs, routes } = report\n const firestore = useFirestore()\n const day = useMemo(() => localDayBounds(nowMs), [nowMs])\n\n const openTasks = () =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n )\n\n const counts = useAggregateRead(\n () =>\n Promise.all([\n getCountFromServer(openTasks()),\n getCountFromServer(query(openTasks(), where('dueAtMs', '<', day.start))),\n getCountFromServer(\n query(\n openTasks(),\n where('dueAtMs', '>=', day.start),\n where('dueAtMs', '<', day.end),\n ),\n ),\n ]).then(([open, overdue, today]) => ({\n open: open.data().count,\n overdue: overdue.data().count,\n today: today.data().count,\n })),\n [firestore, scope, tokens, day],\n { cacheKey: reportCacheKey(report, 'tasks:counts') },\n )\n\n const taskWindow = useWindowRead<TaskRow>(\n () =>\n query(\n openTasks(),\n orderBy('dueAtMs', 'asc'),\n limit(OPEN_TASK_CEILING + 1),\n ),\n OPEN_TASK_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'tasks:open') },\n )\n const tasksStatus = taskWindow.status\n\n const load = useMemo(() => {\n const byAssignee = new Map<string, AssigneeLoad>()\n for (const task of taskWindow.rows) {\n const uid = task.assigneeUid || ''\n const row = byAssignee.get(uid) ?? {\n uid,\n overdue: 0,\n today: 0,\n upcoming: 0,\n undated: 0,\n open: 0,\n }\n const state = Aglyn.taskDueState(task, nowMs)\n if (state === 'overdue') row.overdue += 1\n else if (state === 'today') row.today += 1\n else if (state === 'upcoming') row.upcoming += 1\n else row.undated += 1\n row.open += 1\n byAssignee.set(uid, row)\n }\n return [...byAssignee.values()].sort(\n (a, b) => b.open - a.open || a.uid.localeCompare(b.uid),\n )\n }, [taskWindow, nowMs])\n\n /*\n * The assignees' names, from their member documents — one `in` query for\n * up to thirty, which is Firestore's cap on the operator and more assignees\n * than a tasks list has. Anyone past it, or a member document the reader\n * cannot open, shows as their id rather than blocking the table.\n */\n const uids = load.map((row) => row.uid).filter(Boolean).slice(0, ASSIGNEE_NAME_CEILING)\n const uidKey = uids.join(',')\n const names = useAggregateRead<Record<string, string>>(\n () =>\n uids.length\n ? getDocs(\n query(\n scopedCollection(firestore, scope, 'members'),\n where(documentId(), 'in', uids),\n ),\n ).then((snapshot) =>\n Object.fromEntries(\n snapshot.docs.map((member) => [\n member.id,\n String(member.get('displayName') || member.get('email') || member.id),\n ]),\n ),\n )\n : Promise.resolve({}),\n [firestore, scope, uidKey],\n { cacheKey: reportCacheKey(report, `tasks:names:${uidKey}`) },\n )\n\n const figures = counts.value\n const assigneeName = (row: AssigneeLoad): string =>\n row.uid ? names.value?.[row.uid] ?? row.uid : 'Unassigned'\n return (\n <CardDisplay\n header={'Tasks'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#tasks',\n excerpt:\n 'Open tasks, how many are overdue or due today, and the open ' +\n 'load per assignee. Today and overdue are decided on your calendar ' +\n 'day, the same way the tasks list decides them.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <ReportExport\n filename={reportFilename('tasks-by-assignee')}\n columns={ASSIGNEE_COLUMNS}\n rows={() =>\n load.map((row) => [\n assigneeName(row),\n row.overdue,\n row.today,\n row.upcoming,\n row.undated,\n row.open,\n ])\n }\n disabled={tasksStatus !== 'success' || !load.length}\n />\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('tasks')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open tasks'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open tasks'}\n value={figures ? figures.open.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Overdue'}\n value={figures ? figures.overdue.toLocaleString() : null}\n note={'due before today'}\n color={figures?.overdue ? 'error.main' : undefined}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Due today'}\n value={figures ? figures.today.toLocaleString() : null}\n note={'on your calendar day'}\n href={routes.section('tasks')}\n />\n </Stack>\n {counts.status === 'error' ? (\n <Alert severity=\"warning\">{'The task counts could not be read.'}</Alert>\n ) : null}\n <Section title={'Open tasks by assignee'}>\n {load.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {ASSIGNEE_COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {load.map((row) => (\n <TableRow key={row.uid || '$unassigned'}>\n <TableCell>{assigneeName(row)}</TableCell>\n <TableCell\n align=\"right\"\n sx={row.overdue ? { color: 'error.main' } : undefined}\n >\n {row.overdue.toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">{row.today.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.upcoming.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.undated.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.open.toLocaleString()}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {tasksStatus === 'loading' ? 'Reading…' : 'No open tasks.'}\n </Typography>\n )}\n {/* What the table, and so the export, is grouped from when that is not everything. */}\n {taskWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Grouped from the ${OPEN_TASK_CEILING.toLocaleString()} soonest-due open tasks; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nTasksCard.displayName = 'TasksCard'\n\nexport default TasksCard\n"],"names":["Aglyn","AppLink","CardDisplay","Section","ScrollTable","Alert","Button","Stack","TableBody","TableCell","TableHead","TableRow","Typography","documentId","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","localDayBounds","OPEN_TASK_CEILING","ASSIGNEE_NAME_CEILING","ASSIGNEE_COLUMNS","TasksCard","props","report","scope","tokens","nowMs","routes","firestore","day","openTasks","CRM_COLLECTIONS","tasks","counts","Promise","all","start","end","then","open","overdue","today","data","count","cacheKey","taskWindow","tasksStatus","status","load","byAssignee","Map","task","rows","uid","assigneeUid","row","get","upcoming","undated","state","taskDueState","set","values","sort","a","b","localeCompare","uids","map","filter","Boolean","slice","uidKey","join","names","length","snapshot","Object","fromEntries","docs","member","id","String","resolve","figures","value","assigneeName","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","filename","columns","disabled","component","componentVariant","nativeButton","href","section","size","color","sx","flexWrap","label","toLocaleString","note","undefined","severity","title","column","index","align","variant","truncated","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/tasks-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Button,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n documentId,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { localDayBounds } from '../../model/crm-reports'\n\n/**\n * How many open tasks the by-assignee table is grouped from.\n *\n * The tiles are counts the server takes; the table needs each task's\n * assignee and due state, so it reads the soonest-due thousand and says\n * when there were more.\n */\nconst OPEN_TASK_CEILING = 1000\n/** One `in` query's worth of member documents, for the assignee names. */\nconst ASSIGNEE_NAME_CEILING = 30\n\ntype TaskRow = Aglyn.CrmTask & { $id: string }\n\n/** The by-assignee table's columns, which are also the CSV's. */\nconst ASSIGNEE_COLUMNS = ['Assignee', 'Overdue', 'Today', 'Upcoming', 'No date', 'Open'] as const\n\ninterface AssigneeLoad {\n uid: string\n overdue: number\n today: number\n upcoming: number\n undated: number\n open: number\n}\n\nexport interface TasksCardProps {\n report: CrmReportScope\n}\n\n/**\n * The task load: open, overdue and due today, and who is carrying what\n * (AGL-2604).\n *\n * \"Today\" and \"overdue\" are decided on the LOCAL calendar day, the same day\n * `taskDueState` decides them on, so the count here and the chip on the\n * tasks list agree about the same task at 11 p.m. The counts are range\n * queries on `dueAtMs`, which the `(visibleTo, status, dueAtMs)` index\n * serves; a task with no due date is in the open count and in none of the\n * dated ones.\n *\n * The table reads open tasks ordered by due date. An ordered read admits\n * only documents that CARRY the field, so a task written without `dueAtMs`\n * at all — rather than with `null`, which sorts first — is counted in the\n * tile and absent from the table. The model types the field as optional,\n * so the table says what it is grouped from.\n */\nexport function TasksCard(props: TasksCardProps) {\n const { report } = props\n const { scope, tokens, nowMs, routes } = report\n const firestore = useFirestore()\n const day = useMemo(() => localDayBounds(nowMs), [nowMs])\n\n const openTasks = () =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks),\n ...visibleToClause(tokens),\n where('status', '==', 'open'),\n )\n\n const counts = useAggregateRead(\n () =>\n Promise.all([\n getCountFromServer(openTasks()),\n getCountFromServer(query(openTasks(), where('dueAtMs', '<', day.start))),\n getCountFromServer(\n query(\n openTasks(),\n where('dueAtMs', '>=', day.start),\n where('dueAtMs', '<', day.end),\n ),\n ),\n ]).then(([open, overdue, today]) => ({\n open: open.data().count,\n overdue: overdue.data().count,\n today: today.data().count,\n })),\n [firestore, scope, tokens, day],\n { cacheKey: reportCacheKey(report, 'tasks:counts') },\n )\n\n const taskWindow = useWindowRead<TaskRow>(\n () =>\n query(\n openTasks(),\n orderBy('dueAtMs', 'asc'),\n limit(OPEN_TASK_CEILING + 1),\n ),\n OPEN_TASK_CEILING,\n [firestore, scope, tokens, nowMs],\n { cacheKey: reportCacheKey(report, 'tasks:open') },\n )\n const tasksStatus = taskWindow.status\n\n const load = useMemo(() => {\n const byAssignee = new Map<string, AssigneeLoad>()\n for (const task of taskWindow.rows) {\n const uid = task.assigneeUid || ''\n const row = byAssignee.get(uid) ?? {\n uid,\n overdue: 0,\n today: 0,\n upcoming: 0,\n undated: 0,\n open: 0,\n }\n const state = Aglyn.taskDueState(task, nowMs)\n if (state === 'overdue') row.overdue += 1\n else if (state === 'today') row.today += 1\n else if (state === 'upcoming') row.upcoming += 1\n else row.undated += 1\n row.open += 1\n byAssignee.set(uid, row)\n }\n return [...byAssignee.values()].sort(\n (a, b) => b.open - a.open || a.uid.localeCompare(b.uid),\n )\n }, [taskWindow, nowMs])\n\n /*\n * The assignees' names, from their member documents — one `in` query for\n * up to thirty, which is Firestore's cap on the operator and more assignees\n * than a tasks list has. Anyone past it, or a member document the reader\n * cannot open, shows as their id rather than blocking the table.\n */\n const uids = load.map((row) => row.uid).filter(Boolean).slice(0, ASSIGNEE_NAME_CEILING)\n const uidKey = uids.join(',')\n const names = useAggregateRead<Record<string, string>>(\n () =>\n uids.length\n ? getDocs(\n query(\n scopedCollection(firestore, scope, 'members'),\n where(documentId(), 'in', uids),\n ),\n ).then((snapshot) =>\n Object.fromEntries(\n snapshot.docs.map((member) => [\n member.id,\n String(member.get('displayName') || member.get('email') || member.id),\n ]),\n ),\n )\n : Promise.resolve({}),\n [firestore, scope, uidKey],\n { cacheKey: reportCacheKey(report, `tasks:names:${uidKey}`) },\n )\n\n const figures = counts.value\n const assigneeName = (row: AssigneeLoad): string =>\n row.uid ? names.value?.[row.uid] ?? row.uid : 'Unassigned'\n return (\n <CardDisplay\n header={'Tasks'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#tasks',\n excerpt:\n 'Open tasks, how many are overdue or due today, and the open ' +\n 'load per assignee. Today and overdue are decided on your calendar ' +\n 'day, the same way the tasks list decides them.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <ReportExport\n filename={reportFilename('tasks-by-assignee')}\n columns={ASSIGNEE_COLUMNS}\n rows={() =>\n load.map((row) => [\n assigneeName(row),\n row.overdue,\n row.today,\n row.upcoming,\n row.undated,\n row.open,\n ])\n }\n disabled={tasksStatus !== 'success' || !load.length}\n />\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('tasks')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open tasks'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack useFlexGap direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Open tasks'}\n value={figures ? figures.open.toLocaleString() : null}\n note={'counted on the server'}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Overdue'}\n value={figures ? figures.overdue.toLocaleString() : null}\n note={'due before today'}\n color={figures?.overdue ? 'error.main' : undefined}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Due today'}\n value={figures ? figures.today.toLocaleString() : null}\n note={'on your calendar day'}\n href={routes.section('tasks')}\n />\n </Stack>\n {counts.status === 'error' ? (\n <Alert severity=\"warning\">{'The task counts could not be read.'}</Alert>\n ) : null}\n <Section title={'Open tasks by assignee'}>\n {load.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {ASSIGNEE_COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {load.map((row) => (\n <TableRow key={row.uid || '$unassigned'}>\n <TableCell>{assigneeName(row)}</TableCell>\n <TableCell\n align=\"right\"\n sx={row.overdue ? { color: 'error.main' } : undefined}\n >\n {row.overdue.toLocaleString()}\n </TableCell>\n <TableCell align=\"right\">{row.today.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.upcoming.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.undated.toLocaleString()}</TableCell>\n <TableCell align=\"right\">{row.open.toLocaleString()}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {tasksStatus === 'loading' ? 'Reading…' : 'No open tasks.'}\n </Typography>\n )}\n {/* What the table, and so the export, is grouped from when that is not everything. */}\n {taskWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Grouped from the ${OPEN_TASK_CEILING.toLocaleString()} soonest-due open tasks; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nTasksCard.displayName = 'TasksCard'\n\nexport default TasksCard\n"],"names":["Aglyn","AppLink","CardDisplay","Section","ScrollTable","Alert","Button","Stack","TableBody","TableCell","TableHead","TableRow","Typography","documentId","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","localDayBounds","OPEN_TASK_CEILING","ASSIGNEE_NAME_CEILING","ASSIGNEE_COLUMNS","TasksCard","props","report","scope","tokens","nowMs","routes","firestore","day","openTasks","CRM_COLLECTIONS","tasks","counts","Promise","all","start","end","then","open","overdue","today","data","count","cacheKey","taskWindow","tasksStatus","status","load","byAssignee","Map","task","rows","uid","assigneeUid","row","get","upcoming","undated","state","taskDueState","set","values","sort","a","b","localeCompare","uids","map","filter","Boolean","slice","uidKey","join","names","length","snapshot","Object","fromEntries","docs","member","id","String","resolve","figures","value","assigneeName","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","direction","spacing","filename","columns","disabled","component","componentVariant","nativeButton","href","section","size","color","useFlexGap","sx","flexWrap","label","toLocaleString","note","undefined","severity","title","column","index","align","variant","truncated","displayName"],"mappings":"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"}
|
|
@@ -162,6 +162,7 @@ import { CRM_REPORT_PERIOD_LABELS, CRM_REPORT_PERIODS, crmReportRange } from "..
|
|
|
162
162
|
spacing: 2,
|
|
163
163
|
children: [
|
|
164
164
|
/*#__PURE__*/ _jsxs(Stack, {
|
|
165
|
+
useFlexGap: true,
|
|
165
166
|
direction: "row",
|
|
166
167
|
spacing: 2,
|
|
167
168
|
sx: {
|
|
@@ -176,6 +177,7 @@ import { CRM_REPORT_PERIOD_LABELS, CRM_REPORT_PERIODS, crmReportRange } from "..
|
|
|
176
177
|
children: 'Reports'
|
|
177
178
|
}),
|
|
178
179
|
/*#__PURE__*/ _jsxs(Stack, {
|
|
180
|
+
useFlexGap: true,
|
|
179
181
|
direction: "row",
|
|
180
182
|
spacing: 1,
|
|
181
183
|
sx: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/reports-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { mdiRefresh } from '@aglyn/shared-data-mdi'\nimport {\n MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { CardColumns } from '@aglyn/shared-ui-jsx/components/card-columns'\nimport {\n Button,\n Stack,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport { getCountFromServer, query } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { crmRoutes } from '../model/crm-routes'\nimport { ActivityCard } from './reports/activity-card'\nimport { ClosedDealsCard } from './reports/closed-deals-card'\nimport { ContactsMixCard } from './reports/contacts-mix-card'\nimport { ContactsTrendCard } from './reports/contacts-trend-card'\nimport { ForecastCard } from './reports/forecast-card'\nimport { LeadFunnelCard } from './reports/lead-funnel-card'\nimport { LeadSourceCard } from './reports/lead-source-card'\nimport { PipelineCard } from './reports/pipeline-card'\nimport { SourceConversionCard } from './reports/source-conversion-card'\nimport { WonLostByOwnerCard } from './reports/won-lost-by-owner-card'\nimport {\n type CrmReportScope,\n reportCacheKey,\n reportCachePrefix,\n scopedCollection,\n visibleToClause,\n} from './reports/report-scope'\nimport { TasksCard } from './reports/tasks-card'\nimport { invalidateAggregateReads, useAggregateRead } from './reports/use-aggregate-read'\nimport {\n CRM_REPORT_PERIOD_LABELS,\n CRM_REPORT_PERIODS,\n type CrmReportPeriod,\n crmReportRange,\n} from '../model/crm-reports'\n\n/**\n * `/crm/reports` — the CRM in aggregate (AGL-2604, AGL-2624).\n *\n * Nine cards, each reading its own collection: contacts over time, contacts\n * by source and stage, which sources convert, this site's lead funnel, the\n * open pipeline, what closed, who logged what, the forecast by close month\n * (AGL-2620, over the pipeline card's own window), and the task load. This\n * section resolves what they share and hands it down — see `CrmReportScope`\n * — so that every card counts the same reader's records over the same\n * clock. The lead funnel is the one card handed the site as well: leads live\n * under the host, not the org.\n *\n * ## The period is a control, and the reads follow it\n *\n * Nothing on this page reads a year on mount. The picker names the period,\n * the period sizes every read (one count per week of it, the deals closed\n * within it), and the default is thirty days. The three cards that are\n * stocks rather than flows — the open pipeline, its forecast, the open\n * tasks — ignore the period because \"what is open\" has no period.\n *\n * ## Scoped like the list\n *\n * Every query carries `visibleTo array-contains-any` over the group's\n * tokens, the predicate the rules evaluate, so a report can never count a\n * record its reader could not open — and the total here is the reader's\n * total, not the org's billable audience, which is the head-count the\n * contacts list keeps for the quota.\n *\n * ## Read once a minute, not once an arrival\n *\n * Every read on this page is keyed by the scope and the period and kept\n * for `AGGREGATE_READ_TTL_MS` — see `useAggregateRead` — so leaving to open\n * a deal the report named and coming straight back costs nothing. Refresh\n * forgets this reader's answers for this org and anchors the period again,\n * which is what makes every card ask afresh.\n */\nexport function ContactsReportsSection(props: ConsolePluginPageProps) {\n const { hostId, org, basePath } = props\n const firestore = useFirestore()\n // The org data root — null until the org lookup settles, and the page\n // reads nothing until it does — and the controller this page is viewed\n // as, resolved the way every CRM surface resolves them so the facet a\n // stage is read from here is the facet the contacts list edits it in.\n const { scope: dataScope, consentGroup, visibleTo } = useCrmScope({ hostId, org })\n // `null` at the organization level (AGL-2630): no clause, and every card\n // totals every site; the holder is then each contact's own.\n const tokens = useMemo(() => (visibleTo ? [...visibleTo] : null), [visibleTo])\n const groupId = consentGroup?.groupId ?? null\n const orgRecord = (org ?? null) as Record<string, unknown> | null\n /*\n * The period AND the moment it was chosen, as one value: the range is\n * anchored when the reader picks, not re-derived from a moving `Date.now()`\n * per render, which would rebuild every query on every pass.\n */\n const [view, setView] = useState<{\n period: CrmReportPeriod\n nowMs: number\n }>(() => ({ period: '30d', nowMs: Date.now() }))\n const range = useMemo(\n () => crmReportRange(view.period, view.nowMs),\n [view],\n )\n const routes = useMemo(() => crmRoutes(basePath ?? ''), [basePath])\n const refresh = useCallback(() => {\n if (dataScope) {\n invalidateAggregateReads(reportCachePrefix({ scope: dataScope, tokens, groupId }))\n }\n // A new anchor is what changes every card's dependencies, so each read\n // runs again and — its key forgotten — reaches the server.\n setView((current) => ({ period: current.period, nowMs: Date.now() }))\n }, [dataScope, tokens, groupId])\n\n const report = useMemo<CrmReportScope | null>(\n () =>\n dataScope\n ? {\n scope: dataScope,\n tokens,\n groupId,\n org: orgRecord,\n period: view.period,\n range,\n nowMs: view.nowMs,\n routes,\n }\n : null,\n [dataScope, tokens, groupId, orgRecord, view, range, routes],\n )\n\n /**\n * Every contact this reader may see, counted once for the two cards that\n * quote it — the tile, and the sample notice that compares the read to it.\n */\n const totalContacts = useAggregateRead<number>(\n () =>\n dataScope\n ? getCountFromServer(\n query(\n scopedCollection(firestore, dataScope, 'contacts'),\n ...visibleToClause(tokens),\n ),\n ).then((snapshot) => snapshot.data().count)\n : null,\n // The anchor is a dependency so Refresh re-asks; the key, which omits\n // it, is what answers a reopened section from memory.\n [firestore, dataScope, tokens, view.nowMs],\n report ? { cacheKey: reportCacheKey(report, 'contacts:total') } : {},\n )\n\n return (\n <Stack spacing={2}>\n <Stack\n direction=\"row\"\n spacing={2}\n sx={{\n alignItems: 'center',\n justifyContent: 'space-between',\n flexWrap: 'wrap',\n }}\n >\n <Typography variant=\"h6\" component=\"h2\">\n {'Reports'}\n </Typography>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}>\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={view.period}\n onChange={(_event, next) => {\n if (next) {\n setView({ period: next as CrmReportPeriod, nowMs: Date.now() })\n }\n }}\n aria-label=\"Report period\"\n >\n {CRM_REPORT_PERIODS.map((period) => (\n <ToggleButton key={period} value={period}>\n {CRM_REPORT_PERIOD_LABELS[period]}\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiRefresh.path} size={0.8} />}\n disabled={!report}\n onClick={refresh}\n >\n {'Refresh'}\n </Button>\n </Stack>\n </Stack>\n {report ? (\n <>\n {/*\n The same masonry the billing and staff pages use: columns as tall\n as their own contents, so a short card beside a tall one leaves no\n hole. Measured live before this changed — 271, 272 and 111 pixels\n of dead space down one report, and an odd card stranded with an\n empty column beside it.\n */}\n <CardColumns\n spacing={2}\n items={[\n {\n key: 'trend',\n children: (\n <ContactsTrendCard report={report} totalContacts={totalContacts} />\n ),\n },\n {\n key: 'mix',\n children: (\n <ContactsMixCard report={report} totalContacts={totalContacts} />\n ),\n },\n { key: 'source-conversion', children: <SourceConversionCard report={report} /> },\n { key: 'lead-funnel', children: <LeadFunnelCard report={report} hostId={hostId} /> },\n { key: 'lead-sources', children: <LeadSourceCard report={report} hostId={hostId} /> },\n { key: 'pipeline', children: <PipelineCard report={report} /> },\n {\n key: 'closed',\n /*\n One column item carrying two cards, because multicol picks\n its own break: two consecutive items are only USUALLY\n adjacent, and a card that is a footnote to the one above it\n cannot be left to that. Won and lost by owner asks a second\n question of the very rows Won and lost already read — the\n same window under the same `closed:won` and `closed:lost`\n keys — so it is read as that card's breakdown, and\n `break-inside: avoid` keeps the pair whole in one column.\n */\n children: (\n <Stack spacing={2}>\n <ClosedDealsCard report={report} />\n <WonLostByOwnerCard report={report} />\n </Stack>\n ),\n },\n ]}\n />\n {/*\n Outside the columns: multicol cannot span, and these three read\n across the full width.\n */}\n <Stack spacing={2}>\n <ActivityCard report={report} />\n <ForecastCard report={report} />\n <TasksCard report={report} />\n </Stack>\n </>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n )}\n </Stack>\n )\n}\nContactsReportsSection.displayName = 'ContactsReportsSection'\n\nexport default ContactsReportsSection\n"],"names":["mdiRefresh","MdiIcon","CardColumns","Button","Stack","ToggleButton","ToggleButtonGroup","Typography","getCountFromServer","query","useCallback","useMemo","useState","useFirestore","useCrmScope","crmRoutes","ActivityCard","ClosedDealsCard","ContactsMixCard","ContactsTrendCard","ForecastCard","LeadFunnelCard","LeadSourceCard","PipelineCard","SourceConversionCard","WonLostByOwnerCard","reportCacheKey","reportCachePrefix","scopedCollection","visibleToClause","TasksCard","invalidateAggregateReads","useAggregateRead","CRM_REPORT_PERIOD_LABELS","CRM_REPORT_PERIODS","crmReportRange","ContactsReportsSection","props","hostId","org","basePath","firestore","scope","dataScope","consentGroup","visibleTo","tokens","groupId","orgRecord","view","setView","period","nowMs","Date","now","range","routes","refresh","current","report","totalContacts","then","snapshot","data","count","cacheKey","spacing","direction","sx","alignItems","justifyContent","flexWrap","variant","component","rowGap","exclusive","size","color","value","onChange","_event","next","aria-label","map","startIcon","path","disabled","onClick","items","key","children","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/reports-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { mdiRefresh } from '@aglyn/shared-data-mdi'\nimport {\n MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { CardColumns } from '@aglyn/shared-ui-jsx/components/card-columns'\nimport {\n Button,\n Stack,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport { getCountFromServer, query } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { crmRoutes } from '../model/crm-routes'\nimport { ActivityCard } from './reports/activity-card'\nimport { ClosedDealsCard } from './reports/closed-deals-card'\nimport { ContactsMixCard } from './reports/contacts-mix-card'\nimport { ContactsTrendCard } from './reports/contacts-trend-card'\nimport { ForecastCard } from './reports/forecast-card'\nimport { LeadFunnelCard } from './reports/lead-funnel-card'\nimport { LeadSourceCard } from './reports/lead-source-card'\nimport { PipelineCard } from './reports/pipeline-card'\nimport { SourceConversionCard } from './reports/source-conversion-card'\nimport { WonLostByOwnerCard } from './reports/won-lost-by-owner-card'\nimport {\n type CrmReportScope,\n reportCacheKey,\n reportCachePrefix,\n scopedCollection,\n visibleToClause,\n} from './reports/report-scope'\nimport { TasksCard } from './reports/tasks-card'\nimport { invalidateAggregateReads, useAggregateRead } from './reports/use-aggregate-read'\nimport {\n CRM_REPORT_PERIOD_LABELS,\n CRM_REPORT_PERIODS,\n type CrmReportPeriod,\n crmReportRange,\n} from '../model/crm-reports'\n\n/**\n * `/crm/reports` — the CRM in aggregate (AGL-2604, AGL-2624).\n *\n * Nine cards, each reading its own collection: contacts over time, contacts\n * by source and stage, which sources convert, this site's lead funnel, the\n * open pipeline, what closed, who logged what, the forecast by close month\n * (AGL-2620, over the pipeline card's own window), and the task load. This\n * section resolves what they share and hands it down — see `CrmReportScope`\n * — so that every card counts the same reader's records over the same\n * clock. The lead funnel is the one card handed the site as well: leads live\n * under the host, not the org.\n *\n * ## The period is a control, and the reads follow it\n *\n * Nothing on this page reads a year on mount. The picker names the period,\n * the period sizes every read (one count per week of it, the deals closed\n * within it), and the default is thirty days. The three cards that are\n * stocks rather than flows — the open pipeline, its forecast, the open\n * tasks — ignore the period because \"what is open\" has no period.\n *\n * ## Scoped like the list\n *\n * Every query carries `visibleTo array-contains-any` over the group's\n * tokens, the predicate the rules evaluate, so a report can never count a\n * record its reader could not open — and the total here is the reader's\n * total, not the org's billable audience, which is the head-count the\n * contacts list keeps for the quota.\n *\n * ## Read once a minute, not once an arrival\n *\n * Every read on this page is keyed by the scope and the period and kept\n * for `AGGREGATE_READ_TTL_MS` — see `useAggregateRead` — so leaving to open\n * a deal the report named and coming straight back costs nothing. Refresh\n * forgets this reader's answers for this org and anchors the period again,\n * which is what makes every card ask afresh.\n */\nexport function ContactsReportsSection(props: ConsolePluginPageProps) {\n const { hostId, org, basePath } = props\n const firestore = useFirestore()\n // The org data root — null until the org lookup settles, and the page\n // reads nothing until it does — and the controller this page is viewed\n // as, resolved the way every CRM surface resolves them so the facet a\n // stage is read from here is the facet the contacts list edits it in.\n const { scope: dataScope, consentGroup, visibleTo } = useCrmScope({ hostId, org })\n // `null` at the organization level (AGL-2630): no clause, and every card\n // totals every site; the holder is then each contact's own.\n const tokens = useMemo(() => (visibleTo ? [...visibleTo] : null), [visibleTo])\n const groupId = consentGroup?.groupId ?? null\n const orgRecord = (org ?? null) as Record<string, unknown> | null\n /*\n * The period AND the moment it was chosen, as one value: the range is\n * anchored when the reader picks, not re-derived from a moving `Date.now()`\n * per render, which would rebuild every query on every pass.\n */\n const [view, setView] = useState<{\n period: CrmReportPeriod\n nowMs: number\n }>(() => ({ period: '30d', nowMs: Date.now() }))\n const range = useMemo(\n () => crmReportRange(view.period, view.nowMs),\n [view],\n )\n const routes = useMemo(() => crmRoutes(basePath ?? ''), [basePath])\n const refresh = useCallback(() => {\n if (dataScope) {\n invalidateAggregateReads(reportCachePrefix({ scope: dataScope, tokens, groupId }))\n }\n // A new anchor is what changes every card's dependencies, so each read\n // runs again and — its key forgotten — reaches the server.\n setView((current) => ({ period: current.period, nowMs: Date.now() }))\n }, [dataScope, tokens, groupId])\n\n const report = useMemo<CrmReportScope | null>(\n () =>\n dataScope\n ? {\n scope: dataScope,\n tokens,\n groupId,\n org: orgRecord,\n period: view.period,\n range,\n nowMs: view.nowMs,\n routes,\n }\n : null,\n [dataScope, tokens, groupId, orgRecord, view, range, routes],\n )\n\n /**\n * Every contact this reader may see, counted once for the two cards that\n * quote it — the tile, and the sample notice that compares the read to it.\n */\n const totalContacts = useAggregateRead<number>(\n () =>\n dataScope\n ? getCountFromServer(\n query(\n scopedCollection(firestore, dataScope, 'contacts'),\n ...visibleToClause(tokens),\n ),\n ).then((snapshot) => snapshot.data().count)\n : null,\n // The anchor is a dependency so Refresh re-asks; the key, which omits\n // it, is what answers a reopened section from memory.\n [firestore, dataScope, tokens, view.nowMs],\n report ? { cacheKey: reportCacheKey(report, 'contacts:total') } : {},\n )\n\n return (\n <Stack spacing={2}>\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={2}\n sx={{\n alignItems: 'center',\n justifyContent: 'space-between',\n flexWrap: 'wrap',\n }}\n >\n <Typography variant=\"h6\" component=\"h2\">\n {'Reports'}\n </Typography>\n <Stack useFlexGap direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}>\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={view.period}\n onChange={(_event, next) => {\n if (next) {\n setView({ period: next as CrmReportPeriod, nowMs: Date.now() })\n }\n }}\n aria-label=\"Report period\"\n >\n {CRM_REPORT_PERIODS.map((period) => (\n <ToggleButton key={period} value={period}>\n {CRM_REPORT_PERIOD_LABELS[period]}\n </ToggleButton>\n ))}\n </ToggleButtonGroup>\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiRefresh.path} size={0.8} />}\n disabled={!report}\n onClick={refresh}\n >\n {'Refresh'}\n </Button>\n </Stack>\n </Stack>\n {report ? (\n <>\n {/*\n The same masonry the billing and staff pages use: columns as tall\n as their own contents, so a short card beside a tall one leaves no\n hole. Measured live before this changed — 271, 272 and 111 pixels\n of dead space down one report, and an odd card stranded with an\n empty column beside it.\n */}\n <CardColumns\n spacing={2}\n items={[\n {\n key: 'trend',\n children: (\n <ContactsTrendCard report={report} totalContacts={totalContacts} />\n ),\n },\n {\n key: 'mix',\n children: (\n <ContactsMixCard report={report} totalContacts={totalContacts} />\n ),\n },\n { key: 'source-conversion', children: <SourceConversionCard report={report} /> },\n { key: 'lead-funnel', children: <LeadFunnelCard report={report} hostId={hostId} /> },\n { key: 'lead-sources', children: <LeadSourceCard report={report} hostId={hostId} /> },\n { key: 'pipeline', children: <PipelineCard report={report} /> },\n {\n key: 'closed',\n /*\n One column item carrying two cards, because multicol picks\n its own break: two consecutive items are only USUALLY\n adjacent, and a card that is a footnote to the one above it\n cannot be left to that. Won and lost by owner asks a second\n question of the very rows Won and lost already read — the\n same window under the same `closed:won` and `closed:lost`\n keys — so it is read as that card's breakdown, and\n `break-inside: avoid` keeps the pair whole in one column.\n */\n children: (\n <Stack spacing={2}>\n <ClosedDealsCard report={report} />\n <WonLostByOwnerCard report={report} />\n </Stack>\n ),\n },\n ]}\n />\n {/*\n Outside the columns: multicol cannot span, and these three read\n across the full width.\n */}\n <Stack spacing={2}>\n <ActivityCard report={report} />\n <ForecastCard report={report} />\n <TasksCard report={report} />\n </Stack>\n </>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading…'}\n </Typography>\n )}\n </Stack>\n )\n}\nContactsReportsSection.displayName = 'ContactsReportsSection'\n\nexport default ContactsReportsSection\n"],"names":["mdiRefresh","MdiIcon","CardColumns","Button","Stack","ToggleButton","ToggleButtonGroup","Typography","getCountFromServer","query","useCallback","useMemo","useState","useFirestore","useCrmScope","crmRoutes","ActivityCard","ClosedDealsCard","ContactsMixCard","ContactsTrendCard","ForecastCard","LeadFunnelCard","LeadSourceCard","PipelineCard","SourceConversionCard","WonLostByOwnerCard","reportCacheKey","reportCachePrefix","scopedCollection","visibleToClause","TasksCard","invalidateAggregateReads","useAggregateRead","CRM_REPORT_PERIOD_LABELS","CRM_REPORT_PERIODS","crmReportRange","ContactsReportsSection","props","hostId","org","basePath","firestore","scope","dataScope","consentGroup","visibleTo","tokens","groupId","orgRecord","view","setView","period","nowMs","Date","now","range","routes","refresh","current","report","totalContacts","then","snapshot","data","count","cacheKey","spacing","useFlexGap","direction","sx","alignItems","justifyContent","flexWrap","variant","component","rowGap","exclusive","size","color","value","onChange","_event","next","aria-label","map","startIcon","path","disabled","onClick","items","key","children","displayName"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/tasks-calendar.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { type CrmTaskPicklists, crmTaskPicklistLabels } from '@aglyn/aglyn'\nimport { mdiChevronLeft, mdiChevronRight } from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { Box, Button, ButtonBase, IconButton, Stack, Typography } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport type { CrmTaskRow } from '../hooks/use-crm-tasks'\nimport {\n bucketTasksByCalendarDay,\n CALENDAR_DAY_LABELS,\n CALENDAR_WEEK_DAYS,\n shiftCalendarMonth,\n taskCalendarMonth,\n} from '../model/task-calendar'\n\n/** How many tasks a square lists before folding the rest into a count. */\nconst TASKS_PER_DAY = 3\n\nexport interface TasksCalendarProps {\n /** The window the list is holding — the calendar reads no more than it. */\n tasks: readonly CrmTaskRow[]\n /** Open one task's drawer, the same drawer a row opens. */\n onOpen: (task: CrmTaskRow) => void\n /** The list's window filled, so neither surface is showing everything. */\n truncated?: boolean\n /** The org's task lists, which name each entry's type and status on hover (AGL-3517). */\n picklists?: CrmTaskPicklists\n}\n\n/**\n * A MONTH OF TASKS, BY DUE DATE (AGL-2662).\n *\n * The Tasks list answers \"what is on my plate\"; this answers \"what does\n * next week look like\", which a list ordered by due date can only imply.\n *\n * ## Drawn over the list's own window, and never over a second read\n *\n * The rows are the ones the section already holds. A calendar that fetched\n * its own month would be a second listener over the same collection, would\n * disagree with the list whenever the two windows differed, and would put\n * a read behind every page of the arrows. What it costs instead is honesty:\n * the grid can only place what the window carries, so the tasks due in\n * other months are counted under it rather than implied to be absent.\n *\n * ## Hand-built rather than a calendar dependency\n *\n * Seven columns of MUI boxes and the local-day arithmetic in\n * `task-calendar.ts`. The one calendar package already installed\n * (`@mui/x-date-pickers`) draws a date PICKER — a grid whose squares are\n * choices, not content — so it answers a different question, and a\n * scheduling library for one surface is a dependency the whole console\n * would carry.\n */\nexport function TasksCalendar(props: TasksCalendarProps) {\n const { tasks, onOpen, truncated, picklists } = props\n /** \"Call Ada · Call · In Progress\" — the entry's type and status on hover. */\n const hoverText = (task: CrmTaskRow) => {\n if (!picklists) return task.title\n const labels = crmTaskPicklistLabels(task, picklists)\n return `${task.title} · ${labels.type} · ${labels.status}`\n }\n /*\n * The clock is read ONCE, at mount. A component that read `Date.now()`\n * per render would move the today square under a reader who left the tab\n * open across midnight, without any state having changed — and the grid\n * is a pure function of this, so what is drawn can be pinned.\n */\n const [nowMs] = useState(() => Date.now())\n const [anchorMs, setAnchorMs] = useState(() => nowMs)\n\n const month = useMemo(\n () => taskCalendarMonth(anchorMs, nowMs),\n [anchorMs, nowMs],\n )\n const buckets = useMemo(\n () => bucketTasksByCalendarDay(tasks, (task) => task.dueAtMs, month),\n [tasks, month],\n )\n const label = useMemo(\n () =>\n month.monthDate.toLocaleDateString(undefined, {\n month: 'long',\n year: 'numeric',\n }),\n [month],\n )\n\n return (\n <Stack spacing={1.5}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n <IconButton\n size=\"small\"\n aria-label=\"Previous month\"\n onClick={() => setAnchorMs((current) => shiftCalendarMonth(current, -1))}\n >\n <MdiIcon path={mdiChevronLeft.path} size={0.9} />\n </IconButton>\n <Typography variant=\"subtitle1\" sx={{ minWidth: 160, textAlign: 'center' }}>\n {label}\n </Typography>\n <IconButton\n size=\"small\"\n aria-label=\"Next month\"\n onClick={() => setAnchorMs((current) => shiftCalendarMonth(current, 1))}\n >\n <MdiIcon path={mdiChevronRight.path} size={0.9} />\n </IconButton>\n <Button size=\"small\" onClick={() => setAnchorMs(nowMs)}>\n {'Today'}\n </Button>\n </Stack>\n\n <Box sx={{ overflowX: 'auto' }}>\n <Box sx={{ minWidth: 640 }}>\n <Box\n sx={{\n display: 'grid',\n gridTemplateColumns: `repeat(${CALENDAR_WEEK_DAYS}, 1fr)`,\n gap: 0.5,\n mb: 0.5,\n }}\n >\n {CALENDAR_DAY_LABELS.map((day) => (\n <Typography\n key={day}\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ textAlign: 'center' }}\n >\n {day}\n </Typography>\n ))}\n </Box>\n <Box\n sx={{\n display: 'grid',\n gridTemplateColumns: `repeat(${CALENDAR_WEEK_DAYS}, 1fr)`,\n gap: 0.5,\n }}\n >\n {month.weeks.flat().map((day) => {\n const held = buckets.byDay.get(day.startMs) ?? []\n const shown = held.slice(0, TASKS_PER_DAY)\n const folded = held.length - shown.length\n return (\n <Box\n key={day.startMs}\n sx={{\n minHeight: 96,\n p: 0.75,\n borderRadius: 1,\n border: 1,\n borderColor: day.isToday ? 'primary.main' : 'divider',\n bgcolor: day.inMonth ? 'background.paper' : 'action.hover',\n }}\n >\n <Typography\n variant=\"caption\"\n sx={{\n display: 'block',\n mb: 0.5,\n fontWeight: day.isToday ? 'fontWeightBold' : undefined,\n color: day.inMonth ? 'text.primary' : 'text.disabled',\n }}\n >\n {day.date}\n </Typography>\n <Stack spacing={0.25}>\n {shown.map((task) => (\n <ButtonBase\n key={task.$id}\n onClick={() => onOpen(task)}\n title={hoverText(task)}\n sx={{\n justifyContent: 'flex-start',\n width: '100%',\n px: 0.5,\n py: 0.25,\n borderRadius: 0.5,\n bgcolor: 'action.selected',\n '&:hover': { bgcolor: 'action.focus' },\n }}\n >\n <Typography\n variant=\"caption\"\n noWrap\n sx={{\n width: '100%',\n textAlign: 'left',\n textDecoration:\n task.status === 'done' ? 'line-through' : undefined,\n color:\n task.status === 'done'\n ? 'text.secondary'\n : day.endMs <= nowMs\n ? 'error.main'\n : 'text.primary',\n }}\n >\n {task.title}\n </Typography>\n </ButtonBase>\n ))}\n {folded > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`+${folded} more`}\n </Typography>\n ) : null}\n </Stack>\n </Box>\n )\n })}\n </Box>\n </Box>\n </Box>\n\n {/*\n What the grid could not place. Said plainly, because a month with\n nothing on it reads as \"nothing is due\" and the honest reading here\n is often \"the window's tasks are somewhere else\".\n */}\n {buckets.undated.length || buckets.elsewhere.length || truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {[\n buckets.elsewhere.length\n ? `${buckets.elsewhere.length} due outside this month`\n : '',\n buckets.undated.length\n ? `${buckets.undated.length} with no due date`\n : '',\n truncated ? 'and the view is showing only its first page' : '',\n ]\n .filter(Boolean)\n .join(' · ')}\n </Typography>\n ) : null}\n </Stack>\n )\n}\nTasksCalendar.displayName = 'TasksCalendar'\n\nexport default TasksCalendar\n"],"names":["crmTaskPicklistLabels","mdiChevronLeft","mdiChevronRight","MdiIcon","Box","Button","ButtonBase","IconButton","Stack","Typography","useMemo","useState","bucketTasksByCalendarDay","CALENDAR_DAY_LABELS","CALENDAR_WEEK_DAYS","shiftCalendarMonth","taskCalendarMonth","TASKS_PER_DAY","TasksCalendar","props","tasks","onOpen","truncated","picklists","hoverText","task","title","labels","type","status","nowMs","Date","now","anchorMs","setAnchorMs","month","buckets","dueAtMs","label","monthDate","toLocaleDateString","undefined","year","spacing","direction","sx","alignItems","flexWrap","rowGap","size","aria-label","onClick","current","path","variant","minWidth","textAlign","overflowX","display","gridTemplateColumns","gap","mb","map","day","color","weeks","flat","held","byDay","get","startMs","shown","slice","folded","length","minHeight","p","borderRadius","border","borderColor","isToday","bgcolor","inMonth","fontWeight","date","justifyContent","width","px","py","noWrap","textDecoration","endMs","$id","undated","elsewhere","filter","Boolean","join","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAAgCA,qBAAqB,QAAQ,eAAc;AAC3E,SAASC,cAAc,EAAEC,eAAe,QAAQ,yBAAwB;AACxE,SAASC,OAAO,QAAQ,uBAAsB;AAC9C,SAASC,GAAG,EAAEC,MAAM,EAAEC,UAAU,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACtF,SAASC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AAEzC,SACEC,wBAAwB,EACxBC,mBAAmB,EACnBC,kBAAkB,EAClBC,kBAAkB,EAClBC,iBAAiB,QACZ,4BAAwB;AAE/B,wEAAwE,GACxE,MAAMC,gBAAgB;AAatB;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GACD,OAAO,SAASC,cAAcC,KAAyB;IACrD,MAAM,EAAEC,KAAK,EAAEC,MAAM,EAAEC,SAAS,EAAEC,SAAS,EAAE,GAAGJ;IAChD,4EAA4E,GAC5E,MAAMK,YAAY,CAACC;QACjB,IAAI,CAACF,WAAW,OAAOE,KAAKC,KAAK;QACjC,MAAMC,SAAS3B,sBAAsByB,MAAMF;QAC3C,OAAO,GAAGE,KAAKC,KAAK,CAAC,GAAG,EAAEC,OAAOC,IAAI,CAAC,GAAG,EAAED,OAAOE,MAAM,EAAE;IAC5D;IACA;;;;;GAKC,GACD,MAAM,CAACC,MAAM,GAAGnB,SAAS,IAAMoB,KAAKC,GAAG;IACvC,MAAM,CAACC,UAAUC,YAAY,GAAGvB,SAAS,IAAMmB;IAE/C,MAAMK,QAAQzB,QACZ,IAAMM,kBAAkBiB,UAAUH,QAClC;QAACG;QAAUH;KAAM;IAEnB,MAAMM,UAAU1B,QACd,IAAME,yBAAyBQ,OAAO,CAACK,OAASA,KAAKY,OAAO,EAAEF,QAC9D;QAACf;QAAOe;KAAM;IAEhB,MAAMG,QAAQ5B,QACZ,IACEyB,MAAMI,SAAS,CAACC,kBAAkB,CAACC,WAAW;YAC5CN,OAAO;YACPO,MAAM;QACR,IACF;QAACP;KAAM;IAGT,qBACE,MAAC3B;QAAMmC,SAAS;;0BACd,MAACnC;gBACCoC,WAAU;gBACVD,SAAS;gBACTE,IAAI;oBAAEC,YAAY;oBAAUC,UAAU;oBAAQC,QAAQ;gBAAE;;kCAExD,KAACzC;wBACC0C,MAAK;wBACLC,cAAW;wBACXC,SAAS,IAAMjB,YAAY,CAACkB,UAAYrC,mBAAmBqC,SAAS,CAAC;kCAErE,cAAA,KAACjD;4BAAQkD,MAAMpD,eAAeoD,IAAI;4BAAEJ,MAAM;;;kCAE5C,KAACxC;wBAAW6C,SAAQ;wBAAYT,IAAI;4BAAEU,UAAU;4BAAKC,WAAW;wBAAS;kCACtElB;;kCAEH,KAAC/B;wBACC0C,MAAK;wBACLC,cAAW;wBACXC,SAAS,IAAMjB,YAAY,CAACkB,UAAYrC,mBAAmBqC,SAAS;kCAEpE,cAAA,KAACjD;4BAAQkD,MAAMnD,gBAAgBmD,IAAI;4BAAEJ,MAAM;;;kCAE7C,KAAC5C;wBAAO4C,MAAK;wBAAQE,SAAS,IAAMjB,YAAYJ;kCAC7C;;;;0BAIL,KAAC1B;gBAAIyC,IAAI;oBAAEY,WAAW;gBAAO;0BAC3B,cAAA,MAACrD;oBAAIyC,IAAI;wBAAEU,UAAU;oBAAI;;sCACvB,KAACnD;4BACCyC,IAAI;gCACFa,SAAS;gCACTC,qBAAqB,CAAC,OAAO,EAAE7C,mBAAmB,MAAM,CAAC;gCACzD8C,KAAK;gCACLC,IAAI;4BACN;sCAEChD,oBAAoBiD,GAAG,CAAC,CAACC,oBACxB,KAACtD;oCAEC6C,SAAQ;oCACRU,OAAM;oCACNnB,IAAI;wCAAEW,WAAW;oCAAS;8CAEzBO;mCALIA;;sCASX,KAAC3D;4BACCyC,IAAI;gCACFa,SAAS;gCACTC,qBAAqB,CAAC,OAAO,EAAE7C,mBAAmB,MAAM,CAAC;gCACzD8C,KAAK;4BACP;sCAECzB,MAAM8B,KAAK,CAACC,IAAI,GAAGJ,GAAG,CAAC,CAACC;oCACV3B;gCAAb,MAAM+B,QAAO/B,qBAAAA,QAAQgC,KAAK,CAACC,GAAG,CAACN,IAAIO,OAAO,aAA7BlC,qBAAkC,EAAE;gCACjD,MAAMmC,QAAQJ,KAAKK,KAAK,CAAC,GAAGvD;gCAC5B,MAAMwD,SAASN,KAAKO,MAAM,GAAGH,MAAMG,MAAM;gCACzC,qBACE,MAACtE;oCAECyC,IAAI;wCACF8B,WAAW;wCACXC,GAAG;wCACHC,cAAc;wCACdC,QAAQ;wCACRC,aAAahB,IAAIiB,OAAO,GAAG,iBAAiB;wCAC5CC,SAASlB,IAAImB,OAAO,GAAG,qBAAqB;oCAC9C;;sDAEA,KAACzE;4CACC6C,SAAQ;4CACRT,IAAI;gDACFa,SAAS;gDACTG,IAAI;gDACJsB,YAAYpB,IAAIiB,OAAO,GAAG,mBAAmBvC;gDAC7CuB,OAAOD,IAAImB,OAAO,GAAG,iBAAiB;4CACxC;sDAECnB,IAAIqB,IAAI;;sDAEX,MAAC5E;4CAAMmC,SAAS;;gDACb4B,MAAMT,GAAG,CAAC,CAACrC,qBACV,KAACnB;wDAEC6C,SAAS,IAAM9B,OAAOI;wDACtBC,OAAOF,UAAUC;wDACjBoB,IAAI;4DACFwC,gBAAgB;4DAChBC,OAAO;4DACPC,IAAI;4DACJC,IAAI;4DACJX,cAAc;4DACdI,SAAS;4DACT,WAAW;gEAAEA,SAAS;4DAAe;wDACvC;kEAEA,cAAA,KAACxE;4DACC6C,SAAQ;4DACRmC,MAAM;4DACN5C,IAAI;gEACFyC,OAAO;gEACP9B,WAAW;gEACXkC,gBACEjE,KAAKI,MAAM,KAAK,SAAS,iBAAiBY;gEAC5CuB,OACEvC,KAAKI,MAAM,KAAK,SACZ,mBACAkC,IAAI4B,KAAK,IAAI7D,QACX,eACA;4DACV;sEAECL,KAAKC,KAAK;;uDA7BRD,KAAKmE,GAAG;gDAiChBnB,SAAS,kBACR,KAAChE;oDAAW6C,SAAQ;oDAAUU,OAAM;8DACjC,CAAC,CAAC,EAAES,OAAO,KAAK,CAAC;qDAElB;;;;mCA7DDV,IAAIO,OAAO;4BAiEtB;;;;;YAULlC,QAAQyD,OAAO,CAACnB,MAAM,IAAItC,QAAQ0D,SAAS,CAACpB,MAAM,IAAIpD,0BACrD,KAACb;gBAAW6C,SAAQ;gBAAUU,OAAM;0BACjC;oBACC5B,QAAQ0D,SAAS,CAACpB,MAAM,GACpB,GAAGtC,QAAQ0D,SAAS,CAACpB,MAAM,CAAC,uBAAuB,CAAC,GACpD;oBACJtC,QAAQyD,OAAO,CAACnB,MAAM,GAClB,GAAGtC,QAAQyD,OAAO,CAACnB,MAAM,CAAC,iBAAiB,CAAC,GAC5C;oBACJpD,YAAY,gDAAgD;iBAC7D,CACEyE,MAAM,CAACC,SACPC,IAAI,CAAC;iBAER;;;AAGV;AACA/E,cAAcgF,WAAW,GAAG;AAE5B,eAAehF,cAAa"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/tasks-calendar.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { type CrmTaskPicklists, crmTaskPicklistLabels } from '@aglyn/aglyn'\nimport { mdiChevronLeft, mdiChevronRight } from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { Box, Button, ButtonBase, IconButton, Stack, Typography } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport type { CrmTaskRow } from '../hooks/use-crm-tasks'\nimport {\n bucketTasksByCalendarDay,\n CALENDAR_DAY_LABELS,\n CALENDAR_WEEK_DAYS,\n shiftCalendarMonth,\n taskCalendarMonth,\n} from '../model/task-calendar'\n\n/** How many tasks a square lists before folding the rest into a count. */\nconst TASKS_PER_DAY = 3\n\nexport interface TasksCalendarProps {\n /** The window the list is holding — the calendar reads no more than it. */\n tasks: readonly CrmTaskRow[]\n /** Open one task's drawer, the same drawer a row opens. */\n onOpen: (task: CrmTaskRow) => void\n /** The list's window filled, so neither surface is showing everything. */\n truncated?: boolean\n /** The org's task lists, which name each entry's type and status on hover (AGL-3517). */\n picklists?: CrmTaskPicklists\n}\n\n/**\n * A MONTH OF TASKS, BY DUE DATE (AGL-2662).\n *\n * The Tasks list answers \"what is on my plate\"; this answers \"what does\n * next week look like\", which a list ordered by due date can only imply.\n *\n * ## Drawn over the list's own window, and never over a second read\n *\n * The rows are the ones the section already holds. A calendar that fetched\n * its own month would be a second listener over the same collection, would\n * disagree with the list whenever the two windows differed, and would put\n * a read behind every page of the arrows. What it costs instead is honesty:\n * the grid can only place what the window carries, so the tasks due in\n * other months are counted under it rather than implied to be absent.\n *\n * ## Hand-built rather than a calendar dependency\n *\n * Seven columns of MUI boxes and the local-day arithmetic in\n * `task-calendar.ts`. The one calendar package already installed\n * (`@mui/x-date-pickers`) draws a date PICKER — a grid whose squares are\n * choices, not content — so it answers a different question, and a\n * scheduling library for one surface is a dependency the whole console\n * would carry.\n */\nexport function TasksCalendar(props: TasksCalendarProps) {\n const { tasks, onOpen, truncated, picklists } = props\n /** \"Call Ada · Call · In Progress\" — the entry's type and status on hover. */\n const hoverText = (task: CrmTaskRow) => {\n if (!picklists) return task.title\n const labels = crmTaskPicklistLabels(task, picklists)\n return `${task.title} · ${labels.type} · ${labels.status}`\n }\n /*\n * The clock is read ONCE, at mount. A component that read `Date.now()`\n * per render would move the today square under a reader who left the tab\n * open across midnight, without any state having changed — and the grid\n * is a pure function of this, so what is drawn can be pinned.\n */\n const [nowMs] = useState(() => Date.now())\n const [anchorMs, setAnchorMs] = useState(() => nowMs)\n\n const month = useMemo(\n () => taskCalendarMonth(anchorMs, nowMs),\n [anchorMs, nowMs],\n )\n const buckets = useMemo(\n () => bucketTasksByCalendarDay(tasks, (task) => task.dueAtMs, month),\n [tasks, month],\n )\n const label = useMemo(\n () =>\n month.monthDate.toLocaleDateString(undefined, {\n month: 'long',\n year: 'numeric',\n }),\n [month],\n )\n\n return (\n <Stack spacing={1.5}>\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n <IconButton\n size=\"small\"\n aria-label=\"Previous month\"\n onClick={() => setAnchorMs((current) => shiftCalendarMonth(current, -1))}\n >\n <MdiIcon path={mdiChevronLeft.path} size={0.9} />\n </IconButton>\n <Typography variant=\"subtitle1\" sx={{ minWidth: 160, textAlign: 'center' }}>\n {label}\n </Typography>\n <IconButton\n size=\"small\"\n aria-label=\"Next month\"\n onClick={() => setAnchorMs((current) => shiftCalendarMonth(current, 1))}\n >\n <MdiIcon path={mdiChevronRight.path} size={0.9} />\n </IconButton>\n <Button size=\"small\" onClick={() => setAnchorMs(nowMs)}>\n {'Today'}\n </Button>\n </Stack>\n\n <Box sx={{ overflowX: 'auto' }}>\n <Box sx={{ minWidth: 640 }}>\n <Box\n sx={{\n display: 'grid',\n gridTemplateColumns: `repeat(${CALENDAR_WEEK_DAYS}, 1fr)`,\n gap: 0.5,\n mb: 0.5,\n }}\n >\n {CALENDAR_DAY_LABELS.map((day) => (\n <Typography\n key={day}\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ textAlign: 'center' }}\n >\n {day}\n </Typography>\n ))}\n </Box>\n <Box\n sx={{\n display: 'grid',\n gridTemplateColumns: `repeat(${CALENDAR_WEEK_DAYS}, 1fr)`,\n gap: 0.5,\n }}\n >\n {month.weeks.flat().map((day) => {\n const held = buckets.byDay.get(day.startMs) ?? []\n const shown = held.slice(0, TASKS_PER_DAY)\n const folded = held.length - shown.length\n return (\n <Box\n key={day.startMs}\n sx={{\n minHeight: 96,\n p: 0.75,\n borderRadius: 1,\n border: 1,\n borderColor: day.isToday ? 'primary.main' : 'divider',\n bgcolor: day.inMonth ? 'background.paper' : 'action.hover',\n }}\n >\n <Typography\n variant=\"caption\"\n sx={{\n display: 'block',\n mb: 0.5,\n fontWeight: day.isToday ? 'fontWeightBold' : undefined,\n color: day.inMonth ? 'text.primary' : 'text.disabled',\n }}\n >\n {day.date}\n </Typography>\n <Stack spacing={0.25}>\n {shown.map((task) => (\n <ButtonBase\n key={task.$id}\n onClick={() => onOpen(task)}\n title={hoverText(task)}\n sx={{\n justifyContent: 'flex-start',\n width: '100%',\n px: 0.5,\n py: 0.25,\n borderRadius: 0.5,\n bgcolor: 'action.selected',\n '&:hover': { bgcolor: 'action.focus' },\n }}\n >\n <Typography\n variant=\"caption\"\n noWrap\n sx={{\n width: '100%',\n textAlign: 'left',\n textDecoration:\n task.status === 'done' ? 'line-through' : undefined,\n color:\n task.status === 'done'\n ? 'text.secondary'\n : day.endMs <= nowMs\n ? 'error.main'\n : 'text.primary',\n }}\n >\n {task.title}\n </Typography>\n </ButtonBase>\n ))}\n {folded > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`+${folded} more`}\n </Typography>\n ) : null}\n </Stack>\n </Box>\n )\n })}\n </Box>\n </Box>\n </Box>\n\n {/*\n What the grid could not place. Said plainly, because a month with\n nothing on it reads as \"nothing is due\" and the honest reading here\n is often \"the window's tasks are somewhere else\".\n */}\n {buckets.undated.length || buckets.elsewhere.length || truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {[\n buckets.elsewhere.length\n ? `${buckets.elsewhere.length} due outside this month`\n : '',\n buckets.undated.length\n ? `${buckets.undated.length} with no due date`\n : '',\n truncated ? 'and the view is showing only its first page' : '',\n ]\n .filter(Boolean)\n .join(' · ')}\n </Typography>\n ) : null}\n </Stack>\n )\n}\nTasksCalendar.displayName = 'TasksCalendar'\n\nexport default TasksCalendar\n"],"names":["crmTaskPicklistLabels","mdiChevronLeft","mdiChevronRight","MdiIcon","Box","Button","ButtonBase","IconButton","Stack","Typography","useMemo","useState","bucketTasksByCalendarDay","CALENDAR_DAY_LABELS","CALENDAR_WEEK_DAYS","shiftCalendarMonth","taskCalendarMonth","TASKS_PER_DAY","TasksCalendar","props","tasks","onOpen","truncated","picklists","hoverText","task","title","labels","type","status","nowMs","Date","now","anchorMs","setAnchorMs","month","buckets","dueAtMs","label","monthDate","toLocaleDateString","undefined","year","spacing","useFlexGap","direction","sx","alignItems","flexWrap","rowGap","size","aria-label","onClick","current","path","variant","minWidth","textAlign","overflowX","display","gridTemplateColumns","gap","mb","map","day","color","weeks","flat","held","byDay","get","startMs","shown","slice","folded","length","minHeight","p","borderRadius","border","borderColor","isToday","bgcolor","inMonth","fontWeight","date","justifyContent","width","px","py","noWrap","textDecoration","endMs","$id","undated","elsewhere","filter","Boolean","join","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAAgCA,qBAAqB,QAAQ,eAAc;AAC3E,SAASC,cAAc,EAAEC,eAAe,QAAQ,yBAAwB;AACxE,SAASC,OAAO,QAAQ,uBAAsB;AAC9C,SAASC,GAAG,EAAEC,MAAM,EAAEC,UAAU,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACtF,SAASC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AAEzC,SACEC,wBAAwB,EACxBC,mBAAmB,EACnBC,kBAAkB,EAClBC,kBAAkB,EAClBC,iBAAiB,QACZ,4BAAwB;AAE/B,wEAAwE,GACxE,MAAMC,gBAAgB;AAatB;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GACD,OAAO,SAASC,cAAcC,KAAyB;IACrD,MAAM,EAAEC,KAAK,EAAEC,MAAM,EAAEC,SAAS,EAAEC,SAAS,EAAE,GAAGJ;IAChD,4EAA4E,GAC5E,MAAMK,YAAY,CAACC;QACjB,IAAI,CAACF,WAAW,OAAOE,KAAKC,KAAK;QACjC,MAAMC,SAAS3B,sBAAsByB,MAAMF;QAC3C,OAAO,GAAGE,KAAKC,KAAK,CAAC,GAAG,EAAEC,OAAOC,IAAI,CAAC,GAAG,EAAED,OAAOE,MAAM,EAAE;IAC5D;IACA;;;;;GAKC,GACD,MAAM,CAACC,MAAM,GAAGnB,SAAS,IAAMoB,KAAKC,GAAG;IACvC,MAAM,CAACC,UAAUC,YAAY,GAAGvB,SAAS,IAAMmB;IAE/C,MAAMK,QAAQzB,QACZ,IAAMM,kBAAkBiB,UAAUH,QAClC;QAACG;QAAUH;KAAM;IAEnB,MAAMM,UAAU1B,QACd,IAAME,yBAAyBQ,OAAO,CAACK,OAASA,KAAKY,OAAO,EAAEF,QAC9D;QAACf;QAAOe;KAAM;IAEhB,MAAMG,QAAQ5B,QACZ,IACEyB,MAAMI,SAAS,CAACC,kBAAkB,CAACC,WAAW;YAC5CN,OAAO;YACPO,MAAM;QACR,IACF;QAACP;KAAM;IAGT,qBACE,MAAC3B;QAAMmC,SAAS;;0BACd,MAACnC;gBACCoC,UAAU;gBACVC,WAAU;gBACVF,SAAS;gBACTG,IAAI;oBAAEC,YAAY;oBAAUC,UAAU;oBAAQC,QAAQ;gBAAE;;kCAExD,KAAC1C;wBACC2C,MAAK;wBACLC,cAAW;wBACXC,SAAS,IAAMlB,YAAY,CAACmB,UAAYtC,mBAAmBsC,SAAS,CAAC;kCAErE,cAAA,KAAClD;4BAAQmD,MAAMrD,eAAeqD,IAAI;4BAAEJ,MAAM;;;kCAE5C,KAACzC;wBAAW8C,SAAQ;wBAAYT,IAAI;4BAAEU,UAAU;4BAAKC,WAAW;wBAAS;kCACtEnB;;kCAEH,KAAC/B;wBACC2C,MAAK;wBACLC,cAAW;wBACXC,SAAS,IAAMlB,YAAY,CAACmB,UAAYtC,mBAAmBsC,SAAS;kCAEpE,cAAA,KAAClD;4BAAQmD,MAAMpD,gBAAgBoD,IAAI;4BAAEJ,MAAM;;;kCAE7C,KAAC7C;wBAAO6C,MAAK;wBAAQE,SAAS,IAAMlB,YAAYJ;kCAC7C;;;;0BAIL,KAAC1B;gBAAI0C,IAAI;oBAAEY,WAAW;gBAAO;0BAC3B,cAAA,MAACtD;oBAAI0C,IAAI;wBAAEU,UAAU;oBAAI;;sCACvB,KAACpD;4BACC0C,IAAI;gCACFa,SAAS;gCACTC,qBAAqB,CAAC,OAAO,EAAE9C,mBAAmB,MAAM,CAAC;gCACzD+C,KAAK;gCACLC,IAAI;4BACN;sCAECjD,oBAAoBkD,GAAG,CAAC,CAACC,oBACxB,KAACvD;oCAEC8C,SAAQ;oCACRU,OAAM;oCACNnB,IAAI;wCAAEW,WAAW;oCAAS;8CAEzBO;mCALIA;;sCASX,KAAC5D;4BACC0C,IAAI;gCACFa,SAAS;gCACTC,qBAAqB,CAAC,OAAO,EAAE9C,mBAAmB,MAAM,CAAC;gCACzD+C,KAAK;4BACP;sCAEC1B,MAAM+B,KAAK,CAACC,IAAI,GAAGJ,GAAG,CAAC,CAACC;oCACV5B;gCAAb,MAAMgC,QAAOhC,qBAAAA,QAAQiC,KAAK,CAACC,GAAG,CAACN,IAAIO,OAAO,aAA7BnC,qBAAkC,EAAE;gCACjD,MAAMoC,QAAQJ,KAAKK,KAAK,CAAC,GAAGxD;gCAC5B,MAAMyD,SAASN,KAAKO,MAAM,GAAGH,MAAMG,MAAM;gCACzC,qBACE,MAACvE;oCAEC0C,IAAI;wCACF8B,WAAW;wCACXC,GAAG;wCACHC,cAAc;wCACdC,QAAQ;wCACRC,aAAahB,IAAIiB,OAAO,GAAG,iBAAiB;wCAC5CC,SAASlB,IAAImB,OAAO,GAAG,qBAAqB;oCAC9C;;sDAEA,KAAC1E;4CACC8C,SAAQ;4CACRT,IAAI;gDACFa,SAAS;gDACTG,IAAI;gDACJsB,YAAYpB,IAAIiB,OAAO,GAAG,mBAAmBxC;gDAC7CwB,OAAOD,IAAImB,OAAO,GAAG,iBAAiB;4CACxC;sDAECnB,IAAIqB,IAAI;;sDAEX,MAAC7E;4CAAMmC,SAAS;;gDACb6B,MAAMT,GAAG,CAAC,CAACtC,qBACV,KAACnB;wDAEC8C,SAAS,IAAM/B,OAAOI;wDACtBC,OAAOF,UAAUC;wDACjBqB,IAAI;4DACFwC,gBAAgB;4DAChBC,OAAO;4DACPC,IAAI;4DACJC,IAAI;4DACJX,cAAc;4DACdI,SAAS;4DACT,WAAW;gEAAEA,SAAS;4DAAe;wDACvC;kEAEA,cAAA,KAACzE;4DACC8C,SAAQ;4DACRmC,MAAM;4DACN5C,IAAI;gEACFyC,OAAO;gEACP9B,WAAW;gEACXkC,gBACElE,KAAKI,MAAM,KAAK,SAAS,iBAAiBY;gEAC5CwB,OACExC,KAAKI,MAAM,KAAK,SACZ,mBACAmC,IAAI4B,KAAK,IAAI9D,QACX,eACA;4DACV;sEAECL,KAAKC,KAAK;;uDA7BRD,KAAKoE,GAAG;gDAiChBnB,SAAS,kBACR,KAACjE;oDAAW8C,SAAQ;oDAAUU,OAAM;8DACjC,CAAC,CAAC,EAAES,OAAO,KAAK,CAAC;qDAElB;;;;mCA7DDV,IAAIO,OAAO;4BAiEtB;;;;;YAULnC,QAAQ0D,OAAO,CAACnB,MAAM,IAAIvC,QAAQ2D,SAAS,CAACpB,MAAM,IAAIrD,0BACrD,KAACb;gBAAW8C,SAAQ;gBAAUU,OAAM;0BACjC;oBACC7B,QAAQ2D,SAAS,CAACpB,MAAM,GACpB,GAAGvC,QAAQ2D,SAAS,CAACpB,MAAM,CAAC,uBAAuB,CAAC,GACpD;oBACJvC,QAAQ0D,OAAO,CAACnB,MAAM,GAClB,GAAGvC,QAAQ0D,OAAO,CAACnB,MAAM,CAAC,iBAAiB,CAAC,GAC5C;oBACJrD,YAAY,gDAAgD;iBAC7D,CACE0E,MAAM,CAACC,SACPC,IAAI,CAAC;iBAER;;;AAGV;AACAhF,cAAciF,WAAW,GAAG;AAE5B,eAAejF,cAAa"}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
18
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
-
import { CRM_COLLECTIONS, crmPicklistShownLabel, crmTaskPicklistLabels, crmTaskStatusWrite, pluginDocsHelp } from "@aglyn/aglyn";
|
|
19
|
+
import { CRM_COLLECTIONS, crmPicklistRank, crmPicklistShownLabel, crmTaskPicklistLabels, crmTaskStatusWrite, pluginDocsHelp } from "@aglyn/aglyn";
|
|
20
20
|
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
21
21
|
import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
22
22
|
import { useCrmSavedView } from "../hooks/use-crm-saved-view.js";
|
|
@@ -37,7 +37,8 @@ import { crmRoutes } from "../model/crm-routes.js";
|
|
|
37
37
|
import { refreshCrmNextActivity } from "../model/next-activity-api.js";
|
|
38
38
|
import { completeCrmTask } from "../model/task-api.js";
|
|
39
39
|
import { crmTaskCallScope } from "../model/task-routes.js";
|
|
40
|
-
import { CRM_TASK_KINDS, CRM_TASK_PRIORITIES, CRM_TASK_VIEWS, TASK_LIST_QUERY_FIELDS } from "../model/task-views.js";
|
|
40
|
+
import { CRM_TASK_KINDS, CRM_TASK_PRIORITIES, CRM_TASK_VIEWS, crmTaskViewPlan, crmTaskViewSort, TASK_LIST_QUERY_FIELDS, TASK_LIST_SORTS, taskRecordLink } from "../model/task-views.js";
|
|
41
|
+
import { crmViewQuerySort, useCrmColumnSort } from "../hooks/use-crm-column-sort.js";
|
|
41
42
|
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
42
43
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
43
44
|
import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
@@ -83,6 +84,10 @@ const TASK_FILTER_HEADERS = {
|
|
|
83
84
|
op: 'equals',
|
|
84
85
|
value: 'mine'
|
|
85
86
|
};
|
|
87
|
+
/** The row controls no header sorts (AGL-3680): the done box, an action like Complete. */ const TASK_ROW_CONTROLS = [
|
|
88
|
+
'status'
|
|
89
|
+
];
|
|
90
|
+
/** A label's place in its org list, for a page sort; `null` for none (sorted last). */ const picklistRank = (picklist, label)=>label ? crmPicklistRank(picklist, label) : null;
|
|
86
91
|
const EMPTY_LABEL = {
|
|
87
92
|
mine: 'Nothing is assigned to you',
|
|
88
93
|
overdue: 'Nothing is overdue',
|
|
@@ -183,6 +188,19 @@ const EMPTY_LABEL = {
|
|
|
183
188
|
const queryClauses = useMemo(()=>clauses.filter((clause)=>clause.field !== 'view'), [
|
|
184
189
|
clauses
|
|
185
190
|
]);
|
|
191
|
+
/*
|
|
192
|
+
* The header order the view asks (AGL-3680), read before the query it
|
|
193
|
+
* orders; with none, the task view's own Due order.
|
|
194
|
+
*/ const viewDirection = crmTaskViewPlan(view, {
|
|
195
|
+
nowMs,
|
|
196
|
+
uid: user == null ? void 0 : user.uid
|
|
197
|
+
}).direction;
|
|
198
|
+
const taskSort = useMemo(()=>crmViewQuerySort(TASK_LIST_SORTS, views.state.sort, crmTaskViewSort({
|
|
199
|
+
direction: viewDirection
|
|
200
|
+
})), [
|
|
201
|
+
views.state.sort,
|
|
202
|
+
viewDirection
|
|
203
|
+
]);
|
|
186
204
|
const list = useCrmTaskList({
|
|
187
205
|
hostId,
|
|
188
206
|
org: orgRecord,
|
|
@@ -190,7 +208,8 @@ const EMPTY_LABEL = {
|
|
|
190
208
|
uid: user == null ? void 0 : user.uid,
|
|
191
209
|
nowMs,
|
|
192
210
|
clauses: queryClauses,
|
|
193
|
-
search: gridFilter.searchWords
|
|
211
|
+
search: gridFilter.searchWords,
|
|
212
|
+
sort: taskSort.sort
|
|
194
213
|
});
|
|
195
214
|
const { tasks, status, fromCache, scope, orgId, readTokens } = list;
|
|
196
215
|
const directory = useOrgMemberDirectory(orgId);
|
|
@@ -513,6 +532,37 @@ const EMPTY_LABEL = {
|
|
|
513
532
|
filterOptions
|
|
514
533
|
]);
|
|
515
534
|
const grid = useCrmViewGrid(views, filterColumns, TASK_HIDDEN_COLUMNS);
|
|
535
|
+
/*
|
|
536
|
+
* EVERY HEADER SORTS (AGL-3680): Due, Task and Priority on the query
|
|
537
|
+
* (`TASK_LIST_SORTS`), the rest over the page, saying so — Type and Status
|
|
538
|
+
* in the order the org keeps their values, For by the record's name. The
|
|
539
|
+
* done box is a row control, like an actions column, and sorts nothing.
|
|
540
|
+
*/ const pageSorts = useMemo(()=>({
|
|
541
|
+
kind: (row)=>picklistRank(picklists.type, crmTaskPicklistLabels(row, picklists).type),
|
|
542
|
+
statusLabel: (row)=>picklistRank(picklists.status, crmTaskPicklistLabels(row, picklists).status),
|
|
543
|
+
record: (row)=>{
|
|
544
|
+
var _nameOf;
|
|
545
|
+
const link = taskRecordLink(row, routes);
|
|
546
|
+
return link ? (_nameOf = nameOf(link.kind, link.id)) != null ? _nameOf : null : null;
|
|
547
|
+
}
|
|
548
|
+
}), [
|
|
549
|
+
picklists,
|
|
550
|
+
routes,
|
|
551
|
+
nameOf
|
|
552
|
+
]);
|
|
553
|
+
const columnSort = useCrmColumnSort({
|
|
554
|
+
views,
|
|
555
|
+
sorts: TASK_LIST_SORTS,
|
|
556
|
+
defaultSort: crmTaskViewSort({
|
|
557
|
+
direction: viewDirection
|
|
558
|
+
}),
|
|
559
|
+
asked: taskSort,
|
|
560
|
+
orderBy: list.plan.orderBy,
|
|
561
|
+
rows: tasks,
|
|
562
|
+
columns: grid.columns,
|
|
563
|
+
pageSorts,
|
|
564
|
+
unsortable: TASK_ROW_CONTROLS
|
|
565
|
+
});
|
|
516
566
|
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
517
567
|
children: [
|
|
518
568
|
/*#__PURE__*/ _jsx(CardDisplay, {
|
|
@@ -592,7 +642,10 @@ const EMPTY_LABEL = {
|
|
|
592
642
|
}),
|
|
593
643
|
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
594
644
|
refused: refused,
|
|
595
|
-
notices:
|
|
645
|
+
notices: [
|
|
646
|
+
...list.plan.notices,
|
|
647
|
+
...columnSort.notices
|
|
648
|
+
]
|
|
596
649
|
}),
|
|
597
650
|
status === 'error' ? /*#__PURE__*/ _jsx(Typography, {
|
|
598
651
|
variant: "body2",
|
|
@@ -620,7 +673,8 @@ const EMPTY_LABEL = {
|
|
|
620
673
|
/*#__PURE__*/ _jsx(CrmColumnOrderProvider, {
|
|
621
674
|
value: grid.columnOrder,
|
|
622
675
|
children: /*#__PURE__*/ _jsx(ListTable, {
|
|
623
|
-
rows:
|
|
676
|
+
rows: columnSort.rows,
|
|
677
|
+
columnSort: columnSort,
|
|
624
678
|
columns: grid.columns,
|
|
625
679
|
slots: CRM_LIST_SLOTS,
|
|
626
680
|
selectable: {
|
|
@@ -657,10 +711,8 @@ const EMPTY_LABEL = {
|
|
|
657
711
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
658
712
|
quickFilter: true,
|
|
659
713
|
// The view orders the query (due soonest, or Done most
|
|
660
|
-
// recently due)
|
|
661
|
-
// sort
|
|
662
|
-
sortingMode: "server",
|
|
663
|
-
disableColumnSorting: true,
|
|
714
|
+
// recently due) until a header asks otherwise: the headers
|
|
715
|
+
// sort through `columnSort` (AGL-3680).
|
|
664
716
|
// Columns are the view's, controlled (AGL-2617).
|
|
665
717
|
columnVisibilityModel: grid.columnVisibilityModel,
|
|
666
718
|
onColumnVisibilityModelChange: grid.onColumnVisibilityModelChange,
|