@aglyn/plugins-crm 1.0.0-beta.199 → 1.0.0-beta.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/lib/components/activity-list.d.ts +36 -5
- package/src/lib/components/activity-list.js +159 -137
- package/src/lib/components/activity-list.js.map +1 -1
- package/src/lib/components/activity-timeline.d.ts +84 -0
- package/src/lib/components/activity-timeline.js +224 -0
- package/src/lib/components/activity-timeline.js.map +1 -0
- package/src/lib/components/companies-section.d.ts +6 -6
- package/src/lib/components/companies-section.js +54 -84
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +17 -4
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-picker.js +6 -2
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/company-properties-card.js +53 -36
- package/src/lib/components/company-properties-card.js.map +1 -1
- package/src/lib/components/contact-associations-card.d.ts +3 -2
- package/src/lib/components/contact-associations-card.js +12 -12
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-detail-page.d.ts +4 -3
- package/src/lib/components/contact-detail-page.js +18 -12
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -5
- package/src/lib/components/contact-list-columns.js +6 -8
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contact-properties-card.d.ts +21 -4
- package/src/lib/components/contact-properties-card.js +129 -29
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/contact-timeline-card.d.ts +12 -6
- package/src/lib/components/contact-timeline-card.js +132 -174
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.js +10 -1
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +282 -331
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-email-state-chip.js +4 -1
- package/src/lib/components/crm-email-state-chip.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.d.ts +4 -2
- package/src/lib/components/crm-next-activity-column.js +6 -3
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +8 -4
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deal-products-card.js +15 -13
- package/src/lib/components/deal-products-card.js.map +1 -1
- package/src/lib/components/deal-properties-card.js +7 -0
- package/src/lib/components/deal-properties-card.js.map +1 -1
- package/src/lib/components/deal-stage-card.js +31 -29
- package/src/lib/components/deal-stage-card.js.map +1 -1
- package/src/lib/components/deals-section.d.ts +2 -2
- package/src/lib/components/deals-section.js +67 -97
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.d.ts +4 -0
- package/src/lib/components/fields-section.js +368 -154
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/form-contact-fields-card.js +24 -20
- package/src/lib/components/form-contact-fields-card.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.d.ts +1 -1
- package/src/lib/components/lead-campaigns-card.js +43 -38
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.d.ts +14 -6
- package/src/lib/components/lead-properties-card.js +323 -311
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +13 -2
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +16 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.d.ts +2 -1
- package/src/lib/components/leads-section.js +110 -148
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/linked-deals-card.js +12 -10
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/recent-activity-feed.d.ts +12 -8
- package/src/lib/components/recent-activity-feed.js +54 -29
- package/src/lib/components/recent-activity-feed.js.map +1 -1
- package/src/lib/components/record-activity-card.d.ts +4 -2
- package/src/lib/components/record-activity-card.js +29 -11
- package/src/lib/components/record-activity-card.js.map +1 -1
- package/src/lib/components/reports/activity-card.js +18 -12
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +47 -37
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +22 -16
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/report-export.d.ts +4 -11
- package/src/lib/components/reports/report-export.js +16 -33
- package/src/lib/components/reports/report-export.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +18 -12
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/reports/tasks-card.js +35 -25
- package/src/lib/components/reports/tasks-card.js.map +1 -1
- package/src/lib/components/tasks-section.js +151 -144
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +13 -0
- package/src/lib/constants/company-filters.js +47 -17
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +61 -10
- package/src/lib/constants/contact-filters.js +373 -141
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +44 -0
- package/src/lib/constants/deal-filters.js +78 -0
- package/src/lib/constants/deal-filters.js.map +1 -0
- package/src/lib/constants/field-list-query.d.ts +23 -0
- package/src/lib/constants/field-list-query.js +80 -0
- package/src/lib/constants/field-list-query.js.map +1 -0
- package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
- package/src/lib/hooks/use-crm-list-query.js +182 -0
- package/src/lib/hooks/use-crm-list-query.js.map +1 -0
- package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
- package/src/lib/hooks/use-crm-tasks.js +44 -42
- package/src/lib/hooks/use-crm-tasks.js.map +1 -1
- package/src/lib/hooks/use-deals.d.ts +2 -34
- package/src/lib/hooks/use-deals.js +3 -62
- package/src/lib/hooks/use-deals.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +91 -0
- package/src/lib/model/crm-list-query.js +182 -0
- package/src/lib/model/crm-list-query.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +50 -44
- package/src/lib/model/lead-filters.js +194 -88
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/list-fields-api.d.ts +26 -0
- package/src/lib/model/list-fields-api.js +47 -0
- package/src/lib/model/list-fields-api.js.map +1 -0
- package/src/lib/model/list-fields.d.ts +47 -0
- package/src/lib/model/list-fields.js +39 -0
- package/src/lib/model/list-fields.js.map +1 -0
- package/src/lib/model/task-views.d.ts +15 -10
- package/src/lib/model/task-views.js +68 -9
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/capture-contact.js +7 -5
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/companies-import.js +3 -3
- package/src/lib/server/companies-import.js.map +1 -1
- package/src/lib/server/company-delete.js +3 -1
- package/src/lib/server/company-delete.js.map +1 -1
- package/src/lib/server/consent-group-participant.js +4 -0
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-update.js +14 -8
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.js +4 -2
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/deals-import.js +4 -2
- package/src/lib/server/deals-import.js.map +1 -1
- package/src/lib/server/erase-person.js +2 -1
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/lead-create.js +4 -1
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-source-values.js +9 -5
- package/src/lib/server/lead-source-values.js.map +1 -1
- package/src/lib/server/leads-import.js +4 -1
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server/list-fields-routes.d.ts +29 -0
- package/src/lib/server/list-fields-routes.js +85 -0
- package/src/lib/server/list-fields-routes.js.map +1 -0
- package/src/lib/server/record-email-state.js +5 -2
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-timeline.js +3 -2
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/task-routes.js +6 -3
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server/tasks-import.js +5 -2
- package/src/lib/server/tasks-import.js.map +1 -1
- package/src/lib/server.js +10 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/components/contact-custom-fields-card.d.ts +0 -39
- package/src/lib/components/contact-custom-fields-card.js +0 -204
- package/src/lib/components/contact-custom-fields-card.js.map +0 -1
- package/src/lib/hooks/use-org-leads.d.ts +0 -50
- package/src/lib/hooks/use-org-leads.js +0 -104
- package/src/lib/hooks/use-org-leads.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/recent-activity-feed.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 type { CrmActivityRow } from '@aglyn/aglyn'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/recent-activity-feed.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 type { CrmActivityRow } from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Typography } from '@mui/material'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { crmRoutes } from '../model/crm-routes'\nimport { ActivityList } from './activity-list'\nimport {\n type CrmOrg,\n useActivityScope,\n useActivityWindow,\n} from './activity-queries'\nimport { TimelineExpandAllButton, useTimelineExpansion } from './activity-timeline'\n\n/** How the record an activity is about reads in the feed. */\nconst RECORD_LABELS = {\n contact: 'Contact',\n deal: 'Deal',\n company: 'Company',\n lead: 'Lead',\n} as const\n\n/** One page of the feed, and the step its listener widens by — a glance, not a log. */\nexport const RECENT_ACTIVITY_LIMIT = 10\n\n/** The empty link: nothing to render, the query returns nothing. */\nconst NO_RECORD = Object.freeze({})\n\nexport interface RecentActivityFeedProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org: CrmOrg\n /** A page of the feed, and how far its listener widens when a page is turned past it. */\n limit?: number\n /**\n * The hub's mount path, for the link each row carries to its record. The\n * shell supplies it on every hub page; a mount without one draws the\n * record's label with no link rather than a link into nowhere.\n */\n basePath?: string\n}\n\n/**\n * The newest logged activity across the CRM (AGL-2600): the last few calls,\n * emails, meetings and notes anyone on the team filed against any record,\n * each linking to the record it is about.\n *\n * The contacts landing shows it as a card of its own under the list card,\n * so that opening the CRM answers \"what has the team been doing\" before\n * anybody opens a record, without the list running on into it. One bounded\n * listener on `(visibleTo, atMs DESC)` — the index that exists for exactly\n * this query — filtered to what this reader may see, the same predicate the\n * rules evaluate. It reads one page, `limit`, and widens by a page only\n * when the reader turns to the next one.\n *\n * Every row is collapsed to its kind, its heading, who and when, and opens\n * in place to the body; **Expand all** in the card header opens them all.\n *\n * A row links to its CONTACT when it has one, else its deal, else its\n * company — `crmActivityRecordLink`'s precedence — through `crmRoutes`, so\n * the address it builds is the one the hub resolves. Nothing is looked up\n * to name the record: a name per row would be a document read per row of a\n * feed drawn on every visit to the list, and the record's page names itself.\n */\nexport function RecentActivityFeed(props: RecentActivityFeedProps) {\n const { hostId, org, limit = RECENT_ACTIVITY_LIMIT, basePath } = props\n const scope = useActivityScope(hostId, org)\n const activities = useActivityWindow(scope, NO_RECORD, limit)\n const expansion = useTimelineExpansion()\n /*\n * Whether the feed has ever had a row. A page turned past the window\n * re-reads it, and the listener holds nothing while it does; the card has\n * to stay through that rather than vanish with its page.\n */\n const [seen, setSeen] = useState(false)\n useEffect(() => {\n if (activities.rows.length) setSeen(true)\n }, [activities.rows.length])\n const routes = useMemo(\n () => (basePath ? crmRoutes(basePath) : null),\n [basePath],\n )\n const subjectFor = useCallback(\n (activity: CrmActivityRow) => {\n const target = Aglyn.crmActivityRecordLink(activity)\n if (!target) return null\n const label = RECORD_LABELS[target.record]\n const href = routes ? routes[target.record](target.id) : null\n return href ? (\n <Typography variant=\"caption\">\n <AppLink href={href}>{label}</AppLink>\n </Typography>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {label}\n </Typography>\n )\n },\n [routes],\n )\n\n // Nothing to show and nothing to say: a landing with no activity yet\n // should not carry an empty heading for a feature the reader has not\n // used, and the record pages are where the \"log one\" affordance lives.\n if (activities.status !== 'error' && !activities.rows.length && !seen) return null\n\n return (\n <CardDisplay\n header={'Recent activity'}\n help={Aglyn.pluginDocsHelp('contactActivities', { anchor: '#the-recent-activity-feed' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <TimelineExpandAllButton\n expansion={expansion}\n disabled={!activities.rows.length}\n />\n ),\n }}\n >\n {activities.status === 'error' ? (\n <Typography variant=\"body2\" color=\"error\">\n {'Recent activity could not be loaded.'}\n </Typography>\n ) : (\n <ActivityList\n rows={activities.rows}\n scope={scope}\n subjectFor={subjectFor}\n readOnly\n hasMore={activities.hasMore}\n onShowMore={activities.showMore}\n loading={activities.status === 'loading'}\n expansion={expansion}\n />\n )}\n </CardDisplay>\n )\n}\nRecentActivityFeed.displayName = 'RecentActivityFeed'\n\nexport default RecentActivityFeed\n"],"names":["Aglyn","AppLink","CardDisplay","Typography","useCallback","useEffect","useMemo","useState","crmRoutes","ActivityList","useActivityScope","useActivityWindow","TimelineExpandAllButton","useTimelineExpansion","RECORD_LABELS","contact","deal","company","lead","RECENT_ACTIVITY_LIMIT","NO_RECORD","Object","freeze","RecentActivityFeed","props","hostId","org","limit","basePath","scope","activities","expansion","seen","setSeen","rows","length","routes","subjectFor","activity","target","crmActivityRecordLink","label","record","href","id","variant","color","status","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","disabled","readOnly","hasMore","onShowMore","showMore","loading","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,YAAYA,WAAW,eAAc;AAErC,SAASC,OAAO,EAAEC,WAAW,QAAQ,uBAAsB;AAC3D,SAASC,UAAU,QAAQ,gBAAe;AAC1C,SAASC,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACjE,SAASC,SAAS,QAAQ,yBAAqB;AAC/C,SAASC,YAAY,QAAQ,qBAAiB;AAC9C,SAEEC,gBAAgB,EAChBC,iBAAiB,QACZ,wBAAoB;AAC3B,SAASC,uBAAuB,EAAEC,oBAAoB,QAAQ,yBAAqB;AAEnF,2DAA2D,GAC3D,MAAMC,gBAAgB;IACpBC,SAAS;IACTC,MAAM;IACNC,SAAS;IACTC,MAAM;AACR;AAEA,qFAAqF,GACrF,OAAO,MAAMC,wBAAwB,GAAE;AAEvC,kEAAkE,GAClE,MAAMC,YAAYC,OAAOC,MAAM,CAAC,CAAC;AAgBjC;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,mBAAmBC,KAA8B;IAC/D,MAAM,EAAEC,MAAM,EAAEC,GAAG,EAAEC,QAAQR,qBAAqB,EAAES,QAAQ,EAAE,GAAGJ;IACjE,MAAMK,QAAQnB,iBAAiBe,QAAQC;IACvC,MAAMI,aAAanB,kBAAkBkB,OAAOT,WAAWO;IACvD,MAAMI,YAAYlB;IAClB;;;;GAIC,GACD,MAAM,CAACmB,MAAMC,QAAQ,GAAG1B,SAAS;IACjCF,UAAU;QACR,IAAIyB,WAAWI,IAAI,CAACC,MAAM,EAAEF,QAAQ;IACtC,GAAG;QAACH,WAAWI,IAAI,CAACC,MAAM;KAAC;IAC3B,MAAMC,SAAS9B,QACb,IAAOsB,WAAWpB,UAAUoB,YAAY,MACxC;QAACA;KAAS;IAEZ,MAAMS,aAAajC,YACjB,CAACkC;QACC,MAAMC,SAASvC,MAAMwC,qBAAqB,CAACF;QAC3C,IAAI,CAACC,QAAQ,OAAO;QACpB,MAAME,QAAQ3B,aAAa,CAACyB,OAAOG,MAAM,CAAC;QAC1C,MAAMC,OAAOP,SAASA,MAAM,CAACG,OAAOG,MAAM,CAAC,CAACH,OAAOK,EAAE,IAAI;QACzD,OAAOD,qBACL,KAACxC;YAAW0C,SAAQ;sBAClB,cAAA,KAAC5C;gBAAQ0C,MAAMA;0BAAOF;;2BAGxB,KAACtC;YAAW0C,SAAQ;YAAUC,OAAM;sBACjCL;;IAGP,GACA;QAACL;KAAO;IAGV,qEAAqE;IACrE,qEAAqE;IACrE,uEAAuE;IACvE,IAAIN,WAAWiB,MAAM,KAAK,WAAW,CAACjB,WAAWI,IAAI,CAACC,MAAM,IAAI,CAACH,MAAM,OAAO;IAE9E,qBACE,KAAC9B;QACC8C,QAAQ;QACRC,MAAMjD,MAAMkD,cAAc,CAAC,qBAAqB;YAAEC,QAAQ;QAA4B;QACtFC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,sBACE,KAAC3C;gBACCmB,WAAWA;gBACXyB,UAAU,CAAC1B,WAAWI,IAAI,CAACC,MAAM;;QAGvC;kBAECL,WAAWiB,MAAM,KAAK,wBACrB,KAAC5C;YAAW0C,SAAQ;YAAQC,OAAM;sBAC/B;2BAGH,KAACrC;YACCyB,MAAMJ,WAAWI,IAAI;YACrBL,OAAOA;YACPQ,YAAYA;YACZoB,QAAQ;YACRC,SAAS5B,WAAW4B,OAAO;YAC3BC,YAAY7B,WAAW8B,QAAQ;YAC/BC,SAAS/B,WAAWiB,MAAM,KAAK;YAC/BhB,WAAWA;;;AAKrB;AACAR,mBAAmBuC,WAAW,GAAG;AAEjC,eAAevC,mBAAkB"}
|
|
@@ -9,8 +9,10 @@ export type RecordActivityCardProps = ActivityRecordLink & {
|
|
|
9
9
|
* (AGL-2615) — with the button that adds to it (AGL-2600).
|
|
10
10
|
*
|
|
11
11
|
* One card for all four record pages, because the log is the same thing on
|
|
12
|
-
* each of them: what people did about this record, newest first,
|
|
13
|
-
*
|
|
12
|
+
* each of them: what people did about this record, newest first, one
|
|
13
|
+
* collapsed entry each and paged by the console's footer over a listener
|
|
14
|
+
* bounded to a hundred, which a page turned past it widens by the next
|
|
15
|
+
* hundred. **Log activity** and **Expand all** sit in the card header. Which
|
|
14
16
|
* record is decided by whichever of the four ids the page passes, and that
|
|
15
17
|
* id is what the dialog files a new activity against — the page fixes it,
|
|
16
18
|
* the reader does not pick it.
|
|
@@ -17,18 +17,21 @@
|
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
18
|
import { pluginDocsHelp } from "@aglyn/aglyn";
|
|
19
19
|
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
20
|
-
import { Button, Typography } from "@mui/material";
|
|
20
|
+
import { Button, Stack, Typography } from "@mui/material";
|
|
21
21
|
import { useCallback, useMemo, useState } from "react";
|
|
22
22
|
import { ActivityList } from "./activity-list.js";
|
|
23
23
|
import { useActivityScope, useActivityWindow } from "./activity-queries.js";
|
|
24
|
+
import { TimelineExpandAllButton, useTimelineExpansion } from "./activity-timeline.js";
|
|
24
25
|
import { LogActivityDialog } from "./log-activity-dialog.js";
|
|
25
26
|
/**
|
|
26
27
|
* The activity log of one record — a contact, a company, a deal or a lead
|
|
27
28
|
* (AGL-2615) — with the button that adds to it (AGL-2600).
|
|
28
29
|
*
|
|
29
30
|
* One card for all four record pages, because the log is the same thing on
|
|
30
|
-
* each of them: what people did about this record, newest first,
|
|
31
|
-
*
|
|
31
|
+
* each of them: what people did about this record, newest first, one
|
|
32
|
+
* collapsed entry each and paged by the console's footer over a listener
|
|
33
|
+
* bounded to a hundred, which a page turned past it widens by the next
|
|
34
|
+
* hundred. **Log activity** and **Expand all** sit in the card header. Which
|
|
32
35
|
* record is decided by whichever of the four ids the page passes, and that
|
|
33
36
|
* id is what the dialog files a new activity against — the page fixes it,
|
|
34
37
|
* the reader does not pick it.
|
|
@@ -56,6 +59,7 @@ import { LogActivityDialog } from "./log-activity-dialog.js";
|
|
|
56
59
|
leadId
|
|
57
60
|
]);
|
|
58
61
|
const activities = useActivityWindow(scope, link);
|
|
62
|
+
const expansion = useTimelineExpansion();
|
|
59
63
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
60
64
|
const [editing, setEditing] = useState(null);
|
|
61
65
|
const openNew = useCallback(()=>{
|
|
@@ -74,14 +78,26 @@ import { LogActivityDialog } from "./log-activity-dialog.js";
|
|
|
74
78
|
help: pluginDocsHelp('contactActivities', {
|
|
75
79
|
anchor: '#logging-an-activity'
|
|
76
80
|
}),
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
HeaderProps: {
|
|
82
|
+
action: /*#__PURE__*/ _jsxs(Stack, {
|
|
83
|
+
direction: "row",
|
|
84
|
+
spacing: 1,
|
|
85
|
+
children: [
|
|
86
|
+
/*#__PURE__*/ _jsx(TimelineExpandAllButton, {
|
|
87
|
+
expansion: expansion,
|
|
88
|
+
disabled: !activities.rows.length
|
|
89
|
+
}),
|
|
90
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
91
|
+
size: "small",
|
|
92
|
+
variant: "contained",
|
|
93
|
+
color: "primary",
|
|
94
|
+
onClick: openNew,
|
|
95
|
+
disabled: !activities.ready,
|
|
96
|
+
children: 'Log activity'
|
|
97
|
+
})
|
|
98
|
+
]
|
|
99
|
+
})
|
|
100
|
+
},
|
|
85
101
|
contentGutterX: true,
|
|
86
102
|
contentGutterY: true,
|
|
87
103
|
children: activities.status === 'error' ? /*#__PURE__*/ _jsx(Typography, {
|
|
@@ -94,6 +110,8 @@ import { LogActivityDialog } from "./log-activity-dialog.js";
|
|
|
94
110
|
onEdit: openEdit,
|
|
95
111
|
hasMore: activities.hasMore,
|
|
96
112
|
onShowMore: activities.showMore,
|
|
113
|
+
loading: activities.status === 'loading',
|
|
114
|
+
expansion: expansion,
|
|
97
115
|
emptyText: "A call, an email, a meeting or a note about this record goes here.",
|
|
98
116
|
emptyAction: /*#__PURE__*/ _jsx(Button, {
|
|
99
117
|
size: "small",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/record-activity-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 type { CrmActivityRow } from '@aglyn/aglyn'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Button, Typography } from '@mui/material'\nimport { useCallback, useMemo, useState } from 'react'\nimport { ActivityList } from './activity-list'\nimport {\n type ActivityRecordLink,\n type CrmOrg,\n useActivityScope,\n useActivityWindow,\n} from './activity-queries'\nimport { LogActivityDialog } from './log-activity-dialog'\n\nexport type RecordActivityCardProps = ActivityRecordLink & {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org: CrmOrg\n}\n\n/**\n * The activity log of one record — a contact, a company, a deal or a lead\n * (AGL-2615) — with the button that adds to it (AGL-2600).\n *\n * One card for all four record pages, because the log is the same thing on\n * each of them: what people did about this record, newest first,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/record-activity-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 type { CrmActivityRow } from '@aglyn/aglyn'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Button, Stack, Typography } from '@mui/material'\nimport { useCallback, useMemo, useState } from 'react'\nimport { ActivityList } from './activity-list'\nimport {\n type ActivityRecordLink,\n type CrmOrg,\n useActivityScope,\n useActivityWindow,\n} from './activity-queries'\nimport { TimelineExpandAllButton, useTimelineExpansion } from './activity-timeline'\nimport { LogActivityDialog } from './log-activity-dialog'\n\nexport type RecordActivityCardProps = ActivityRecordLink & {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org: CrmOrg\n}\n\n/**\n * The activity log of one record — a contact, a company, a deal or a lead\n * (AGL-2615) — with the button that adds to it (AGL-2600).\n *\n * One card for all four record pages, because the log is the same thing on\n * each of them: what people did about this record, newest first, one\n * collapsed entry each and paged by the console's footer over a listener\n * bounded to a hundred, which a page turned past it widens by the next\n * hundred. **Log activity** and **Expand all** sit in the card header. Which\n * record is decided by whichever of the four ids the page passes, and that\n * id is what the dialog files a new activity against — the page fixes it,\n * the reader does not pick it.\n *\n * The contact's page does NOT use this card. A person has a second history\n * the platform captured on the row itself, and the two belong in one\n * stream; see `contact-timeline-card.tsx`, which is this card's superset.\n */\nexport function RecordActivityCard(props: RecordActivityCardProps) {\n const { hostId, org, contactId, companyId, dealId, leadId } = props\n const scope = useActivityScope(hostId, org)\n /*\n * Memoized because the link is a listener dependency by value inside\n * `useActivityWindow` and the dialog's prop by reference: a fresh object\n * each render would be harmless to the query, which reads the ids, and\n * noisy for anything that compares the object.\n */\n const link = useMemo<ActivityRecordLink>(\n () => ({ contactId, companyId, dealId, leadId }),\n [contactId, companyId, dealId, leadId],\n )\n const activities = useActivityWindow(scope, link)\n const expansion = useTimelineExpansion()\n const [dialogOpen, setDialogOpen] = useState(false)\n const [editing, setEditing] = useState<CrmActivityRow | null>(null)\n const openNew = useCallback(() => {\n setEditing(null)\n setDialogOpen(true)\n }, [])\n const openEdit = useCallback((activity: CrmActivityRow) => {\n setEditing(activity)\n setDialogOpen(true)\n }, [])\n const close = useCallback(() => setDialogOpen(false), [])\n\n return (\n <>\n <CardDisplay\n header={'Activity'}\n help={pluginDocsHelp('contactActivities', { anchor: '#logging-an-activity' })}\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <TimelineExpandAllButton\n expansion={expansion}\n disabled={!activities.rows.length}\n />\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={openNew}\n disabled={!activities.ready}\n >\n {'Log activity'}\n </Button>\n </Stack>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n {activities.status === 'error' ? (\n <Typography variant=\"body2\" color=\"error\">\n {'The activity log could not be loaded.'}\n </Typography>\n ) : (\n <ActivityList\n rows={activities.rows}\n scope={scope}\n onEdit={openEdit}\n hasMore={activities.hasMore}\n onShowMore={activities.showMore}\n loading={activities.status === 'loading'}\n expansion={expansion}\n emptyText=\"A call, an email, a meeting or a note about this record goes here.\"\n emptyAction={\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={openNew}\n disabled={!activities.ready}\n >\n {'Log activity'}\n </Button>\n }\n />\n )}\n </CardDisplay>\n <LogActivityDialog\n open={dialogOpen}\n onClose={close}\n scope={scope}\n link={link}\n activity={editing}\n />\n </>\n )\n}\nRecordActivityCard.displayName = 'RecordActivityCard'\n\nexport default RecordActivityCard\n"],"names":["pluginDocsHelp","CardDisplay","Button","Stack","Typography","useCallback","useMemo","useState","ActivityList","useActivityScope","useActivityWindow","TimelineExpandAllButton","useTimelineExpansion","LogActivityDialog","RecordActivityCard","props","hostId","org","contactId","companyId","dealId","leadId","scope","link","activities","expansion","dialogOpen","setDialogOpen","editing","setEditing","openNew","openEdit","activity","close","header","help","anchor","HeaderProps","action","direction","spacing","disabled","rows","length","size","variant","color","onClick","ready","contentGutterX","contentGutterY","status","onEdit","hasMore","onShowMore","showMore","loading","emptyText","emptyAction","open","onClose","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAGA,SAASA,cAAc,QAAQ,eAAc;AAC7C,SAASC,WAAW,QAAQ,uBAAsB;AAClD,SAASC,MAAM,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACzD,SAASC,WAAW,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACtD,SAASC,YAAY,QAAQ,qBAAiB;AAC9C,SAGEC,gBAAgB,EAChBC,iBAAiB,QACZ,wBAAoB;AAC3B,SAASC,uBAAuB,EAAEC,oBAAoB,QAAQ,yBAAqB;AACnF,SAASC,iBAAiB,QAAQ,2BAAuB;AAQzD;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,mBAAmBC,KAA8B;IAC/D,MAAM,EAAEC,MAAM,EAAEC,GAAG,EAAEC,SAAS,EAAEC,SAAS,EAAEC,MAAM,EAAEC,MAAM,EAAE,GAAGN;IAC9D,MAAMO,QAAQb,iBAAiBO,QAAQC;IACvC;;;;;GAKC,GACD,MAAMM,OAAOjB,QACX,IAAO,CAAA;YAAEY;YAAWC;YAAWC;YAAQC;QAAO,CAAA,GAC9C;QAACH;QAAWC;QAAWC;QAAQC;KAAO;IAExC,MAAMG,aAAad,kBAAkBY,OAAOC;IAC5C,MAAME,YAAYb;IAClB,MAAM,CAACc,YAAYC,cAAc,GAAGpB,SAAS;IAC7C,MAAM,CAACqB,SAASC,WAAW,GAAGtB,SAAgC;IAC9D,MAAMuB,UAAUzB,YAAY;QAC1BwB,WAAW;QACXF,cAAc;IAChB,GAAG,EAAE;IACL,MAAMI,WAAW1B,YAAY,CAAC2B;QAC5BH,WAAWG;QACXL,cAAc;IAChB,GAAG,EAAE;IACL,MAAMM,QAAQ5B,YAAY,IAAMsB,cAAc,QAAQ,EAAE;IAExD,qBACE;;0BACE,KAAC1B;gBACCiC,QAAQ;gBACRC,MAAMnC,eAAe,qBAAqB;oBAAEoC,QAAQ;gBAAuB;gBAC3EC,aAAa;oBACXC,sBACE,MAACnC;wBAAMoC,WAAU;wBAAMC,SAAS;;0CAC9B,KAAC7B;gCACCc,WAAWA;gCACXgB,UAAU,CAACjB,WAAWkB,IAAI,CAACC,MAAM;;0CAEnC,KAACzC;gCACC0C,MAAK;gCACLC,SAAQ;gCACRC,OAAM;gCACNC,SAASjB;gCACTW,UAAU,CAACjB,WAAWwB,KAAK;0CAE1B;;;;gBAIT;gBACAC,cAAc;gBACdC,cAAc;0BAEb1B,WAAW2B,MAAM,KAAK,wBACrB,KAAC/C;oBAAWyC,SAAQ;oBAAQC,OAAM;8BAC/B;mCAGH,KAACtC;oBACCkC,MAAMlB,WAAWkB,IAAI;oBACrBpB,OAAOA;oBACP8B,QAAQrB;oBACRsB,SAAS7B,WAAW6B,OAAO;oBAC3BC,YAAY9B,WAAW+B,QAAQ;oBAC/BC,SAAShC,WAAW2B,MAAM,KAAK;oBAC/B1B,WAAWA;oBACXgC,WAAU;oBACVC,2BACE,KAACxD;wBACC0C,MAAK;wBACLC,SAAQ;wBACRC,OAAM;wBACNC,SAASjB;wBACTW,UAAU,CAACjB,WAAWwB,KAAK;kCAE1B;;;;0BAMX,KAACnC;gBACC8C,MAAMjC;gBACNkC,SAAS3B;gBACTX,OAAOA;gBACPC,MAAMA;gBACNS,UAAUJ;;;;AAIlB;AACAd,mBAAmB+C,WAAW,GAAG;AAEjC,eAAe/C,mBAAkB"}
|
|
@@ -172,6 +172,19 @@ const TASK_DONE_CEILING = 1000;
|
|
|
172
172
|
}),
|
|
173
173
|
contentGutterX: true,
|
|
174
174
|
contentGutterY: true,
|
|
175
|
+
HeaderProps: {
|
|
176
|
+
action: /*#__PURE__*/ _jsx(ReportExport, {
|
|
177
|
+
filename: reportFilename('activity', period),
|
|
178
|
+
columns: COLUMNS,
|
|
179
|
+
rows: ()=>rows.map((row)=>[
|
|
180
|
+
nameOf(row),
|
|
181
|
+
...Aglyn.CRM_ACTIVITY_KINDS.map((kind)=>row.kinds[kind]),
|
|
182
|
+
row.activities,
|
|
183
|
+
tasksRead ? row.tasksDone : ''
|
|
184
|
+
]),
|
|
185
|
+
disabled: !activitiesRead || !rows.length
|
|
186
|
+
})
|
|
187
|
+
},
|
|
175
188
|
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
176
189
|
spacing: 2,
|
|
177
190
|
children: [
|
|
@@ -257,18 +270,11 @@ const TASK_DONE_CEILING = 1000;
|
|
|
257
270
|
color: "text.secondary",
|
|
258
271
|
children: 'No teammate — activities an automation logged, and tasks completed with nobody assigned.'
|
|
259
272
|
}) : null,
|
|
260
|
-
/*#__PURE__*/ _jsx(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
...Aglyn.CRM_ACTIVITY_KINDS.map((kind)=>row.kinds[kind]),
|
|
266
|
-
row.activities,
|
|
267
|
-
tasksRead ? row.tasksDone : ''
|
|
268
|
-
]),
|
|
269
|
-
disabled: !activitiesRead || !rows.length,
|
|
270
|
-
caption: caption ? `${caption}; the tiles are counted on the server.` : undefined
|
|
271
|
-
})
|
|
273
|
+
caption ? /*#__PURE__*/ _jsx(Typography, {
|
|
274
|
+
variant: "caption",
|
|
275
|
+
color: "text.secondary",
|
|
276
|
+
children: `${caption}; the tiles are counted on the server.`
|
|
277
|
+
}) : null
|
|
272
278
|
]
|
|
273
279
|
})
|
|
274
280
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/activity-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 { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n getCountFromServer,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useOrgMemberOptions } from '../../hooks/use-org-member-options'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\n\n/**\n * How many activities and how many completed tasks the leaderboard is\n * grouped from.\n *\n * The tiles are server counts and never meet these bounds; the table needs\n * each activity's kind and author and each task's completer, so it reads\n * the period's newest thousand of each and says when there were more —\n * the contacts list's bound, and the one every window on this page shares.\n */\nconst ACTIVITY_CEILING = 1000\nconst TASK_DONE_CEILING = 1000\n\ntype ActivityRow = Aglyn.CrmActivityRow\ntype TaskRow = Aglyn.CrmTask & { $id: string }\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = [\n 'Teammate',\n ...Aglyn.CRM_ACTIVITY_KINDS.map((kind) => Aglyn.CRM_ACTIVITY_KIND_LABELS[kind]),\n 'Activities',\n 'Tasks done',\n] as const\n\nexport interface ActivityCardProps {\n report: CrmReportScope\n}\n\n/**\n * Activity by teammate (AGL-2624): the calls, emails, meetings and notes\n * each member logged in the period, and the tasks they ticked off,\n * busiest first.\n *\n * ## Two windows, two indexes, one table\n *\n * Activities are counted within the period on `atMs`; completed tasks are\n * counted the same way on `completedAtMs`, under `status == 'done'`. Each\n * count is also drawn as a window, newest first, for the leaderboard.\n *\n * ## Why that is four indexes and not one\n *\n * The counts and the windows do not share an index, and neither do the two\n * audiences.\n *\n * A COUNT here names no `orderBy`. Firestore then orders implicitly by the\n * range field, ASCENDING, so a count wants `completedAtMs` ascending — while\n * the window beside it asks for `desc` explicitly and wants the descending\n * one. A composite index serves one direction or its exact reverse, never\n * both, so declaring only the direction the window reads leaves the count\n * with nothing: `FAILED_PRECONDITION`, one column of dashes, and a card that\n * otherwise looks fine.\n *\n * Then the visibility clause doubles it. `visibleToClause` contributes\n * nothing when the reader's tokens are null, which is every organization-level\n * read, so the shape arriving at Firestore has no leading array field — a\n * different index, not a prefix of the one with it. `(visibleTo, status,\n * completedAtMs)` serves a site and `(status, completedAtMs)` the org above\n * it; one without the other is a card that works for exactly one audience.\n *\n * The two reads are independent so that a missing task index degrades ONE\n * column to a dash and a notice rather than the whole card: the activities\n * were read, and the leaderboard draws them.\n *\n * A task ordered by `completedAtMs` is a task the complete route stamped;\n * a task marked done some other way, without the stamp, is not in the\n * window and the caption says what the column is grouped from.\n *\n * ## Names\n *\n * Every activity carries the name it was signed with, for the reason\n * `CrmActivity.byName` gives — a scoped reader cannot resolve a colleague's\n * uid. The roster is read too, through the members route any member may\n * ask, and wins when it knows the uid, so a renamed teammate reads by\n * their current name; the signed name covers a uid the roster no longer\n * has, and the uid itself covers the rest.\n */\nexport function ActivityCard(props: ActivityCardProps) {\n const { report } = props\n const { scope, tokens, range, period, routes } = report\n const firestore = useFirestore()\n const roster = useOrgMemberOptions(scope[1])\n\n const activitiesBetween = (from: number, to: number) =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.activities),\n ...visibleToClause(tokens),\n where('atMs', '>=', from),\n where('atMs', '<', to),\n )\n const tasksDoneBetween = (from: number, to: number) =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks),\n ...visibleToClause(tokens),\n where('status', '==', 'done'),\n where('completedAtMs', '>=', from),\n where('completedAtMs', '<', to),\n )\n const countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n const activityCounts = useAggregateRead(\n () =>\n Promise.all([\n countOf(activitiesBetween(range.from, range.to)),\n countOf(activitiesBetween(range.previousFrom, range.previousTo)),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:counts') },\n )\n const taskCounts = useAggregateRead(\n () =>\n Promise.all([\n countOf(tasksDoneBetween(range.from, range.to)),\n countOf(tasksDoneBetween(range.previousFrom, range.previousTo)),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:tasks-counts') },\n )\n\n const activityWindow = useWindowRead<ActivityRow>(\n () =>\n query(\n activitiesBetween(range.from, range.to),\n orderBy('atMs', 'desc'),\n limit(ACTIVITY_CEILING + 1),\n ),\n ACTIVITY_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:window') },\n )\n const taskWindow = useWindowRead<TaskRow>(\n () =>\n query(\n tasksDoneBetween(range.from, range.to),\n orderBy('completedAtMs', 'desc'),\n limit(TASK_DONE_CEILING + 1),\n ),\n TASK_DONE_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:tasks-window') },\n )\n\n const rows = useMemo(\n () => Aglyn.activityLeaderboard(activityWindow.rows, taskWindow.rows),\n [activityWindow, taskWindow],\n )\n const nameOf = (row: Aglyn.ActivityLeaderboardRow): string => {\n if (!row.uid) return 'No teammate'\n return Aglyn.findOrgMember(roster.options, row.uid)?.label ?? row.name ?? row.uid\n }\n const teammates = rows.filter((row) => row.uid).length\n\n const activitiesRead = activityWindow.status === 'success'\n const tasksRead = taskWindow.status === 'success'\n const settled = activityWindow.status !== 'loading' && taskWindow.status !== 'loading'\n const caption = [\n activityWindow.truncated\n ? `Grouped from the ${ACTIVITY_CEILING.toLocaleString()} most recent activities in the period`\n : null,\n taskWindow.truncated\n ? `the ${TASK_DONE_CEILING.toLocaleString()} most recently completed tasks`\n : null,\n ]\n .filter(Boolean)\n .join(' and ')\n const activityFigures = activityCounts.value\n const taskFigures = taskCounts.value\n\n return (\n <CardDisplay\n header={'Activity by teammate'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#activity-by-teammate',\n excerpt:\n 'The calls, emails, meetings and notes each teammate logged in ' +\n 'the period, and the tasks they completed, busiest first. ' +\n 'Grouped from the period’s newest thousand of each.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Activities logged'}\n value={activityFigures ? activityFigures.current.toLocaleString() : null}\n deltaPct={\n activityFigures\n ? Aglyn.deltaPercent(activityFigures.current, activityFigures.previous)\n : null\n }\n note={'counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Tasks completed'}\n value={taskFigures ? taskFigures.current.toLocaleString() : null}\n deltaPct={\n taskFigures\n ? Aglyn.deltaPercent(taskFigures.current, taskFigures.previous)\n : null\n }\n note={'ticked off in the period'}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Teammates active'}\n value={settled ? teammates.toLocaleString() : null}\n note={'logged or completed something'}\n />\n </Stack>\n {activityCounts.status === 'error' || activityWindow.status === 'error' ? (\n <Alert severity=\"warning\">{'The activities could not be read.'}</Alert>\n ) : null}\n {taskCounts.status === 'error' || taskWindow.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The completed-task counts could not be read; the Tasks done column is left blank.'}\n </Alert>\n ) : null}\n <Section title={'Who did what'}>\n {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 {rows.map((row) => (\n <TableRow key={row.uid || '$nobody'}>\n <TableCell>{nameOf(row)}</TableCell>\n {Aglyn.CRM_ACTIVITY_KINDS.map((kind) => (\n <TableCell key={kind} align=\"right\">\n {row.kinds[kind].toLocaleString()}\n </TableCell>\n ))}\n <TableCell align=\"right\">{row.activities.toLocaleString()}</TableCell>\n <TableCell align=\"right\">\n {tasksRead ? row.tasksDone.toLocaleString() : '—'}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled ? 'Nothing logged or completed in this period.' : 'Reading…'}\n </Typography>\n )}\n {rows.some((row) => !row.uid) ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No teammate — activities an automation logged, and tasks completed with nobody assigned.'}\n </Typography>\n ) : null}\n <ReportExport\n filename={reportFilename('activity', period)}\n columns={COLUMNS}\n rows={() =>\n rows.map((row) => [\n nameOf(row),\n ...Aglyn.CRM_ACTIVITY_KINDS.map((kind) => row.kinds[kind]),\n row.activities,\n tasksRead ? row.tasksDone : '',\n ])\n }\n disabled={!activitiesRead || !rows.length}\n caption={caption ? `${caption}; the tiles are counted on the server.` : undefined}\n />\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nActivityCard.displayName = 'ActivityCard'\n\nexport default ActivityCard\n"],"names":["Aglyn","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","where","useMemo","useFirestore","useOrgMemberOptions","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","ACTIVITY_CEILING","TASK_DONE_CEILING","COLUMNS","CRM_ACTIVITY_KINDS","map","kind","CRM_ACTIVITY_KIND_LABELS","ActivityCard","props","report","scope","tokens","range","period","routes","firestore","roster","activitiesBetween","from","to","CRM_COLLECTIONS","activities","tasksDoneBetween","tasks","countOf","target","then","snapshot","data","count","activityCounts","Promise","all","previousFrom","previousTo","current","previous","cacheKey","taskCounts","activityWindow","taskWindow","rows","activityLeaderboard","nameOf","row","uid","findOrgMember","options","label","name","teammates","filter","length","activitiesRead","status","tasksRead","settled","caption","truncated","toLocaleString","Boolean","join","activityFigures","value","taskFigures","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","direction","sx","flexWrap","deltaPct","deltaPercent","note","href","section","severity","title","size","column","index","align","kinds","tasksDone","variant","color","some","filename","columns","disabled","undefined","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/activity-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 { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n getCountFromServer,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useOrgMemberOptions } from '../../hooks/use-org-member-options'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\n\n/**\n * How many activities and how many completed tasks the leaderboard is\n * grouped from.\n *\n * The tiles are server counts and never meet these bounds; the table needs\n * each activity's kind and author and each task's completer, so it reads\n * the period's newest thousand of each and says when there were more —\n * the contacts list's bound, and the one every window on this page shares.\n */\nconst ACTIVITY_CEILING = 1000\nconst TASK_DONE_CEILING = 1000\n\ntype ActivityRow = Aglyn.CrmActivityRow\ntype TaskRow = Aglyn.CrmTask & { $id: string }\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = [\n 'Teammate',\n ...Aglyn.CRM_ACTIVITY_KINDS.map((kind) => Aglyn.CRM_ACTIVITY_KIND_LABELS[kind]),\n 'Activities',\n 'Tasks done',\n] as const\n\nexport interface ActivityCardProps {\n report: CrmReportScope\n}\n\n/**\n * Activity by teammate (AGL-2624): the calls, emails, meetings and notes\n * each member logged in the period, and the tasks they ticked off,\n * busiest first.\n *\n * ## Two windows, two indexes, one table\n *\n * Activities are counted within the period on `atMs`; completed tasks are\n * counted the same way on `completedAtMs`, under `status == 'done'`. Each\n * count is also drawn as a window, newest first, for the leaderboard.\n *\n * ## Why that is four indexes and not one\n *\n * The counts and the windows do not share an index, and neither do the two\n * audiences.\n *\n * A COUNT here names no `orderBy`. Firestore then orders implicitly by the\n * range field, ASCENDING, so a count wants `completedAtMs` ascending — while\n * the window beside it asks for `desc` explicitly and wants the descending\n * one. A composite index serves one direction or its exact reverse, never\n * both, so declaring only the direction the window reads leaves the count\n * with nothing: `FAILED_PRECONDITION`, one column of dashes, and a card that\n * otherwise looks fine.\n *\n * Then the visibility clause doubles it. `visibleToClause` contributes\n * nothing when the reader's tokens are null, which is every organization-level\n * read, so the shape arriving at Firestore has no leading array field — a\n * different index, not a prefix of the one with it. `(visibleTo, status,\n * completedAtMs)` serves a site and `(status, completedAtMs)` the org above\n * it; one without the other is a card that works for exactly one audience.\n *\n * The two reads are independent so that a missing task index degrades ONE\n * column to a dash and a notice rather than the whole card: the activities\n * were read, and the leaderboard draws them.\n *\n * A task ordered by `completedAtMs` is a task the complete route stamped;\n * a task marked done some other way, without the stamp, is not in the\n * window and the caption says what the column is grouped from.\n *\n * ## Names\n *\n * Every activity carries the name it was signed with, for the reason\n * `CrmActivity.byName` gives — a scoped reader cannot resolve a colleague's\n * uid. The roster is read too, through the members route any member may\n * ask, and wins when it knows the uid, so a renamed teammate reads by\n * their current name; the signed name covers a uid the roster no longer\n * has, and the uid itself covers the rest.\n */\nexport function ActivityCard(props: ActivityCardProps) {\n const { report } = props\n const { scope, tokens, range, period, routes } = report\n const firestore = useFirestore()\n const roster = useOrgMemberOptions(scope[1])\n\n const activitiesBetween = (from: number, to: number) =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.activities),\n ...visibleToClause(tokens),\n where('atMs', '>=', from),\n where('atMs', '<', to),\n )\n const tasksDoneBetween = (from: number, to: number) =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks),\n ...visibleToClause(tokens),\n where('status', '==', 'done'),\n where('completedAtMs', '>=', from),\n where('completedAtMs', '<', to),\n )\n const countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n const activityCounts = useAggregateRead(\n () =>\n Promise.all([\n countOf(activitiesBetween(range.from, range.to)),\n countOf(activitiesBetween(range.previousFrom, range.previousTo)),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:counts') },\n )\n const taskCounts = useAggregateRead(\n () =>\n Promise.all([\n countOf(tasksDoneBetween(range.from, range.to)),\n countOf(tasksDoneBetween(range.previousFrom, range.previousTo)),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:tasks-counts') },\n )\n\n const activityWindow = useWindowRead<ActivityRow>(\n () =>\n query(\n activitiesBetween(range.from, range.to),\n orderBy('atMs', 'desc'),\n limit(ACTIVITY_CEILING + 1),\n ),\n ACTIVITY_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:window') },\n )\n const taskWindow = useWindowRead<TaskRow>(\n () =>\n query(\n tasksDoneBetween(range.from, range.to),\n orderBy('completedAtMs', 'desc'),\n limit(TASK_DONE_CEILING + 1),\n ),\n TASK_DONE_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:tasks-window') },\n )\n\n const rows = useMemo(\n () => Aglyn.activityLeaderboard(activityWindow.rows, taskWindow.rows),\n [activityWindow, taskWindow],\n )\n const nameOf = (row: Aglyn.ActivityLeaderboardRow): string => {\n if (!row.uid) return 'No teammate'\n return Aglyn.findOrgMember(roster.options, row.uid)?.label ?? row.name ?? row.uid\n }\n const teammates = rows.filter((row) => row.uid).length\n\n const activitiesRead = activityWindow.status === 'success'\n const tasksRead = taskWindow.status === 'success'\n const settled = activityWindow.status !== 'loading' && taskWindow.status !== 'loading'\n const caption = [\n activityWindow.truncated\n ? `Grouped from the ${ACTIVITY_CEILING.toLocaleString()} most recent activities in the period`\n : null,\n taskWindow.truncated\n ? `the ${TASK_DONE_CEILING.toLocaleString()} most recently completed tasks`\n : null,\n ]\n .filter(Boolean)\n .join(' and ')\n const activityFigures = activityCounts.value\n const taskFigures = taskCounts.value\n\n return (\n <CardDisplay\n header={'Activity by teammate'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#activity-by-teammate',\n excerpt:\n 'The calls, emails, meetings and notes each teammate logged in ' +\n 'the period, and the tasks they completed, busiest first. ' +\n 'Grouped from the period’s newest thousand of each.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('activity', period)}\n columns={COLUMNS}\n rows={() =>\n rows.map((row) => [\n nameOf(row),\n ...Aglyn.CRM_ACTIVITY_KINDS.map((kind) => row.kinds[kind]),\n row.activities,\n tasksRead ? row.tasksDone : '',\n ])\n }\n disabled={!activitiesRead || !rows.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Activities logged'}\n value={activityFigures ? activityFigures.current.toLocaleString() : null}\n deltaPct={\n activityFigures\n ? Aglyn.deltaPercent(activityFigures.current, activityFigures.previous)\n : null\n }\n note={'counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Tasks completed'}\n value={taskFigures ? taskFigures.current.toLocaleString() : null}\n deltaPct={\n taskFigures\n ? Aglyn.deltaPercent(taskFigures.current, taskFigures.previous)\n : null\n }\n note={'ticked off in the period'}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Teammates active'}\n value={settled ? teammates.toLocaleString() : null}\n note={'logged or completed something'}\n />\n </Stack>\n {activityCounts.status === 'error' || activityWindow.status === 'error' ? (\n <Alert severity=\"warning\">{'The activities could not be read.'}</Alert>\n ) : null}\n {taskCounts.status === 'error' || taskWindow.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The completed-task counts could not be read; the Tasks done column is left blank.'}\n </Alert>\n ) : null}\n <Section title={'Who did what'}>\n {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 {rows.map((row) => (\n <TableRow key={row.uid || '$nobody'}>\n <TableCell>{nameOf(row)}</TableCell>\n {Aglyn.CRM_ACTIVITY_KINDS.map((kind) => (\n <TableCell key={kind} align=\"right\">\n {row.kinds[kind].toLocaleString()}\n </TableCell>\n ))}\n <TableCell align=\"right\">{row.activities.toLocaleString()}</TableCell>\n <TableCell align=\"right\">\n {tasksRead ? row.tasksDone.toLocaleString() : '—'}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled ? 'Nothing logged or completed in this period.' : 'Reading…'}\n </Typography>\n )}\n {rows.some((row) => !row.uid) ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No teammate — activities an automation logged, and tasks completed with nobody assigned.'}\n </Typography>\n ) : null}\n {/* What the table, and so the export, is grouped from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${caption}; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nActivityCard.displayName = 'ActivityCard'\n\nexport default ActivityCard\n"],"names":["Aglyn","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","where","useMemo","useFirestore","useOrgMemberOptions","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","ACTIVITY_CEILING","TASK_DONE_CEILING","COLUMNS","CRM_ACTIVITY_KINDS","map","kind","CRM_ACTIVITY_KIND_LABELS","ActivityCard","props","report","scope","tokens","range","period","routes","firestore","roster","activitiesBetween","from","to","CRM_COLLECTIONS","activities","tasksDoneBetween","tasks","countOf","target","then","snapshot","data","count","activityCounts","Promise","all","previousFrom","previousTo","current","previous","cacheKey","taskCounts","activityWindow","taskWindow","rows","activityLeaderboard","nameOf","row","uid","findOrgMember","options","label","name","teammates","filter","length","activitiesRead","status","tasksRead","settled","caption","truncated","toLocaleString","Boolean","join","activityFigures","value","taskFigures","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","kinds","tasksDone","disabled","spacing","direction","sx","flexWrap","deltaPct","deltaPercent","note","href","section","severity","title","size","column","index","align","variant","color","some","displayName"],"mappings":"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"}
|
|
@@ -244,17 +244,48 @@ OrgLeadFunnelCard.displayName = 'OrgLeadFunnelCard';
|
|
|
244
244
|
contentGutterX: true,
|
|
245
245
|
contentGutterY: true,
|
|
246
246
|
HeaderProps: {
|
|
247
|
-
action: /*#__PURE__*/
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
247
|
+
action: /*#__PURE__*/ _jsxs(Stack, {
|
|
248
|
+
direction: "row",
|
|
249
|
+
spacing: 1,
|
|
250
|
+
children: [
|
|
251
|
+
/*#__PURE__*/ _jsx(ReportExport, {
|
|
252
|
+
filename: reportFilename('lead-funnel', period),
|
|
253
|
+
columns: COLUMNS,
|
|
254
|
+
rows: ()=>[
|
|
255
|
+
...Aglyn.CRM_LEAD_STATUSES.map((leadStatus)=>{
|
|
256
|
+
var _share;
|
|
257
|
+
return [
|
|
258
|
+
'Status',
|
|
259
|
+
Aglyn.CRM_LEAD_STATUS_LABELS[leadStatus],
|
|
260
|
+
funnel.byStatus[leadStatus],
|
|
261
|
+
(_share = share(funnel.byStatus[leadStatus], funnel.total)) != null ? _share : ''
|
|
262
|
+
];
|
|
263
|
+
}),
|
|
264
|
+
...funnel.reasons.map((reason)=>{
|
|
265
|
+
var _share;
|
|
266
|
+
return [
|
|
267
|
+
'Unqualified reason',
|
|
268
|
+
reason.label,
|
|
269
|
+
reason.count,
|
|
270
|
+
(_share = share(reason.count, funnel.byStatus.unqualified)) != null ? _share : ''
|
|
271
|
+
];
|
|
272
|
+
})
|
|
273
|
+
],
|
|
274
|
+
disabled: !read || !funnel.total
|
|
275
|
+
}),
|
|
276
|
+
/*#__PURE__*/ _jsx(Button, _extends({
|
|
277
|
+
component: AppLink
|
|
278
|
+
}, {
|
|
279
|
+
componentVariant: 'naked',
|
|
280
|
+
nativeButton: false
|
|
281
|
+
}, {
|
|
282
|
+
href: routes.section('leads'),
|
|
283
|
+
size: "small",
|
|
284
|
+
color: "primary",
|
|
285
|
+
children: 'Open leads'
|
|
286
|
+
}))
|
|
287
|
+
]
|
|
288
|
+
})
|
|
258
289
|
},
|
|
259
290
|
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
260
291
|
spacing: 2,
|
|
@@ -342,32 +373,11 @@ OrgLeadFunnelCard.displayName = 'OrgLeadFunnelCard';
|
|
|
342
373
|
}) : null
|
|
343
374
|
]
|
|
344
375
|
}),
|
|
345
|
-
/*#__PURE__*/ _jsx(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
var _share;
|
|
351
|
-
return [
|
|
352
|
-
'Status',
|
|
353
|
-
Aglyn.CRM_LEAD_STATUS_LABELS[leadStatus],
|
|
354
|
-
funnel.byStatus[leadStatus],
|
|
355
|
-
(_share = share(funnel.byStatus[leadStatus], funnel.total)) != null ? _share : ''
|
|
356
|
-
];
|
|
357
|
-
}),
|
|
358
|
-
...funnel.reasons.map((reason)=>{
|
|
359
|
-
var _share;
|
|
360
|
-
return [
|
|
361
|
-
'Unqualified reason',
|
|
362
|
-
reason.label,
|
|
363
|
-
reason.count,
|
|
364
|
-
(_share = share(reason.count, funnel.byStatus.unqualified)) != null ? _share : ''
|
|
365
|
-
];
|
|
366
|
-
})
|
|
367
|
-
],
|
|
368
|
-
disabled: !read || !funnel.total,
|
|
369
|
-
caption: caption
|
|
370
|
-
})
|
|
376
|
+
caption ? /*#__PURE__*/ _jsx(Typography, {
|
|
377
|
+
variant: "caption",
|
|
378
|
+
color: "text.secondary",
|
|
379
|
+
children: caption
|
|
380
|
+
}) : null
|
|
371
381
|
]
|
|
372
382
|
})
|
|
373
383
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/lead-funnel-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport {\n collection,\n type Firestore,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../../hooks/use-crm-org-mount'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename } from './report-format'\nimport { type CrmReportScope, reportCacheKey } from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport {\n type AggregateRead,\n useAggregateRead,\n useWindowRead,\n type WindowRead,\n} from './use-aggregate-read'\n\n/**\n * How many of the period's leads the funnel is placed from — the Leads\n * section's own window, over the period rather than over everything.\n *\n * The captured count is a server aggregate and never meets this bound; the\n * funnel needs each lead's status and reason, so it reads the period's\n * newest two hundred and says when there were more. At the organization\n * level the bound is PER SITE (AGL-2634): each site's window is its own,\n * and a site with a thousand leads must not crowd out a site with ten.\n */\nexport const LEAD_CEILING = 200\n/** How many reasons the bar list draws; the rest are counted beneath it. */\nconst REASONS_SHOWN = 8\n\n/** The CSV's columns: one file for both the statuses and the reasons. */\nconst COLUMNS = ['Kind', 'Label', 'Count', 'Share'] as const\n\ntype LeadRow = Record<string, unknown> & Aglyn.CrmLeadFields & { $id: string }\n\n/** The period's captured count, and the previous period's for the delta. */\ninterface CapturedFigures {\n current: number\n previous: number\n}\n\nexport interface LeadFunnelCardProps {\n report: CrmReportScope\n /**\n * The site whose leads are placed. A lead lives under its host, not the\n * org — `hosts/{hostId}/leads`, private by path, no `visibleTo` — so the\n * report's org scope cannot reach it and the card is told the site. At\n * the organization level `null` (AGL-2630): the card then reads every\n * site the mount lists, one window each (AGL-2634).\n */\n hostId: string | null\n}\n\nconst LEAD_FUNNEL_HELP = Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#lead-funnel',\n excerpt:\n 'Of the leads this site captured in the period, how many are ' +\n 'still new, being worked, qualified or unqualified — and the ' +\n 'reasons the unqualified ones were closed. Placed by when each ' +\n 'lead was first seen.',\n})\n\n/**\n * The leads a scope may see, first seen in a range (AGL-3275).\n *\n * `visibleTo` is the site's tokens under a site, and `null` at the\n * organization level where an org-wide member reads without a clause. The\n * range stays a single-field one; with the scope clause it composes with the\n * `visibleTo` + `firstSeenAtMs` entry in `cloud/firebase-firestore.indexes.json`.\n */\nfunction leadsBetween(\n firestore: Firestore,\n orgId: string,\n visibleTo: readonly string[] | null,\n from: number,\n to: number,\n) {\n return query(\n collection(firestore, 'orgs', orgId, 'leads'),\n ...(visibleTo ? [where('visibleTo', 'array-contains-any', [...visibleTo])] : []),\n where('firstSeenAtMs', '>=', from),\n where('firstSeenAtMs', '<', to),\n )\n}\n\nconst countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n/**\n * The lead funnel, at whichever level the report is mounted (AGL-2624,\n * AGL-2630, AGL-2634).\n *\n * Under a site: that site. At the organization level: every site the\n * mount lists, one window each, totaled — the org hub's reports total\n * every site, and the leads are no exception now that the fan-out is\n * bounded by the org's site list. With no sites to read the card says so\n * instead of guessing.\n */\nexport function LeadFunnelCard(props: LeadFunnelCardProps) {\n const { report, hostId } = props\n const mount = useCrmOrgMount()\n if (hostId) return <SiteLeadFunnelCard report={report} hostId={hostId} />\n if (mount && (!mount.hostsReady || mount.hosts.length > 0)) {\n return <OrgLeadFunnelCard report={report} />\n }\n return (\n <CardDisplay header={'Lead funnel'} help={LEAD_FUNNEL_HELP} contentGutterX contentGutterY>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mount\n ? 'Leads live under a site, and this organization has no sites yet.'\n : 'Leads live under a site, and this report is mounted under none.'}\n </Typography>\n </CardDisplay>\n )\n}\n\n/**\n * The lead funnel (AGL-2624): of the leads this site captured in the\n * period, how many are still new, being worked, qualified or unqualified —\n * and why the unqualified ones were closed.\n *\n * A cohort by first sighting, followed to where each lead stands now,\n * because that is the question a period asks of leads — \"what became of\n * the ones we got\" — and because a lead's status has no history on the\n * document: only its current value. The window is a range on\n * `firstSeenAtMs`, which the capture door stamps once on creation and\n * never again, so a returning visitor's second form is not a second lead\n * in a second period. A single-field range needs no composite index.\n *\n * Read directly off `hosts/{hostId}/leads` under the site's own rules —\n * any member of the site may read it — the way the Leads section reads\n * it, so the funnel and the list can never disagree about who may see a\n * lead.\n */\nfunction SiteLeadFunnelCard(props: { report: CrmReportScope; hostId: string }) {\n const { report, hostId } = props\n const { range } = report\n const firestore = useFirestore()\n\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], report.tokens, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:counts:${hostId}`) },\n )\n const window = useWindowRead<LeadRow>(\n () =>\n query(\n leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n LEAD_CEILING,\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:window:${hostId}`) },\n )\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n (figures !== null ? ` of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'This site’s leads could not be read.'}\n />\n )\n}\nSiteLeadFunnelCard.displayName = 'SiteLeadFunnelCard'\n\n/**\n * THE ORGANIZATION'S FUNNEL (AGL-2634, AGL-3275): every lead the org holds.\n *\n * This totaled one aggregate and one window PER SITE, because the sites'\n * lead collections were disjoint. They share one collection now, so a sum\n * would count a lead that two brands in a consent group both hold once for\n * each of them — and the merge that existed so a busy site could not crowd\n * out a quiet one has nothing left to balance. One aggregate and one\n * bounded read, unscoped, as an org-wide member's reads are.\n */\nfunction OrgLeadFunnelCard(props: { report: CrmReportScope }) {\n const { report } = props\n const { range } = report\n const firestore = useFirestore()\n const mount = useCrmOrgMount()\n const hostIds = useMemo(\n () => (mount?.hostsReady ? mount.hosts.map((host) => host.id) : null),\n [mount],\n )\n const key = hostIds?.join('\\n') ?? ''\n\n /*\n * ONE READ, not one per site (AGL-3275). The sites share a collection now,\n * so a lead two brands in a consent group both hold would be counted once\n * for each of them — and the org's funnel would report more leads than the\n * org has. `null` tokens is the org-wide read that carries no clause.\n */\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], null, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], null, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:counts:org') },\n )\n const merged = useAggregateRead<{ rows: LeadRow[]; truncated: boolean }>(\n () =>\n getDocs(\n query(\n leadsBetween(firestore, report.scope[1], null, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n ).then((snapshot) =>\n ceilingedWindow(\n snapshot.docs.map(\n (document) => ({ ...document.data(), $id: document.id }) as unknown as LeadRow,\n ),\n LEAD_CEILING,\n ),\n ),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:window:org') },\n )\n const window = useMemo<WindowRead<LeadRow>>(\n () => ({\n rows: merged.value?.rows ?? [],\n truncated: merged.value?.truncated ?? false,\n status: merged.status,\n }),\n [merged],\n )\n const sites = hostIds?.length ?? 0\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n subheader={`Every site (${sites.toLocaleString()})`}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n ` — at most ${LEAD_CEILING.toLocaleString()}` +\n (figures !== null ? ` — of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'The organization’s leads could not be read.'}\n />\n )\n}\nOrgLeadFunnelCard.displayName = 'OrgLeadFunnelCard'\n\n/**\n * The funnel as drawn, whichever level fed it: four tiles, the statuses,\n * the reasons, the export. Pure over the two reads.\n */\nfunction LeadFunnelBody(props: {\n report: CrmReportScope\n subheader?: string\n captured: AggregateRead<CapturedFigures>\n window: WindowRead<LeadRow>\n caption?: string\n errorText: string\n}) {\n const { report, subheader, captured, window, caption, errorText } = props\n const { period, routes } = report\n const status = window.status\n\n const funnel = useMemo(() => Aglyn.leadFunnel(window.rows), [window])\n const read = status === 'success'\n const figures = captured.value\n const share = (count: number, of: number): string | null =>\n of > 0 ? percent(count / of) : null\n const reasons = funnel.reasons.slice(0, REASONS_SHOWN)\n const moreReasons = funnel.reasons.length - reasons.length\n\n return (\n <CardDisplay\n header={'Lead funnel'}\n subheader={subheader}\n help={LEAD_FUNNEL_HELP}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('leads')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open leads'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Leads captured'}\n value={figures ? figures.current.toLocaleString() : null}\n deltaPct={figures ? Aglyn.deltaPercent(figures.current, figures.previous) : null}\n note={'first seen in the period'}\n href={routes.section('leads')}\n />\n <ReportStatTile\n label={'Qualified'}\n value={read ? funnel.byStatus.qualified.toLocaleString() : null}\n note={\n funnel.qualifiedRate === null\n ? read\n ? 'nobody captured in the period'\n : undefined\n : `${percent(funnel.qualifiedRate)} of those captured`\n }\n />\n <ReportStatTile\n label={'Unqualified'}\n value={read ? funnel.byStatus.unqualified.toLocaleString() : null}\n note={\n funnel.unqualifiedRate === null\n ? undefined\n : `${percent(funnel.unqualifiedRate)} of those captured`\n }\n riseIsGood={false}\n />\n <ReportStatTile\n label={'Still open'}\n value={read ? funnel.open.toLocaleString() : null}\n note={'new or being worked'}\n href={routes.section('leads')}\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{errorText}</Alert>\n ) : null}\n <Section title={'Where they stand'}>\n <ReportBreakdown\n rows={Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => ({\n key: leadStatus,\n label: Aglyn.CRM_LEAD_STATUS_LABELS[leadStatus],\n value: funnel.byStatus[leadStatus],\n note: share(funnel.byStatus[leadStatus], funnel.total) ?? undefined,\n color:\n leadStatus === 'qualified'\n ? 'success.main'\n : leadStatus === 'unqualified'\n ? 'warning.main'\n : undefined,\n }))}\n emptyText={''}\n />\n {read && !funnel.total ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No leads captured in this period.'}\n </Typography>\n ) : null}\n </Section>\n <Section title={'Why leads were unqualified'}>\n <ReportBreakdown\n rows={reasons.map((reason) => ({\n key: reason.key,\n label: reason.label,\n value: reason.count,\n note: share(reason.count, funnel.byStatus.unqualified) ?? undefined,\n color: 'warning.main',\n }))}\n emptyText={\n status === 'loading' ? 'Reading…' : 'No lead was unqualified in this period.'\n }\n />\n {moreReasons > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`And ${plural(moreReasons, 'other reason')}, each given once or twice.`}\n </Typography>\n ) : null}\n </Section>\n <ReportExport\n filename={reportFilename('lead-funnel', period)}\n columns={COLUMNS}\n rows={() => [\n ...Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => [\n 'Status',\n Aglyn.CRM_LEAD_STATUS_LABELS[leadStatus],\n funnel.byStatus[leadStatus],\n share(funnel.byStatus[leadStatus], funnel.total) ?? '',\n ]),\n ...funnel.reasons.map((reason) => [\n 'Unqualified reason',\n reason.label,\n reason.count,\n share(reason.count, funnel.byStatus.unqualified) ?? '',\n ]),\n ]}\n disabled={!read || !funnel.total}\n caption={caption}\n />\n </Stack>\n </CardDisplay>\n )\n}\nLeadFunnelBody.displayName = 'LeadFunnelBody'\nLeadFunnelCard.displayName = 'LeadFunnelCard'\n\nexport default LeadFunnelCard\n"],"names":["Aglyn","AppLink","CardDisplay","percent","Section","ceilingedWindow","Alert","Button","Stack","Typography","collection","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","useCrmOrgMount","ReportBreakdown","ReportExport","plural","reportFilename","reportCacheKey","ReportStatTile","useAggregateRead","useWindowRead","LEAD_CEILING","REASONS_SHOWN","COLUMNS","LEAD_FUNNEL_HELP","pluginDocsHelp","anchor","excerpt","leadsBetween","firestore","orgId","visibleTo","from","to","countOf","target","then","snapshot","data","count","LeadFunnelCard","props","report","hostId","mount","SiteLeadFunnelCard","hostsReady","hosts","length","OrgLeadFunnelCard","header","help","contentGutterX","contentGutterY","variant","color","range","captured","Promise","all","scope","tokens","previousFrom","previousTo","current","previous","cacheKey","window","figures","value","sampled","status","truncated","rows","LeadFunnelBody","caption","toLocaleString","undefined","errorText","displayName","hostIds","map","host","id","key","join","merged","docs","document","$id","sites","subheader","period","routes","funnel","leadFunnel","read","share","of","reasons","slice","moreReasons","HeaderProps","action","component","componentVariant","nativeButton","href","section","size","spacing","direction","sx","flexWrap","label","deltaPct","deltaPercent","note","byStatus","qualified","qualifiedRate","unqualified","unqualifiedRate","riseIsGood","open","severity","title","CRM_LEAD_STATUSES","leadStatus","CRM_LEAD_STATUS_LABELS","total","emptyText","reason","filename","columns","disabled"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/lead-funnel-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ceilingedWindow } from '@aglyn/tenant-feature-instance/hooks/host-collection-queries'\nimport { Alert, Button, Stack, Typography } from '@mui/material'\nimport {\n collection,\n type Firestore,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useCrmOrgMount } from '../../hooks/use-crm-org-mount'\nimport { ReportBreakdown } from './report-breakdown'\nimport { ReportExport } from './report-export'\nimport { plural, reportFilename } from './report-format'\nimport { type CrmReportScope, reportCacheKey } from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport {\n type AggregateRead,\n useAggregateRead,\n useWindowRead,\n type WindowRead,\n} from './use-aggregate-read'\n\n/**\n * How many of the period's leads the funnel is placed from — the Leads\n * section's own window, over the period rather than over everything.\n *\n * The captured count is a server aggregate and never meets this bound; the\n * funnel needs each lead's status and reason, so it reads the period's\n * newest two hundred and says when there were more. At the organization\n * level the bound is PER SITE (AGL-2634): each site's window is its own,\n * and a site with a thousand leads must not crowd out a site with ten.\n */\nexport const LEAD_CEILING = 200\n/** How many reasons the bar list draws; the rest are counted beneath it. */\nconst REASONS_SHOWN = 8\n\n/** The CSV's columns: one file for both the statuses and the reasons. */\nconst COLUMNS = ['Kind', 'Label', 'Count', 'Share'] as const\n\ntype LeadRow = Record<string, unknown> & Aglyn.CrmLeadFields & { $id: string }\n\n/** The period's captured count, and the previous period's for the delta. */\ninterface CapturedFigures {\n current: number\n previous: number\n}\n\nexport interface LeadFunnelCardProps {\n report: CrmReportScope\n /**\n * The site whose leads are placed. A lead lives under its host, not the\n * org — `hosts/{hostId}/leads`, private by path, no `visibleTo` — so the\n * report's org scope cannot reach it and the card is told the site. At\n * the organization level `null` (AGL-2630): the card then reads every\n * site the mount lists, one window each (AGL-2634).\n */\n hostId: string | null\n}\n\nconst LEAD_FUNNEL_HELP = Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#lead-funnel',\n excerpt:\n 'Of the leads this site captured in the period, how many are ' +\n 'still new, being worked, qualified or unqualified — and the ' +\n 'reasons the unqualified ones were closed. Placed by when each ' +\n 'lead was first seen.',\n})\n\n/**\n * The leads a scope may see, first seen in a range (AGL-3275).\n *\n * `visibleTo` is the site's tokens under a site, and `null` at the\n * organization level where an org-wide member reads without a clause. The\n * range stays a single-field one; with the scope clause it composes with the\n * `visibleTo` + `firstSeenAtMs` entry in `cloud/firebase-firestore.indexes.json`.\n */\nfunction leadsBetween(\n firestore: Firestore,\n orgId: string,\n visibleTo: readonly string[] | null,\n from: number,\n to: number,\n) {\n return query(\n collection(firestore, 'orgs', orgId, 'leads'),\n ...(visibleTo ? [where('visibleTo', 'array-contains-any', [...visibleTo])] : []),\n where('firstSeenAtMs', '>=', from),\n where('firstSeenAtMs', '<', to),\n )\n}\n\nconst countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n/**\n * The lead funnel, at whichever level the report is mounted (AGL-2624,\n * AGL-2630, AGL-2634).\n *\n * Under a site: that site. At the organization level: every site the\n * mount lists, one window each, totaled — the org hub's reports total\n * every site, and the leads are no exception now that the fan-out is\n * bounded by the org's site list. With no sites to read the card says so\n * instead of guessing.\n */\nexport function LeadFunnelCard(props: LeadFunnelCardProps) {\n const { report, hostId } = props\n const mount = useCrmOrgMount()\n if (hostId) return <SiteLeadFunnelCard report={report} hostId={hostId} />\n if (mount && (!mount.hostsReady || mount.hosts.length > 0)) {\n return <OrgLeadFunnelCard report={report} />\n }\n return (\n <CardDisplay header={'Lead funnel'} help={LEAD_FUNNEL_HELP} contentGutterX contentGutterY>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {mount\n ? 'Leads live under a site, and this organization has no sites yet.'\n : 'Leads live under a site, and this report is mounted under none.'}\n </Typography>\n </CardDisplay>\n )\n}\n\n/**\n * The lead funnel (AGL-2624): of the leads this site captured in the\n * period, how many are still new, being worked, qualified or unqualified —\n * and why the unqualified ones were closed.\n *\n * A cohort by first sighting, followed to where each lead stands now,\n * because that is the question a period asks of leads — \"what became of\n * the ones we got\" — and because a lead's status has no history on the\n * document: only its current value. The window is a range on\n * `firstSeenAtMs`, which the capture door stamps once on creation and\n * never again, so a returning visitor's second form is not a second lead\n * in a second period. A single-field range needs no composite index.\n *\n * Read directly off `hosts/{hostId}/leads` under the site's own rules —\n * any member of the site may read it — the way the Leads section reads\n * it, so the funnel and the list can never disagree about who may see a\n * lead.\n */\nfunction SiteLeadFunnelCard(props: { report: CrmReportScope; hostId: string }) {\n const { report, hostId } = props\n const { range } = report\n const firestore = useFirestore()\n\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], report.tokens, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:counts:${hostId}`) },\n )\n const window = useWindowRead<LeadRow>(\n () =>\n query(\n leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n LEAD_CEILING,\n [firestore, hostId, range],\n { cacheKey: reportCacheKey(report, `leads:window:${hostId}`) },\n )\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n (figures !== null ? ` of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'This site’s leads could not be read.'}\n />\n )\n}\nSiteLeadFunnelCard.displayName = 'SiteLeadFunnelCard'\n\n/**\n * THE ORGANIZATION'S FUNNEL (AGL-2634, AGL-3275): every lead the org holds.\n *\n * This totaled one aggregate and one window PER SITE, because the sites'\n * lead collections were disjoint. They share one collection now, so a sum\n * would count a lead that two brands in a consent group both hold once for\n * each of them — and the merge that existed so a busy site could not crowd\n * out a quiet one has nothing left to balance. One aggregate and one\n * bounded read, unscoped, as an org-wide member's reads are.\n */\nfunction OrgLeadFunnelCard(props: { report: CrmReportScope }) {\n const { report } = props\n const { range } = report\n const firestore = useFirestore()\n const mount = useCrmOrgMount()\n const hostIds = useMemo(\n () => (mount?.hostsReady ? mount.hosts.map((host) => host.id) : null),\n [mount],\n )\n const key = hostIds?.join('\\n') ?? ''\n\n /*\n * ONE READ, not one per site (AGL-3275). The sites share a collection now,\n * so a lead two brands in a consent group both hold would be counted once\n * for each of them — and the org's funnel would report more leads than the\n * org has. `null` tokens is the org-wide read that carries no clause.\n */\n const captured = useAggregateRead<CapturedFigures>(\n () =>\n Promise.all([\n countOf(leadsBetween(firestore, report.scope[1], null, range.from, range.to)),\n countOf(\n leadsBetween(firestore, report.scope[1], null, range.previousFrom, range.previousTo),\n ),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:counts:org') },\n )\n const merged = useAggregateRead<{ rows: LeadRow[]; truncated: boolean }>(\n () =>\n getDocs(\n query(\n leadsBetween(firestore, report.scope[1], null, range.from, range.to),\n orderBy('firstSeenAtMs', 'desc'),\n limit(LEAD_CEILING + 1),\n ),\n ).then((snapshot) =>\n ceilingedWindow(\n snapshot.docs.map(\n (document) => ({ ...document.data(), $id: document.id }) as unknown as LeadRow,\n ),\n LEAD_CEILING,\n ),\n ),\n [firestore, report.scope, range],\n { cacheKey: reportCacheKey(report, 'leads:window:org') },\n )\n const window = useMemo<WindowRead<LeadRow>>(\n () => ({\n rows: merged.value?.rows ?? [],\n truncated: merged.value?.truncated ?? false,\n status: merged.status,\n }),\n [merged],\n )\n const sites = hostIds?.length ?? 0\n const figures = captured.value\n const sampled =\n window.status === 'success' &&\n (window.truncated || (figures !== null && figures.current > window.rows.length))\n return (\n <LeadFunnelBody\n report={report}\n subheader={`Every site (${sites.toLocaleString()})`}\n captured={captured}\n window={window}\n caption={\n sampled\n ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` +\n ` — at most ${LEAD_CEILING.toLocaleString()}` +\n (figures !== null ? ` — of ${figures.current.toLocaleString()}` : '') +\n ' in the period; the captured tile is counted on the server.'\n : undefined\n }\n errorText={'The organization’s leads could not be read.'}\n />\n )\n}\nOrgLeadFunnelCard.displayName = 'OrgLeadFunnelCard'\n\n/**\n * The funnel as drawn, whichever level fed it: four tiles, the statuses,\n * the reasons, the export. Pure over the two reads.\n */\nfunction LeadFunnelBody(props: {\n report: CrmReportScope\n subheader?: string\n captured: AggregateRead<CapturedFigures>\n window: WindowRead<LeadRow>\n caption?: string\n errorText: string\n}) {\n const { report, subheader, captured, window, caption, errorText } = props\n const { period, routes } = report\n const status = window.status\n\n const funnel = useMemo(() => Aglyn.leadFunnel(window.rows), [window])\n const read = status === 'success'\n const figures = captured.value\n const share = (count: number, of: number): string | null =>\n of > 0 ? percent(count / of) : null\n const reasons = funnel.reasons.slice(0, REASONS_SHOWN)\n const moreReasons = funnel.reasons.length - reasons.length\n\n return (\n <CardDisplay\n header={'Lead funnel'}\n subheader={subheader}\n help={LEAD_FUNNEL_HELP}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n <ReportExport\n filename={reportFilename('lead-funnel', period)}\n columns={COLUMNS}\n rows={() => [\n ...Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => [\n 'Status',\n Aglyn.CRM_LEAD_STATUS_LABELS[leadStatus],\n funnel.byStatus[leadStatus],\n share(funnel.byStatus[leadStatus], funnel.total) ?? '',\n ]),\n ...funnel.reasons.map((reason) => [\n 'Unqualified reason',\n reason.label,\n reason.count,\n share(reason.count, funnel.byStatus.unqualified) ?? '',\n ]),\n ]}\n disabled={!read || !funnel.total}\n />\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.section('leads')}\n size=\"small\"\n color=\"primary\"\n >\n {'Open leads'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Leads captured'}\n value={figures ? figures.current.toLocaleString() : null}\n deltaPct={figures ? Aglyn.deltaPercent(figures.current, figures.previous) : null}\n note={'first seen in the period'}\n href={routes.section('leads')}\n />\n <ReportStatTile\n label={'Qualified'}\n value={read ? funnel.byStatus.qualified.toLocaleString() : null}\n note={\n funnel.qualifiedRate === null\n ? read\n ? 'nobody captured in the period'\n : undefined\n : `${percent(funnel.qualifiedRate)} of those captured`\n }\n />\n <ReportStatTile\n label={'Unqualified'}\n value={read ? funnel.byStatus.unqualified.toLocaleString() : null}\n note={\n funnel.unqualifiedRate === null\n ? undefined\n : `${percent(funnel.unqualifiedRate)} of those captured`\n }\n riseIsGood={false}\n />\n <ReportStatTile\n label={'Still open'}\n value={read ? funnel.open.toLocaleString() : null}\n note={'new or being worked'}\n href={routes.section('leads')}\n />\n </Stack>\n {captured.status === 'error' || status === 'error' ? (\n <Alert severity=\"warning\">{errorText}</Alert>\n ) : null}\n <Section title={'Where they stand'}>\n <ReportBreakdown\n rows={Aglyn.CRM_LEAD_STATUSES.map((leadStatus) => ({\n key: leadStatus,\n label: Aglyn.CRM_LEAD_STATUS_LABELS[leadStatus],\n value: funnel.byStatus[leadStatus],\n note: share(funnel.byStatus[leadStatus], funnel.total) ?? undefined,\n color:\n leadStatus === 'qualified'\n ? 'success.main'\n : leadStatus === 'unqualified'\n ? 'warning.main'\n : undefined,\n }))}\n emptyText={''}\n />\n {read && !funnel.total ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No leads captured in this period.'}\n </Typography>\n ) : null}\n </Section>\n <Section title={'Why leads were unqualified'}>\n <ReportBreakdown\n rows={reasons.map((reason) => ({\n key: reason.key,\n label: reason.label,\n value: reason.count,\n note: share(reason.count, funnel.byStatus.unqualified) ?? undefined,\n color: 'warning.main',\n }))}\n emptyText={\n status === 'loading' ? 'Reading…' : 'No lead was unqualified in this period.'\n }\n />\n {moreReasons > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`And ${plural(moreReasons, 'other reason')}, each given once or twice.`}\n </Typography>\n ) : null}\n </Section>\n {/* What the funnel, and so the export, is placed from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {caption}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadFunnelBody.displayName = 'LeadFunnelBody'\nLeadFunnelCard.displayName = 'LeadFunnelCard'\n\nexport default LeadFunnelCard\n"],"names":["Aglyn","AppLink","CardDisplay","percent","Section","ceilingedWindow","Alert","Button","Stack","Typography","collection","getCountFromServer","getDocs","limit","orderBy","query","where","useMemo","useFirestore","useCrmOrgMount","ReportBreakdown","ReportExport","plural","reportFilename","reportCacheKey","ReportStatTile","useAggregateRead","useWindowRead","LEAD_CEILING","REASONS_SHOWN","COLUMNS","LEAD_FUNNEL_HELP","pluginDocsHelp","anchor","excerpt","leadsBetween","firestore","orgId","visibleTo","from","to","countOf","target","then","snapshot","data","count","LeadFunnelCard","props","report","hostId","mount","SiteLeadFunnelCard","hostsReady","hosts","length","OrgLeadFunnelCard","header","help","contentGutterX","contentGutterY","variant","color","range","captured","Promise","all","scope","tokens","previousFrom","previousTo","current","previous","cacheKey","window","figures","value","sampled","status","truncated","rows","LeadFunnelBody","caption","toLocaleString","undefined","errorText","displayName","hostIds","map","host","id","key","join","merged","docs","document","$id","sites","subheader","period","routes","funnel","leadFunnel","read","share","of","reasons","slice","moreReasons","HeaderProps","action","direction","spacing","filename","columns","CRM_LEAD_STATUSES","leadStatus","CRM_LEAD_STATUS_LABELS","byStatus","total","reason","label","unqualified","disabled","component","componentVariant","nativeButton","href","section","size","sx","flexWrap","deltaPct","deltaPercent","note","qualified","qualifiedRate","unqualifiedRate","riseIsGood","open","severity","title","emptyText"],"mappings":"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"}
|