@aglyn/plugins-crm 1.0.0-beta.165 → 1.0.0-beta.166

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/contact-associations-card.js +7 -1
  3. package/src/lib/components/contact-associations-card.js.map +1 -1
  4. package/src/lib/components/crm-console-page.js +12 -7
  5. package/src/lib/components/crm-console-page.js.map +1 -1
  6. package/src/lib/components/crm-glance-card.js +4 -2
  7. package/src/lib/components/crm-glance-card.js.map +1 -1
  8. package/src/lib/components/lead-campaigns-card.js +7 -1
  9. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  10. package/src/lib/components/lead-detail-page.js +1 -1
  11. package/src/lib/components/lead-detail-page.js.map +1 -1
  12. package/src/lib/components/lead-properties-card.js +1 -1
  13. package/src/lib/components/lead-properties-card.js.map +1 -1
  14. package/src/lib/components/lead-unqualify-dialog.js +7 -1
  15. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  16. package/src/lib/components/leads-bulk-bar.d.ts +1 -2
  17. package/src/lib/components/leads-bulk-bar.js +8 -3
  18. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  19. package/src/lib/components/leads-section.js +27 -15
  20. package/src/lib/components/leads-section.js.map +1 -1
  21. package/src/lib/components/reports/lead-counts.d.ts +16 -9
  22. package/src/lib/components/reports/lead-counts.js +29 -18
  23. package/src/lib/components/reports/lead-counts.js.map +1 -1
  24. package/src/lib/components/reports/lead-funnel-card.js +44 -40
  25. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  26. package/src/lib/hooks/use-org-leads.d.ts +29 -32
  27. package/src/lib/hooks/use-org-leads.js +58 -68
  28. package/src/lib/hooks/use-org-leads.js.map +1 -1
  29. package/src/lib/model/crm-routes.d.ts +9 -6
  30. package/src/lib/model/crm-routes.js +9 -6
  31. package/src/lib/model/crm-routes.js.map +1 -1
  32. package/src/lib/server/capture-contact.js +11 -5
  33. package/src/lib/server/capture-contact.js.map +1 -1
  34. package/src/lib/server/convert-open-lead.js +6 -2
  35. package/src/lib/server/convert-open-lead.js.map +1 -1
  36. package/src/lib/server/email-send.js +3 -3
  37. package/src/lib/server/email-send.js.map +1 -1
  38. package/src/lib/server/erase-person.js +25 -7
  39. package/src/lib/server/erase-person.js.map +1 -1
  40. package/src/lib/server/lead-campaign-carry.js +3 -2
  41. package/src/lib/server/lead-campaign-carry.js.map +1 -1
  42. package/src/lib/server/lead-create.js +8 -6
  43. package/src/lib/server/lead-create.js.map +1 -1
  44. package/src/lib/server/leads-import.js +2 -2
  45. package/src/lib/server/leads-import.js.map +1 -1
  46. package/src/lib/server/record-email-state.js +10 -0
  47. package/src/lib/server/record-email-state.js.map +1 -1
  48. package/src/lib/server/record-facts.js +3 -3
  49. package/src/lib/server/record-facts.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/leads-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type {\n ConsolePluginPageProps,\n CrmLeadFields,\n CrmLeadStatus,\n} from '@aglyn/aglyn'\nimport {\n mdiAccountArrowRight,\n mdiAccountCancelOutline,\n mdiAccountConvertOutline,\n mdiAccountTieOutline,\n mdiMagnify,\n} from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n type OrgMemberOptions,\n useOrgMemberOptions,\n} from '../hooks/use-org-member-options'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { customFieldColumns } from './contact-custom-columns'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport { useOrgLeads } from '../hooks/use-org-leads'\nimport CrmViewsControl from './crm-views-control'\nimport RowActionsMenu from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useHostCampaigns,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Box,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControl,\n InputAdornment,\n InputLabel,\n MenuItem,\n Select,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n deleteField,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n updateDoc,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useId, useMemo, useState } from 'react'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n LEAD_FILTER_LABELS,\n LEAD_EMAIL_FILTER_LABELS,\n LEAD_EMAIL_FILTERS,\n LEAD_FILTERS,\n type LeadEmailFilter,\n type LeadFilter,\n leadMatchesCampaignFilter,\n leadMatchesEmailFilter,\n leadMatchesFilter,\n leadMatchesSearch,\n} from '../model/lead-filters'\nimport { type LeadCsvOptions, leadsCsv } from '../model/leads-csv'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport {\n leadSourceLabel,\n leadSources,\n leadTimeLabel,\n} from './lead-history-card'\nimport { LeadImportButton } from './lead-import-drawer'\nimport NewLeadDrawer, { type NewLeadValues } from './new-lead-drawer'\nimport { useCrmApi } from './use-crm-api'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { CONVERT_PENDING_ERASURE_REASON } from './lead-properties-card'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { LeadStatusChip } from './lead-status-chip'\nimport LeadSurfacesNote from './lead-surfaces-note'\nimport { LeadUnqualifyDialog } from './lead-unqualify-dialog'\nimport LeadsBulkBar from './leads-bulk-bar'\nimport OrgLeadSurfacesNote from './org-lead-surfaces-note'\n\n/**\n * How many leads the section reads: the newest by last seen, plus one probe\n * row so \"there are more\" is a fact rather than a guess at the boundary.\n *\n * A CEILING and a client-side filter rather than a paged status query, and\n * the reason is the lead documents themselves. Every lead the capture door\n * writes carries NO `status` — the field exists only once somebody in the\n * CRM has touched the lead — and Firestore cannot select documents by a\n * field's absence: `where('status','in',[…])`, `!=` and `not-in` all skip a\n * document without the field. A server-side \"open leads\" query would\n * therefore hide every lead nobody has worked yet, which is the entire\n * population the section exists to show on the day it ships. So the query\n * is the one order every lead can satisfy (`lastSeenAtMs`, stamped on every\n * capture), and the status filter narrows the loaded window — said out loud\n * beneath the table whenever the window is not the whole collection.\n *\n * The window is then PAGED in memory under the shared footer, the way the\n * workspace pickers page a slice of a window they cannot re-key: the rows\n * are already in the snapshot, so turning a page costs nothing, and the\n * footer's count is exact because it counts the filtered window rather than\n * a collection nobody has measured.\n */\nconst LEADS_WINDOW = 200\n\n/**\n * One row of the list. `$id` keys the grid — the document id under a site,\n * `{hostId}/{leadId}` at the organization level, where a lead's id is a\n * person key the same on every site that met the person — and `leadId`\n * and `hostId` say which document, under which site, a write or a link\n * names.\n */\ntype LeadRow = Record<string, unknown> &\n CrmLeadFields & { $id: string; leadId: string; hostId: string }\n\n/**\n * `/crm/leads` — the people a site has met but not yet qualified (AGL-2608).\n *\n * A section of its own, the way Salesforce keeps Leads apart from Contacts:\n * a lead is a capture — a form, a booking, a sign-up — that somebody has\n * still to work, and it converts into a contact, a company and a deal when\n * it is real. Reads `hosts/{hostId}/leads`, host-scoped by path, so there is\n * no `visibleTo` filter; the Firestore rules admit any member of the site to\n * read it and an admin, editor or author to update it, which is what makes\n * the inline status and owner changes client-direct writes.\n *\n * At the ORGANIZATION level (AGL-2630) there is no one site to read: the\n * section opens the same query under every site the org has (`useOrgLeads`)\n * and lists the merged window with a Site column, every row naming the site\n * its writes and its link go to. The per-site notes — which of a site's\n * forms file a lead — belong to a site's own hub and are not drawn here.\n */\nexport function CrmLeadsSection(props: ConsolePluginPageProps) {\n const { hostId, org, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, createHostId } = useCrmScope({ hostId, org })\n const mount = useCrmOrgMount()\n const roster = useOrgMemberOptions(orgId)\n // The org's lead fields, for the optional columns below (AGL-3272).\n const leadFields = useContactFieldDefinitions(orgId, 'lead')\n const routes = crmRoutes(basePath ?? '')\n\n // Under a site: the site's own window, rows keyed by document id.\n const site = useFirestoreCollection<\n Record<string, unknown> & CrmLeadFields & { $id: string }\n >(\n () =>\n hostId\n ? query(\n collection(firestore, 'hosts', hostId, 'leads'),\n orderBy('lastSeenAtMs', 'desc'),\n limit(LEADS_WINDOW + 1),\n )\n : null,\n [firestore, hostId],\n { idField: '$id' },\n )\n // At the organization level: every site's window, merged.\n const orgHostIds = useMemo(\n () => (hostId ? [] : (mount?.hosts ?? []).map((host) => host.id)),\n [hostId, mount?.hosts],\n )\n const orgLeads = useOrgLeads({\n hostIds: orgHostIds,\n windowSize: LEADS_WINDOW,\n })\n const leadDocs = useMemo<LeadRow[]>(\n () =>\n hostId\n ? site.data.map((row) => ({ ...row, leadId: row.$id, hostId }))\n : orgLeads.data,\n [hostId, site.data, orgLeads.data],\n )\n const status = hostId\n ? site.status\n : mount?.hostsReady && !orgHostIds.length\n ? 'success'\n : orgLeads.status\n const truncated = hostId ? leadDocs.length > LEADS_WINDOW : orgLeads.truncated\n const window = useMemo(() => leadDocs.slice(0, LEADS_WINDOW), [leadDocs])\n\n /*\n * The `Show` filter is the saved VIEW'S (AGL-2617): a saved view of leads\n * holds the status beside the columns and the sort, and the select below\n * writes into it. Unset reads as `open`, which is what the section opened\n * on before views existed and the one reading a query cannot express.\n */\n const views = useCrmSavedView({\n section: 'leads',\n hostId,\n org: props.org,\n basePath: basePath ?? '',\n })\n const filter: LeadFilter = useMemo(() => {\n const value = views.state.filters.find(\n (clause) => clause.field === 'status',\n )?.value\n return (LEAD_FILTERS as readonly string[]).includes(value ?? '')\n ? (value as LeadFilter)\n : 'open'\n }, [views.state.filters])\n /*\n * The `Email` filter (AGL-3245) is the view's too, as an `emailState`\n * clause beside the status one. Each setter keeps the other's clause:\n * narrowing to bounced leads does not reopen the unqualified ones.\n */\n const emailFilter: LeadEmailFilter = useMemo(() => {\n const value = views.state.filters.find(\n (clause) => clause.field === 'emailState',\n )?.value\n return (LEAD_EMAIL_FILTERS as readonly string[]).includes(value ?? '')\n ? (value as LeadEmailFilter)\n : 'any'\n }, [views.state.filters])\n const setFilter = useCallback(\n (next: LeadFilter) =>\n views.setFilters([\n ...views.state.filters.filter((clause) => clause.field !== 'status'),\n ...(next === 'open'\n ? []\n : [{ field: 'status', op: 'equals', value: next }]),\n ]),\n [views.setFilters, views.state.filters],\n )\n const setEmailFilter = useCallback(\n (next: LeadEmailFilter) =>\n views.setFilters([\n ...views.state.filters.filter(\n (clause) => clause.field !== 'emailState',\n ),\n ...(next === 'any'\n ? []\n : [{ field: 'emailState', op: 'equals', value: next }]),\n ]),\n [views.setFilters, views.state.filters],\n )\n /*\n * The `Campaign` filter (AGL-3254) is the view's too, as a `campaignIds`\n * clause: the id of one of the site's campaign containers, resolved to\n * its name from the containers themselves — ids only in storage, so a\n * renamed campaign keeps its leads. Under a site alone: a campaign\n * belongs to one site, and the organization-level list spans them all.\n */\n const campaignFilter = useMemo(\n () =>\n views.state.filters.find((clause) => clause.field === 'campaignIds')\n ?.value ?? '',\n [views.state.filters],\n )\n const setCampaignFilter = useCallback(\n (next: string) =>\n views.setFilters([\n ...views.state.filters.filter(\n (clause) => clause.field !== 'campaignIds',\n ),\n ...(next\n ? [{ field: 'campaignIds', op: 'contains', value: next }]\n : []),\n ]),\n [views.setFilters, views.state.filters],\n )\n const campaigns = useHostCampaigns(hostId, { enabled: Boolean(hostId) })\n const campaignName = useCallback(\n (id: string) =>\n campaigns.options.find((option) => option.value === id)?.label ?? id,\n [campaigns.options],\n )\n // The label's id, so the filter's combobox is named \"Show\" rather than\n // after the option it shows — see `LeadOwnerSelect`.\n const filterLabelId = useId()\n const emailFilterLabelId = useId()\n const campaignFilterLabelId = useId()\n /*\n * The search box is the SECTION'S, not the grid's (AGL-3246). The grid's\n * quick filter runs over the rows the grid holds, and the grid holds one\n * PAGE of the window — so a lead on page three answered \"no match\" while\n * the footer below went on counting the unfiltered window. The term\n * narrows the whole loaded window here, beside the status filter and\n * before the footer's count and the page slice, over the fields a person\n * types to find a lead: name, email, company, title and tags.\n */\n const [search, setSearch] = useState('')\n const rows = useMemo(\n () =>\n window.filter(\n (lead) =>\n leadMatchesFilter(lead, filter) &&\n leadMatchesEmailFilter(lead, emailFilter) &&\n leadMatchesCampaignFilter(lead, campaignFilter) &&\n leadMatchesSearch(lead, search),\n ),\n [window, filter, emailFilter, campaignFilter, search],\n )\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n // A new filter or search term starts on page one: page three of the open\n // leads is not a page of the unqualified ones, and an out-of-range page\n // renders empty.\n useEffect(() => {\n setPage(0)\n }, [filter, emailFilter, campaignFilter, search])\n const pageRows = useMemo(\n () => rows.slice(page * pageSize, (page + 1) * pageSize),\n [rows, page, pageSize],\n )\n\n /*\n * The ticked rows, for the bulk bar (AGL-2662). Cleared when the filter or\n * the search term changes: a selection made on the open leads is not a\n * selection of the unqualified ones, and the bar's count would be over\n * rows no longer listed.\n */\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n useEffect(\n () => setSelectedIds([]),\n [filter, emailFilter, campaignFilter, search],\n )\n // How the file names the owner and, at the org level, the site.\n const csvOptions: LeadCsvOptions = useMemo(\n () => ({\n ownerEmail: roster.emailFor,\n ...(hostId ? {} : { siteName: (id: string) => mount?.siteName(id) }),\n }),\n [roster.emailFor, hostId, mount],\n )\n // The listed window — every row the filter and the search admit, not just\n // the page.\n const handleExport = useCallback(() => {\n downloadTextFile('leads.csv', 'text/csv', leadsCsv(rows, csvOptions))\n }, [rows, csvOptions])\n\n const [assigning, setAssigning] = useState<LeadRow | null>(null)\n const [unqualifying, setUnqualifying] = useState<LeadRow | null>(null)\n // The row whose conversion dialog is open (AGL-2641) — the same dialog\n // the lead's page opens, fed the row so the list is one click shorter.\n const [converting, setConverting] = useState<LeadRow | null>(null)\n\n /*==========================================\n * NEW LEAD (AGL-3231) — Salesforce's New Lead, in a drawer over the\n * list. The route files the lead through the one lead door under the\n * mounted site, or at the organization level under the site the drawer's\n * picker named; the drawer holds its submit until one is known. It makes\n * a lead and nothing else — the conversion is what makes the contact.\n *=========================================*/\n const crmApi = useCrmApi(createHostId)\n const [createOpen, setCreateOpen] = useState(false)\n const [createBusy, setCreateBusy] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const handleCreate = useCallback(\n async (values: NewLeadValues) => {\n setCreateBusy(true)\n setCreateError(null)\n try {\n const { response, payload } = await crmApi('leads-create', {\n email: values.email,\n ...(values.name ? { name: values.name } : {}),\n ...(values.company ? { company: values.company } : {}),\n ...(values.jobTitle ? { jobTitle: values.jobTitle } : {}),\n ...(values.phone ? { phone: values.phone } : {}),\n ...(values.website ? { website: values.website } : {}),\n ...(values.leadSource ? { leadSource: values.leadSource } : {}),\n ...(values.address ? { address: values.address } : {}),\n ...(values.tags.length ? { tags: values.tags } : {}),\n ...(values.campaignIds.length\n ? { campaignIds: values.campaignIds }\n : {}),\n ...(values.ownerUid ? { ownerUid: values.ownerUid } : {}),\n ...(values.notes ? { notes: values.notes } : {}),\n // The org's own lead fields (AGL-3272), sent only when one was\n // filled — the route reads the definitions to judge the map, and\n // a body without it pays for no read.\n ...(values.custom ? { custom: values.custom } : {}),\n status: values.status,\n })\n if (!response.ok) {\n // The route's own sentence, shown above the form unchanged.\n setCreateError(\n String(payload['error'] ?? 'The lead could not be added.'),\n )\n return\n }\n // The activity entry is the route's: it verified the caller and\n // performed the write.\n enqueueSnackbar(\n payload['created']\n ? 'Lead added'\n : 'This site already held a lead for that address — it was updated',\n { variant: 'success', persist: false },\n )\n setCreateOpen(false)\n } catch (error) {\n console.error(error)\n setCreateError('The lead could not be added.')\n } finally {\n setCreateBusy(false)\n }\n },\n [crmApi, enqueueSnackbar],\n )\n\n const writeLead = useCallback(\n async (lead: LeadRow, fields: Record<string, unknown>, done: string) => {\n try {\n await updateDoc(\n doc(firestore, 'hosts', lead.hostId, 'leads', lead.leadId),\n {\n ...fields,\n updatedAt: serverTimestamp(),\n },\n )\n enqueueSnackbar(done, { variant: 'success', persist: false })\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error\n ? error.message\n : 'The lead could not be updated.',\n { variant: 'error' },\n )\n }\n },\n [firestore, enqueueSnackbar],\n )\n\n const columns = useMemo<GridColDef[]>(\n () => [\n {\n field: 'name',\n headerName: 'Lead',\n flex: 1.4,\n minWidth: 160,\n valueGetter: (_value, row: LeadRow) =>\n String(row['name'] || row['email'] || ''),\n renderCell: ({ row }: { row: LeadRow }) => (\n <Stack\n spacing={0}\n sx={{ minWidth: 0, justifyContent: 'center', height: '100%' }}\n >\n <Typography variant=\"body2\" noWrap>\n {String(row['name'] || row['email'] || row.$id)}\n </Typography>\n {row['name'] ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {String(row['email'] ?? '')}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n // The lead's own profile (AGL-3231): the two facts a work queue is\n // scanned by, beside the person.\n {\n field: 'company',\n headerName: 'Company',\n flex: 1,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) => String(row.company ?? ''),\n },\n {\n field: 'jobTitle',\n headerName: 'Title',\n flex: 0.9,\n minWidth: 130,\n valueGetter: (_value, row: LeadRow) => String(row.jobTitle ?? ''),\n },\n {\n field: 'status',\n headerName: 'Status',\n flex: 0.9,\n minWidth: 150,\n valueGetter: (_value, row: LeadRow) => Aglyn.crmLeadStatus(row),\n renderCell: ({ row }: { row: LeadRow }) => (\n <InlineStatus\n lead={row}\n onChange={(next) => {\n if (next === 'unqualified') {\n setUnqualifying(row)\n return\n }\n void writeLead(\n row,\n {\n status: next,\n ...(Aglyn.crmLeadStatus(row) === 'unqualified'\n ? { unqualifiedReason: deleteField() }\n : {}),\n },\n 'Status updated',\n )\n }}\n />\n ),\n },\n {\n /*\n * The verdict on the address (AGL-3245): the chip the lead's page\n * carries, so a bounced lead is told apart in the queue it is worked\n * from; its label for the sort and the export.\n */\n field: 'emailState',\n headerName: 'Email',\n flex: 0.9,\n minWidth: 150,\n valueGetter: (_value, row: LeadRow) => {\n const state = Aglyn.readEmailState(row)\n return state ? Aglyn.EMAIL_STATE_LABELS[state.status] : ''\n },\n renderCell: ({ row }: { row: LeadRow }) => {\n const state = Aglyn.readEmailState(row)\n return state ? (\n <CrmEmailStateChip state={state} />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n )\n },\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 1,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) =>\n row.ownerUid ? roster.labelFor(row.ownerUid) : 'Unassigned',\n },\n // Only at the organization level, where a row can be any site's.\n ...(hostId\n ? []\n : [\n {\n field: 'hostId',\n headerName: 'Site',\n flex: 0.9,\n minWidth: 140,\n valueGetter: (_value: unknown, row: LeadRow) =>\n mount?.siteName(row.hostId) ?? row.hostId,\n } satisfies GridColDef,\n ]),\n {\n field: 'sources',\n headerName: 'Source',\n flex: 1,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) =>\n leadSources(row).map(leadSourceLabel).join(', '),\n },\n {\n field: 'tags',\n headerName: 'Tags',\n flex: 0.9,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) => (row.tags ?? []).join(', '),\n },\n // The campaigns the lead is filed under (AGL-3254), by name — the\n // ids are the storage. Under a site only, like the filter.\n ...(hostId\n ? [\n {\n field: 'campaignIds',\n headerName: 'Campaign',\n flex: 1,\n minWidth: 150,\n valueGetter: (_value: unknown, row: LeadRow) =>\n Aglyn.readCampaignIds(row).map(campaignName).join(', '),\n } satisfies GridColDef,\n ]\n : []),\n {\n field: 'lastSeenAtMs',\n headerName: 'Last seen',\n flex: 0.9,\n minWidth: 160,\n valueGetter: (_value, row: LeadRow) =>\n leadTimeLabel(row['lastSeenAtMs'] ?? row['createdAt']),\n },\n // The org's lead fields as optional columns (AGL-3272), read off the\n // row's own `custom` map the way the contacts list reads its own.\n // Ahead of the row menu, so the overflow stays at the right edge\n // however many fields the org has defined.\n ...customFieldColumns(leadFields.active),\n {\n field: 'actions',\n headerName: '',\n width: 56,\n sortable: false,\n filterable: false,\n disableColumnMenu: true,\n renderCell: ({ row }: { row: LeadRow }) => {\n /*\n * Why Convert is refused, in the order the lead's page refuses it:\n * a converted lead has its contact already, a closed one was\n * judged not real, and a person with an erasure pending must not\n * be captured again — a conversion is a capture (AGL-2623).\n */\n const converted = Boolean(row.convertedContactId)\n const erasurePending = Aglyn.readErasureRequestedAtMs(row) !== null\n const convertRefusal = converted\n ? 'This lead was converted'\n : !Aglyn.isCrmLeadOpen(row)\n ? 'This lead was unqualified'\n : erasurePending\n ? CONVERT_PENDING_ERASURE_REASON\n : null\n return (\n <Box\n onClick={(event) => event.stopPropagation()}\n sx={{ display: 'flex', alignItems: 'center', height: '100%' }}\n >\n <RowActionsMenu\n label={String(row['email'] ?? row.$id)}\n items={[\n {\n key: 'open',\n label: 'Open lead',\n icon: (\n <MdiIcon path={mdiAccountArrowRight.path} size={0.8} />\n ),\n href: routes.lead(row.leadId, hostId ? null : row.hostId),\n },\n {\n key: 'convert',\n label: 'Convert…',\n icon: (\n <MdiIcon\n path={mdiAccountConvertOutline.path}\n size={0.8}\n />\n ),\n onClick: () => setConverting(row),\n disabled: convertRefusal !== null,\n disabledReason: convertRefusal ?? undefined,\n },\n {\n key: 'assign',\n label: 'Assign owner',\n icon: (\n <MdiIcon path={mdiAccountTieOutline.path} size={0.8} />\n ),\n onClick: () => setAssigning(row),\n },\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: (\n <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />\n ),\n onClick: () => setUnqualifying(row),\n disabled:\n !Aglyn.isCrmLeadOpen(row) ||\n Boolean(row.convertedContactId),\n disabledReason: row.convertedContactId\n ? 'This lead was converted'\n : 'This lead is already closed',\n },\n ]}\n />\n </Box>\n )\n },\n },\n ],\n [roster, routes, writeLead, hostId, mount, campaignName, leadFields.active],\n )\n /* The column and sort models are the view's (AGL-2617). */\n const grid = useCrmViewGrid(views, columns)\n\n return (\n <>\n <CardDisplay\n header={'Leads'}\n help={Aglyn.pluginDocsHelp('contacts', {\n anchor: '#whats-in-the-crm-area',\n })}\n actions={\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n {/* The saved view this list is showing, beside the status it narrows to (AGL-2617). */}\n <CrmViewsControl controller={views} allLabel=\"All leads\" />\n <FormControl size=\"small\" sx={{ minWidth: 160 }}>\n <InputLabel id={filterLabelId}>{'Show'}</InputLabel>\n <Select\n labelId={filterLabelId}\n label=\"Show\"\n value={filter}\n onChange={(event) =>\n setFilter(event.target.value as LeadFilter)\n }\n >\n {LEAD_FILTERS.map((option) => (\n <MenuItem key={option} value={option}>\n {LEAD_FILTER_LABELS[option]}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n {/* The verdict on the address (AGL-3245): the bounced, the blocked, the ones who left. */}\n <FormControl size=\"small\" sx={{ minWidth: 160 }}>\n <InputLabel id={emailFilterLabelId}>{'Email'}</InputLabel>\n <Select\n labelId={emailFilterLabelId}\n label=\"Email\"\n value={emailFilter}\n onChange={(event) =>\n setEmailFilter(event.target.value as LeadEmailFilter)\n }\n >\n {LEAD_EMAIL_FILTERS.map((option) => (\n <MenuItem key={option} value={option}>\n {LEAD_EMAIL_FILTER_LABELS[option]}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n {/* The campaign the lead is filed under (AGL-3254): the site's containers, by name. */}\n {hostId ? (\n <FormControl size=\"small\" sx={{ minWidth: 180 }}>\n <InputLabel id={campaignFilterLabelId} shrink>\n {'Campaign'}\n </InputLabel>\n <Select\n labelId={campaignFilterLabelId}\n label=\"Campaign\"\n notched\n value={campaignFilter}\n onChange={(event) =>\n setCampaignFilter(String(event.target.value))\n }\n displayEmpty\n >\n <MenuItem value=\"\">{'Any campaign'}</MenuItem>\n {campaigns.options.map((option) => (\n <MenuItem key={option.value} value={option.value}>\n {option.label}\n </MenuItem>\n ))}\n {/* A stored filter naming a campaign the site no longer lists stays selectable, by id, so it can be cleared. */}\n {campaignFilter &&\n !campaigns.options.some(\n (option) => option.value === campaignFilter,\n ) ? (\n <MenuItem value={campaignFilter}>{campaignFilter}</MenuItem>\n ) : null}\n </Select>\n </FormControl>\n ) : null}\n <TextField\n size=\"small\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n placeholder=\"Search leads\"\n slotProps={{\n input: {\n startAdornment: (\n <InputAdornment position=\"start\">\n <MdiIcon path={mdiMagnify.path} size={0.8} />\n </InputAdornment>\n ),\n },\n htmlInput: { 'aria-label': 'Search leads', type: 'search' },\n }}\n sx={{ minWidth: 200 }}\n />\n <LeadImportButton hostId={hostId} />\n <Button size=\"small\" onClick={handleExport} disabled={!rows.length}>\n {'Export CSV'}\n </Button>\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => setCreateOpen(true)}\n >\n {'New lead'}\n </Button>\n </Stack>\n }\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {/* Which surfaces file a lead, by name (AGL-2612) — under a site its own, at the org level every site's (AGL-2638). */}\n {hostId ? (\n <LeadSurfacesNote hostId={hostId} />\n ) : (\n <OrgLeadSurfacesNote />\n )}\n {status === 'success' && window.length === 0 ? (\n <EmptyStateComponent\n label={'No leads yet'}\n description={\n 'Bookings and lead-routed forms on your site become leads on their own — or add one with New lead, or bring a list in with Import CSV.'\n }\n />\n ) : status === 'success' && rows.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`No ${LEAD_FILTER_LABELS[filter].toLowerCase()} leads` +\n (search.trim() ? ` match “${search.trim()}”` : '') +\n ` among the ${window.length.toLocaleString()} most recently seen.`}\n </Typography>\n ) : (\n <>\n <LeadsBulkBar\n rows={rows}\n selected={selectedIds}\n onSelectedChange={setSelectedIds}\n roster={roster}\n csv={csvOptions}\n orgId={orgId}\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={pageRows}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{\n selected: selectedIds,\n onChange: setSelectedIds,\n }}\n loading={status === 'loading'}\n onOpen={(_id, row: LeadRow) =>\n router.push(\n routes.lead(row.leadId, hostId ? null : row.hostId),\n )\n }\n // Columns and sort are the view's, controlled (AGL-2617).\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={\n grid.onColumnVisibilityModelChange\n }\n sortModel={grid.sortModel}\n onSortModelChange={grid.onSortModelChange}\n // The search is the section's, above: the grid's own box\n // would search this page alone.\n quickFilter={false}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n </CrmColumnOrderProvider>\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={rows.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n {truncated ? (\n <Alert severity=\"info\">\n {`Showing the ${LEADS_WINDOW.toLocaleString()} most recently seen ` +\n 'leads. The search box and the status filter narrow these ' +\n `${LEADS_WINDOW.toLocaleString()} only; older leads are still ` +\n 'listed in the Inbox and reached by campaign audiences.'}\n </Alert>\n ) : null}\n </Stack>\n </CardDisplay>\n <NewLeadDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n hostId={hostId ?? null}\n org={org as Record<string, unknown> | undefined}\n busy={createBusy}\n error={createError}\n roster={roster}\n orgId={orgId}\n onSubmit={(values) => void handleCreate(values)}\n />\n <AssignOwnerDialog\n lead={assigning}\n roster={roster}\n onClose={() => setAssigning(null)}\n onAssign={(uid) => {\n if (!assigning) return\n void writeLead(\n assigning,\n { ownerUid: uid || deleteField() },\n uid ? 'Owner assigned' : 'Owner cleared',\n )\n setAssigning(null)\n }}\n />\n <LeadUnqualifyDialog\n open={Boolean(unqualifying)}\n onClose={() => setUnqualifying(null)}\n hostId={unqualifying?.hostId ?? hostId ?? ''}\n leadId={unqualifying?.leadId ?? ''}\n leadLabel={String(\n unqualifying?.['name'] || unqualifying?.['email'] || '',\n )}\n />\n {/* The row's site, not the mounted one: at the organization level a\n lead is its own site's record, and the conversion is that site's\n capture (AGL-2641). Under a site the two are the same. */}\n <LeadConvertDialog\n open={Boolean(converting)}\n onClose={() => setConverting(null)}\n hostId={converting?.hostId ?? hostId ?? ''}\n orgId={orgId}\n org={org as Record<string, unknown> | undefined}\n leadId={converting?.leadId ?? ''}\n lead={converting ?? {}}\n basePath={basePath ?? ''}\n roster={roster}\n />\n </>\n )\n}\nCrmLeadsSection.displayName = 'CrmLeadsSection'\n\n/**\n * The status, editable in place for a lead that is still open.\n *\n * A converted lead shows the chip alone: its status IS the conversion, and\n * the route stamped it. The select stops its click from reaching the row, or\n * every status change would also open the record.\n */\nfunction InlineStatus(props: {\n lead: LeadRow\n onChange: (next: CrmLeadStatus) => void\n}) {\n const { lead, onChange } = props\n if (lead.convertedContactId) {\n return (\n <Box sx={{ display: 'flex', alignItems: 'center', height: '100%' }}>\n <LeadStatusChip lead={lead} />\n </Box>\n )\n }\n const status = Aglyn.crmLeadStatus(lead)\n return (\n <Box\n onClick={(event) => event.stopPropagation()}\n sx={{\n display: 'flex',\n alignItems: 'center',\n height: '100%',\n width: '100%',\n }}\n >\n <Select\n size=\"small\"\n variant=\"standard\"\n disableUnderline\n value={status}\n onChange={(event) => onChange(event.target.value as CrmLeadStatus)}\n renderValue={() => <LeadStatusChip lead={lead} />}\n sx={{ width: '100%' }}\n >\n <MenuItem value=\"new\">{Aglyn.CRM_LEAD_STATUS_LABELS.new}</MenuItem>\n <MenuItem value=\"working\">\n {Aglyn.CRM_LEAD_STATUS_LABELS.working}\n </MenuItem>\n <MenuItem value=\"unqualified\">{`${Aglyn.CRM_LEAD_STATUS_LABELS.unqualified}…`}</MenuItem>\n </Select>\n </Box>\n )\n}\nInlineStatus.displayName = 'InlineStatus'\n\n/** Hand a lead to a team member. */\nfunction AssignOwnerDialog(props: {\n lead: LeadRow | null\n roster: OrgMemberOptions\n onClose: () => void\n onAssign: (uid: string) => void\n}) {\n const { lead, roster, onClose, onAssign } = props\n const [uid, setUid] = useState<string | null>(null)\n const current = uid ?? String(lead?.ownerUid ?? '')\n return (\n <Dialog\n open={Boolean(lead)}\n onClose={onClose}\n maxWidth=\"xs\"\n fullWidth\n slotProps={{ transition: { onExited: () => setUid(null) } }}\n >\n <DialogTitle>{`Assign ${String(lead?.['name'] || lead?.['email'] || 'lead')}`}</DialogTitle>\n <DialogContent>\n <Box sx={{ pt: 1 }}>\n <LeadOwnerSelect\n value={current}\n onChange={setUid}\n roster={roster}\n size=\"medium\"\n />\n </Box>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{'Cancel'}</Button>\n <Button variant=\"contained\" onClick={() => onAssign(current)}>\n {'Assign'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nAssignOwnerDialog.displayName = 'AssignOwnerDialog'\n\nexport default CrmLeadsSection\n"],"names":["Aglyn","mdiAccountArrowRight","mdiAccountCancelOutline","mdiAccountConvertOutline","mdiAccountTieOutline","mdiMagnify","CardDisplay","MdiIcon","ListPagination","ListTable","useOrgMemberOptions","useCrmOrgMount","useCrmSavedView","useCrmScope","useContactFieldDefinitions","customFieldColumns","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","useOrgLeads","CrmViewsControl","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","EmptyStateComponent","useSnackbar","useFirestore","useFirestoreCollection","useHostCampaigns","Alert","Box","Button","Dialog","DialogActions","DialogContent","DialogTitle","FormControl","InputAdornment","InputLabel","MenuItem","Select","Stack","TextField","Typography","collection","deleteField","doc","limit","orderBy","query","serverTimestamp","updateDoc","useRouter","useCallback","useEffect","useId","useMemo","useState","downloadTextFile","crmRoutes","LEAD_FILTER_LABELS","LEAD_EMAIL_FILTER_LABELS","LEAD_EMAIL_FILTERS","LEAD_FILTERS","leadMatchesCampaignFilter","leadMatchesEmailFilter","leadMatchesFilter","leadMatchesSearch","leadsCsv","LeadConvertDialog","leadSourceLabel","leadSources","leadTimeLabel","LeadImportButton","NewLeadDrawer","useCrmApi","LeadOwnerSelect","CONVERT_PENDING_ERASURE_REASON","CrmEmailStateChip","LeadStatusChip","LeadSurfacesNote","LeadUnqualifyDialog","LeadsBulkBar","OrgLeadSurfacesNote","LEADS_WINDOW","CrmLeadsSection","props","unqualifying","converting","hostId","org","basePath","firestore","router","enqueueSnackbar","orgId","createHostId","mount","roster","leadFields","routes","site","idField","orgHostIds","hosts","map","host","id","orgLeads","hostIds","windowSize","leadDocs","data","row","leadId","$id","status","hostsReady","length","truncated","window","slice","views","section","filter","value","state","filters","find","clause","field","includes","emailFilter","setFilter","next","setFilters","op","setEmailFilter","campaignFilter","setCampaignFilter","campaigns","enabled","Boolean","campaignName","options","option","label","filterLabelId","emailFilterLabelId","campaignFilterLabelId","search","setSearch","rows","lead","page","setPage","pageSize","setPageSize","pageRows","selectedIds","setSelectedIds","csvOptions","ownerEmail","emailFor","siteName","handleExport","assigning","setAssigning","setUnqualifying","setConverting","crmApi","createOpen","setCreateOpen","createBusy","setCreateBusy","createError","setCreateError","handleCreate","values","response","payload","email","name","company","jobTitle","phone","website","leadSource","address","tags","campaignIds","ownerUid","notes","custom","ok","String","variant","persist","error","console","writeLead","fields","done","updatedAt","Error","message","columns","headerName","flex","minWidth","valueGetter","_value","renderCell","spacing","sx","justifyContent","height","noWrap","color","crmLeadStatus","InlineStatus","onChange","unqualifiedReason","readEmailState","EMAIL_STATE_LABELS","labelFor","join","readCampaignIds","active","width","sortable","filterable","disableColumnMenu","converted","convertedContactId","erasurePending","readErasureRequestedAtMs","convertRefusal","isCrmLeadOpen","onClick","event","stopPropagation","display","alignItems","items","key","icon","path","size","href","disabled","disabledReason","undefined","grid","header","help","pluginDocsHelp","anchor","actions","direction","controller","allLabel","labelId","target","shrink","notched","displayEmpty","some","placeholder","slotProps","input","startAdornment","position","htmlInput","type","contentGutterX","contentGutterY","description","toLowerCase","trim","toLocaleString","selected","onSelectedChange","csv","columnOrder","slots","selectable","loading","onOpen","_id","push","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","quickFilter","hideFooter","rowCount","count","onPageChange","onPageSizeChange","severity","open","onClose","busy","onSubmit","AssignOwnerDialog","onAssign","uid","leadLabel","displayName","disableUnderline","renderValue","CRM_LEAD_STATUS_LABELS","new","working","unqualified","setUid","current","maxWidth","fullWidth","transition","onExited","pt"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/leads-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type {\n ConsolePluginPageProps,\n CrmLeadFields,\n CrmLeadStatus,\n} from '@aglyn/aglyn'\nimport {\n mdiAccountArrowRight,\n mdiAccountCancelOutline,\n mdiAccountConvertOutline,\n mdiAccountTieOutline,\n mdiMagnify,\n} from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n type OrgMemberOptions,\n useOrgMemberOptions,\n} from '../hooks/use-org-member-options'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { customFieldColumns } from './contact-custom-columns'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport { useOrgLeads } from '../hooks/use-org-leads'\nimport CrmViewsControl from './crm-views-control'\nimport RowActionsMenu from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useHostCampaigns,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Box,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControl,\n InputAdornment,\n InputLabel,\n MenuItem,\n Select,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n deleteField,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n updateDoc,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useId, useMemo, useState } from 'react'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n LEAD_FILTER_LABELS,\n LEAD_EMAIL_FILTER_LABELS,\n LEAD_EMAIL_FILTERS,\n LEAD_FILTERS,\n type LeadEmailFilter,\n type LeadFilter,\n leadMatchesCampaignFilter,\n leadMatchesEmailFilter,\n leadMatchesFilter,\n leadMatchesSearch,\n} from '../model/lead-filters'\nimport { type LeadCsvOptions, leadsCsv } from '../model/leads-csv'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport {\n leadSourceLabel,\n leadSources,\n leadTimeLabel,\n} from './lead-history-card'\nimport { LeadImportButton } from './lead-import-drawer'\nimport NewLeadDrawer, { type NewLeadValues } from './new-lead-drawer'\nimport { useCrmApi } from './use-crm-api'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { CONVERT_PENDING_ERASURE_REASON } from './lead-properties-card'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { LeadStatusChip } from './lead-status-chip'\nimport LeadSurfacesNote from './lead-surfaces-note'\nimport { LeadUnqualifyDialog } from './lead-unqualify-dialog'\nimport LeadsBulkBar from './leads-bulk-bar'\nimport OrgLeadSurfacesNote from './org-lead-surfaces-note'\n\n/**\n * How many leads the section reads: the newest by last seen, plus one probe\n * row so \"there are more\" is a fact rather than a guess at the boundary.\n *\n * A CEILING and a client-side filter rather than a paged status query, and\n * the reason is the lead documents themselves. Every lead the capture door\n * writes carries NO `status` — the field exists only once somebody in the\n * CRM has touched the lead — and Firestore cannot select documents by a\n * field's absence: `where('status','in',[…])`, `!=` and `not-in` all skip a\n * document without the field. A server-side \"open leads\" query would\n * therefore hide every lead nobody has worked yet, which is the entire\n * population the section exists to show on the day it ships. So the query\n * is the one order every lead can satisfy (`lastSeenAtMs`, stamped on every\n * capture), and the status filter narrows the loaded window — said out loud\n * beneath the table whenever the window is not the whole collection.\n *\n * The window is then PAGED in memory under the shared footer, the way the\n * workspace pickers page a slice of a window they cannot re-key: the rows\n * are already in the snapshot, so turning a page costs nothing, and the\n * footer's count is exact because it counts the filtered window rather than\n * a collection nobody has measured.\n */\nconst LEADS_WINDOW = 200\n\n/**\n * One row of the list. `$id` keys the grid and `leadId` names the document,\n * and they are the same value at both levels (AGL-3275).\n *\n * `$id` used to be `{hostId}/{leadId}` at the organization level, because a\n * lead's id is a person key and the same person met by two sites was two\n * documents carrying it — so the id alone could not key a list that spanned\n * sites. One org collection ends that, and the row no longer carries a site\n * at all: which sites hold this person is `capturedByHostIds`, a fact about\n * the person rather than part of their address.\n */\ntype LeadRow = Record<string, unknown> &\n CrmLeadFields & { $id: string; leadId: string }\n\n/**\n * `/crm/leads` — the people a site has met but not yet qualified (AGL-2608).\n *\n * A section of its own, the way Salesforce keeps Leads apart from Contacts:\n * a lead is a capture — a form, a booking, a sign-up — that somebody has\n * still to work, and it converts into a contact, a company and a deal when\n * it is real. Reads `hosts/{hostId}/leads`, host-scoped by path, so there is\n * no `visibleTo` filter; the Firestore rules admit any member of the site to\n * read it and an admin, editor or author to update it, which is what makes\n * the inline status and owner changes client-direct writes.\n *\n * At the ORGANIZATION level (AGL-2630) there is no one site to read: the\n * section opens the same query under every site the org has (`useOrgLeads`)\n * and lists the merged window with a Site column, every row naming the site\n * its writes and its link go to. The per-site notes — which of a site's\n * forms file a lead — belong to a site's own hub and are not drawn here.\n */\nexport function CrmLeadsSection(props: ConsolePluginPageProps) {\n const { hostId, org, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, createHostId } = useCrmScope({ hostId, org })\n const mount = useCrmOrgMount()\n const roster = useOrgMemberOptions(orgId)\n // The org's lead fields, for the optional columns below (AGL-3272).\n const leadFields = useContactFieldDefinitions(orgId, 'lead')\n const routes = crmRoutes(basePath ?? '')\n\n // Under a site: the site's own window, rows keyed by document id.\n const site = useFirestoreCollection<\n Record<string, unknown> & CrmLeadFields & { $id: string }\n >(\n () =>\n hostId\n ? query(\n collection(firestore, 'hosts', hostId, 'leads'),\n orderBy('lastSeenAtMs', 'desc'),\n limit(LEADS_WINDOW + 1),\n )\n : null,\n [firestore, hostId],\n { idField: '$id' },\n )\n // At the organization level: every site's window, merged.\n const orgHostIds = useMemo(\n () => (hostId ? [] : (mount?.hosts ?? []).map((host) => host.id)),\n [hostId, mount?.hosts],\n )\n /*\n * ONE LISTENER (AGL-3275), where this used to fan out across the org's\n * sites and merge. At the organization level an org-wide member reads with\n * no scope clause, which is what `visibleTo: null` asks for.\n */\n const orgLeads = useOrgLeads({\n orgId,\n visibleTo: null,\n windowSize: LEADS_WINDOW,\n })\n const leadDocs = useMemo<LeadRow[]>(\n () =>\n hostId\n ? site.data.map((row) => ({ ...row, leadId: row.$id }))\n : orgLeads.data,\n [hostId, site.data, orgLeads.data],\n )\n const status = hostId\n ? site.status\n : mount?.hostsReady && !orgHostIds.length\n ? 'success'\n : orgLeads.status\n const truncated = hostId ? leadDocs.length > LEADS_WINDOW : orgLeads.truncated\n const window = useMemo(() => leadDocs.slice(0, LEADS_WINDOW), [leadDocs])\n\n /*\n * The `Show` filter is the saved VIEW'S (AGL-2617): a saved view of leads\n * holds the status beside the columns and the sort, and the select below\n * writes into it. Unset reads as `open`, which is what the section opened\n * on before views existed and the one reading a query cannot express.\n */\n const views = useCrmSavedView({\n section: 'leads',\n hostId,\n org: props.org,\n basePath: basePath ?? '',\n })\n const filter: LeadFilter = useMemo(() => {\n const value = views.state.filters.find(\n (clause) => clause.field === 'status',\n )?.value\n return (LEAD_FILTERS as readonly string[]).includes(value ?? '')\n ? (value as LeadFilter)\n : 'open'\n }, [views.state.filters])\n /*\n * The `Email` filter (AGL-3245) is the view's too, as an `emailState`\n * clause beside the status one. Each setter keeps the other's clause:\n * narrowing to bounced leads does not reopen the unqualified ones.\n */\n const emailFilter: LeadEmailFilter = useMemo(() => {\n const value = views.state.filters.find(\n (clause) => clause.field === 'emailState',\n )?.value\n return (LEAD_EMAIL_FILTERS as readonly string[]).includes(value ?? '')\n ? (value as LeadEmailFilter)\n : 'any'\n }, [views.state.filters])\n const setFilter = useCallback(\n (next: LeadFilter) =>\n views.setFilters([\n ...views.state.filters.filter((clause) => clause.field !== 'status'),\n ...(next === 'open'\n ? []\n : [{ field: 'status', op: 'equals', value: next }]),\n ]),\n [views.setFilters, views.state.filters],\n )\n const setEmailFilter = useCallback(\n (next: LeadEmailFilter) =>\n views.setFilters([\n ...views.state.filters.filter(\n (clause) => clause.field !== 'emailState',\n ),\n ...(next === 'any'\n ? []\n : [{ field: 'emailState', op: 'equals', value: next }]),\n ]),\n [views.setFilters, views.state.filters],\n )\n /*\n * The `Campaign` filter (AGL-3254) is the view's too, as a `campaignIds`\n * clause: the id of one of the site's campaign containers, resolved to\n * its name from the containers themselves — ids only in storage, so a\n * renamed campaign keeps its leads. Under a site alone: a campaign\n * belongs to one site, and the organization-level list spans them all.\n */\n const campaignFilter = useMemo(\n () =>\n views.state.filters.find((clause) => clause.field === 'campaignIds')\n ?.value ?? '',\n [views.state.filters],\n )\n const setCampaignFilter = useCallback(\n (next: string) =>\n views.setFilters([\n ...views.state.filters.filter(\n (clause) => clause.field !== 'campaignIds',\n ),\n ...(next\n ? [{ field: 'campaignIds', op: 'contains', value: next }]\n : []),\n ]),\n [views.setFilters, views.state.filters],\n )\n const campaigns = useHostCampaigns(hostId, { enabled: Boolean(hostId) })\n const campaignName = useCallback(\n (id: string) =>\n campaigns.options.find((option) => option.value === id)?.label ?? id,\n [campaigns.options],\n )\n // The label's id, so the filter's combobox is named \"Show\" rather than\n // after the option it shows — see `LeadOwnerSelect`.\n const filterLabelId = useId()\n const emailFilterLabelId = useId()\n const campaignFilterLabelId = useId()\n /*\n * The search box is the SECTION'S, not the grid's (AGL-3246). The grid's\n * quick filter runs over the rows the grid holds, and the grid holds one\n * PAGE of the window — so a lead on page three answered \"no match\" while\n * the footer below went on counting the unfiltered window. The term\n * narrows the whole loaded window here, beside the status filter and\n * before the footer's count and the page slice, over the fields a person\n * types to find a lead: name, email, company, title and tags.\n */\n const [search, setSearch] = useState('')\n const rows = useMemo(\n () =>\n window.filter(\n (lead) =>\n leadMatchesFilter(lead, filter) &&\n leadMatchesEmailFilter(lead, emailFilter) &&\n leadMatchesCampaignFilter(lead, campaignFilter) &&\n leadMatchesSearch(lead, search),\n ),\n [window, filter, emailFilter, campaignFilter, search],\n )\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n // A new filter or search term starts on page one: page three of the open\n // leads is not a page of the unqualified ones, and an out-of-range page\n // renders empty.\n useEffect(() => {\n setPage(0)\n }, [filter, emailFilter, campaignFilter, search])\n const pageRows = useMemo(\n () => rows.slice(page * pageSize, (page + 1) * pageSize),\n [rows, page, pageSize],\n )\n\n /*\n * The ticked rows, for the bulk bar (AGL-2662). Cleared when the filter or\n * the search term changes: a selection made on the open leads is not a\n * selection of the unqualified ones, and the bar's count would be over\n * rows no longer listed.\n */\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n useEffect(\n () => setSelectedIds([]),\n [filter, emailFilter, campaignFilter, search],\n )\n // How the file names the owner and, at the org level, the site.\n const csvOptions: LeadCsvOptions = useMemo(\n () => ({\n ownerEmail: roster.emailFor,\n ...(hostId ? {} : { siteName: (id: string) => mount?.siteName(id) }),\n }),\n [roster.emailFor, hostId, mount],\n )\n // The listed window — every row the filter and the search admit, not just\n // the page.\n const handleExport = useCallback(() => {\n downloadTextFile('leads.csv', 'text/csv', leadsCsv(rows, csvOptions))\n }, [rows, csvOptions])\n\n const [assigning, setAssigning] = useState<LeadRow | null>(null)\n const [unqualifying, setUnqualifying] = useState<LeadRow | null>(null)\n // The row whose conversion dialog is open (AGL-2641) — the same dialog\n // the lead's page opens, fed the row so the list is one click shorter.\n const [converting, setConverting] = useState<LeadRow | null>(null)\n\n /*==========================================\n * NEW LEAD (AGL-3231) — Salesforce's New Lead, in a drawer over the\n * list. The route files the lead through the one lead door under the\n * mounted site, or at the organization level under the site the drawer's\n * picker named; the drawer holds its submit until one is known. It makes\n * a lead and nothing else — the conversion is what makes the contact.\n *=========================================*/\n const crmApi = useCrmApi(createHostId)\n const [createOpen, setCreateOpen] = useState(false)\n const [createBusy, setCreateBusy] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const handleCreate = useCallback(\n async (values: NewLeadValues) => {\n setCreateBusy(true)\n setCreateError(null)\n try {\n const { response, payload } = await crmApi('leads-create', {\n email: values.email,\n ...(values.name ? { name: values.name } : {}),\n ...(values.company ? { company: values.company } : {}),\n ...(values.jobTitle ? { jobTitle: values.jobTitle } : {}),\n ...(values.phone ? { phone: values.phone } : {}),\n ...(values.website ? { website: values.website } : {}),\n ...(values.leadSource ? { leadSource: values.leadSource } : {}),\n ...(values.address ? { address: values.address } : {}),\n ...(values.tags.length ? { tags: values.tags } : {}),\n ...(values.campaignIds.length\n ? { campaignIds: values.campaignIds }\n : {}),\n ...(values.ownerUid ? { ownerUid: values.ownerUid } : {}),\n ...(values.notes ? { notes: values.notes } : {}),\n // The org's own lead fields (AGL-3272), sent only when one was\n // filled — the route reads the definitions to judge the map, and\n // a body without it pays for no read.\n ...(values.custom ? { custom: values.custom } : {}),\n status: values.status,\n })\n if (!response.ok) {\n // The route's own sentence, shown above the form unchanged.\n setCreateError(\n String(payload['error'] ?? 'The lead could not be added.'),\n )\n return\n }\n // The activity entry is the route's: it verified the caller and\n // performed the write.\n enqueueSnackbar(\n payload['created']\n ? 'Lead added'\n : 'This site already held a lead for that address — it was updated',\n { variant: 'success', persist: false },\n )\n setCreateOpen(false)\n } catch (error) {\n console.error(error)\n setCreateError('The lead could not be added.')\n } finally {\n setCreateBusy(false)\n }\n },\n [crmApi, enqueueSnackbar],\n )\n\n const writeLead = useCallback(\n async (lead: LeadRow, fields: Record<string, unknown>, done: string) => {\n try {\n await updateDoc(\n doc(firestore, 'orgs', orgId, 'leads', lead.leadId),\n {\n ...fields,\n updatedAt: serverTimestamp(),\n },\n )\n enqueueSnackbar(done, { variant: 'success', persist: false })\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error\n ? error.message\n : 'The lead could not be updated.',\n { variant: 'error' },\n )\n }\n },\n [firestore, enqueueSnackbar],\n )\n\n const columns = useMemo<GridColDef[]>(\n () => [\n {\n field: 'name',\n headerName: 'Lead',\n flex: 1.4,\n minWidth: 160,\n valueGetter: (_value, row: LeadRow) =>\n String(row['name'] || row['email'] || ''),\n renderCell: ({ row }: { row: LeadRow }) => (\n <Stack\n spacing={0}\n sx={{ minWidth: 0, justifyContent: 'center', height: '100%' }}\n >\n <Typography variant=\"body2\" noWrap>\n {String(row['name'] || row['email'] || row.$id)}\n </Typography>\n {row['name'] ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {String(row['email'] ?? '')}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n // The lead's own profile (AGL-3231): the two facts a work queue is\n // scanned by, beside the person.\n {\n field: 'company',\n headerName: 'Company',\n flex: 1,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) => String(row.company ?? ''),\n },\n {\n field: 'jobTitle',\n headerName: 'Title',\n flex: 0.9,\n minWidth: 130,\n valueGetter: (_value, row: LeadRow) => String(row.jobTitle ?? ''),\n },\n {\n field: 'status',\n headerName: 'Status',\n flex: 0.9,\n minWidth: 150,\n valueGetter: (_value, row: LeadRow) => Aglyn.crmLeadStatus(row),\n renderCell: ({ row }: { row: LeadRow }) => (\n <InlineStatus\n lead={row}\n onChange={(next) => {\n if (next === 'unqualified') {\n setUnqualifying(row)\n return\n }\n void writeLead(\n row,\n {\n status: next,\n ...(Aglyn.crmLeadStatus(row) === 'unqualified'\n ? { unqualifiedReason: deleteField() }\n : {}),\n },\n 'Status updated',\n )\n }}\n />\n ),\n },\n {\n /*\n * The verdict on the address (AGL-3245): the chip the lead's page\n * carries, so a bounced lead is told apart in the queue it is worked\n * from; its label for the sort and the export.\n */\n field: 'emailState',\n headerName: 'Email',\n flex: 0.9,\n minWidth: 150,\n valueGetter: (_value, row: LeadRow) => {\n const state = Aglyn.readEmailState(row)\n return state ? Aglyn.EMAIL_STATE_LABELS[state.status] : ''\n },\n renderCell: ({ row }: { row: LeadRow }) => {\n const state = Aglyn.readEmailState(row)\n return state ? (\n <CrmEmailStateChip state={state} />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n )\n },\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 1,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) =>\n row.ownerUid ? roster.labelFor(row.ownerUid) : 'Unassigned',\n },\n // Only at the organization level, where a row can be any site's.\n ...(hostId\n ? []\n : [\n {\n field: 'hostId',\n headerName: 'Site',\n flex: 0.9,\n minWidth: 140,\n /*\n * KNOWN BY, not \"the site\" (AGL-3275). A lead is one org row\n * and several sites in a consent group can hold the same\n * person, so this names every site that captured them — the\n * answer the Contacts list has always given in its own column.\n */\n valueGetter: (_value: unknown, row: LeadRow) => {\n const held = Array.isArray(row['capturedByHostIds'])\n ? (row['capturedByHostIds'] as string[])\n : []\n return held.map((id) => mount?.siteName(id) ?? id).join(', ')\n },\n } satisfies GridColDef,\n ]),\n {\n field: 'sources',\n headerName: 'Source',\n flex: 1,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) =>\n leadSources(row).map(leadSourceLabel).join(', '),\n },\n {\n field: 'tags',\n headerName: 'Tags',\n flex: 0.9,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) => (row.tags ?? []).join(', '),\n },\n // The campaigns the lead is filed under (AGL-3254), by name — the\n // ids are the storage. Under a site only, like the filter.\n ...(hostId\n ? [\n {\n field: 'campaignIds',\n headerName: 'Campaign',\n flex: 1,\n minWidth: 150,\n valueGetter: (_value: unknown, row: LeadRow) =>\n Aglyn.readCampaignIds(row).map(campaignName).join(', '),\n } satisfies GridColDef,\n ]\n : []),\n {\n field: 'lastSeenAtMs',\n headerName: 'Last seen',\n flex: 0.9,\n minWidth: 160,\n valueGetter: (_value, row: LeadRow) =>\n leadTimeLabel(row['lastSeenAtMs'] ?? row['createdAt']),\n },\n // The org's lead fields as optional columns (AGL-3272), read off the\n // row's own `custom` map the way the contacts list reads its own.\n // Ahead of the row menu, so the overflow stays at the right edge\n // however many fields the org has defined.\n ...customFieldColumns(leadFields.active),\n {\n field: 'actions',\n headerName: '',\n width: 56,\n sortable: false,\n filterable: false,\n disableColumnMenu: true,\n renderCell: ({ row }: { row: LeadRow }) => {\n /*\n * Why Convert is refused, in the order the lead's page refuses it:\n * a converted lead has its contact already, a closed one was\n * judged not real, and a person with an erasure pending must not\n * be captured again — a conversion is a capture (AGL-2623).\n */\n const converted = Boolean(row.convertedContactId)\n const erasurePending = Aglyn.readErasureRequestedAtMs(row) !== null\n const convertRefusal = converted\n ? 'This lead was converted'\n : !Aglyn.isCrmLeadOpen(row)\n ? 'This lead was unqualified'\n : erasurePending\n ? CONVERT_PENDING_ERASURE_REASON\n : null\n return (\n <Box\n onClick={(event) => event.stopPropagation()}\n sx={{ display: 'flex', alignItems: 'center', height: '100%' }}\n >\n <RowActionsMenu\n label={String(row['email'] ?? row.$id)}\n items={[\n {\n key: 'open',\n label: 'Open lead',\n icon: (\n <MdiIcon path={mdiAccountArrowRight.path} size={0.8} />\n ),\n href: routes.lead(row.leadId),\n },\n {\n key: 'convert',\n label: 'Convert…',\n icon: (\n <MdiIcon\n path={mdiAccountConvertOutline.path}\n size={0.8}\n />\n ),\n onClick: () => setConverting(row),\n disabled: convertRefusal !== null,\n disabledReason: convertRefusal ?? undefined,\n },\n {\n key: 'assign',\n label: 'Assign owner',\n icon: (\n <MdiIcon path={mdiAccountTieOutline.path} size={0.8} />\n ),\n onClick: () => setAssigning(row),\n },\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: (\n <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />\n ),\n onClick: () => setUnqualifying(row),\n disabled:\n !Aglyn.isCrmLeadOpen(row) ||\n Boolean(row.convertedContactId),\n disabledReason: row.convertedContactId\n ? 'This lead was converted'\n : 'This lead is already closed',\n },\n ]}\n />\n </Box>\n )\n },\n },\n ],\n [roster, routes, writeLead, hostId, mount, campaignName, leadFields.active],\n )\n /* The column and sort models are the view's (AGL-2617). */\n const grid = useCrmViewGrid(views, columns)\n\n return (\n <>\n <CardDisplay\n header={'Leads'}\n help={Aglyn.pluginDocsHelp('contacts', {\n anchor: '#whats-in-the-crm-area',\n })}\n actions={\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n {/* The saved view this list is showing, beside the status it narrows to (AGL-2617). */}\n <CrmViewsControl controller={views} allLabel=\"All leads\" />\n <FormControl size=\"small\" sx={{ minWidth: 160 }}>\n <InputLabel id={filterLabelId}>{'Show'}</InputLabel>\n <Select\n labelId={filterLabelId}\n label=\"Show\"\n value={filter}\n onChange={(event) =>\n setFilter(event.target.value as LeadFilter)\n }\n >\n {LEAD_FILTERS.map((option) => (\n <MenuItem key={option} value={option}>\n {LEAD_FILTER_LABELS[option]}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n {/* The verdict on the address (AGL-3245): the bounced, the blocked, the ones who left. */}\n <FormControl size=\"small\" sx={{ minWidth: 160 }}>\n <InputLabel id={emailFilterLabelId}>{'Email'}</InputLabel>\n <Select\n labelId={emailFilterLabelId}\n label=\"Email\"\n value={emailFilter}\n onChange={(event) =>\n setEmailFilter(event.target.value as LeadEmailFilter)\n }\n >\n {LEAD_EMAIL_FILTERS.map((option) => (\n <MenuItem key={option} value={option}>\n {LEAD_EMAIL_FILTER_LABELS[option]}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n {/* The campaign the lead is filed under (AGL-3254): the site's containers, by name. */}\n {hostId ? (\n <FormControl size=\"small\" sx={{ minWidth: 180 }}>\n <InputLabel id={campaignFilterLabelId} shrink>\n {'Campaign'}\n </InputLabel>\n <Select\n labelId={campaignFilterLabelId}\n label=\"Campaign\"\n notched\n value={campaignFilter}\n onChange={(event) =>\n setCampaignFilter(String(event.target.value))\n }\n displayEmpty\n >\n <MenuItem value=\"\">{'Any campaign'}</MenuItem>\n {campaigns.options.map((option) => (\n <MenuItem key={option.value} value={option.value}>\n {option.label}\n </MenuItem>\n ))}\n {/* A stored filter naming a campaign the site no longer lists stays selectable, by id, so it can be cleared. */}\n {campaignFilter &&\n !campaigns.options.some(\n (option) => option.value === campaignFilter,\n ) ? (\n <MenuItem value={campaignFilter}>{campaignFilter}</MenuItem>\n ) : null}\n </Select>\n </FormControl>\n ) : null}\n <TextField\n size=\"small\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n placeholder=\"Search leads\"\n slotProps={{\n input: {\n startAdornment: (\n <InputAdornment position=\"start\">\n <MdiIcon path={mdiMagnify.path} size={0.8} />\n </InputAdornment>\n ),\n },\n htmlInput: { 'aria-label': 'Search leads', type: 'search' },\n }}\n sx={{ minWidth: 200 }}\n />\n <LeadImportButton hostId={hostId} />\n <Button size=\"small\" onClick={handleExport} disabled={!rows.length}>\n {'Export CSV'}\n </Button>\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => setCreateOpen(true)}\n >\n {'New lead'}\n </Button>\n </Stack>\n }\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {/* Which surfaces file a lead, by name (AGL-2612) — under a site its own, at the org level every site's (AGL-2638). */}\n {hostId ? (\n <LeadSurfacesNote hostId={hostId} />\n ) : (\n <OrgLeadSurfacesNote />\n )}\n {status === 'success' && window.length === 0 ? (\n <EmptyStateComponent\n label={'No leads yet'}\n description={\n 'Bookings and lead-routed forms on your site become leads on their own — or add one with New lead, or bring a list in with Import CSV.'\n }\n />\n ) : status === 'success' && rows.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`No ${LEAD_FILTER_LABELS[filter].toLowerCase()} leads` +\n (search.trim() ? ` match “${search.trim()}”` : '') +\n ` among the ${window.length.toLocaleString()} most recently seen.`}\n </Typography>\n ) : (\n <>\n <LeadsBulkBar\n rows={rows}\n selected={selectedIds}\n onSelectedChange={setSelectedIds}\n roster={roster}\n csv={csvOptions}\n orgId={orgId}\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={pageRows}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{\n selected: selectedIds,\n onChange: setSelectedIds,\n }}\n loading={status === 'loading'}\n onOpen={(_id, row: LeadRow) =>\n router.push(\n routes.lead(row.leadId),\n )\n }\n // Columns and sort are the view's, controlled (AGL-2617).\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={\n grid.onColumnVisibilityModelChange\n }\n sortModel={grid.sortModel}\n onSortModelChange={grid.onSortModelChange}\n // The search is the section's, above: the grid's own box\n // would search this page alone.\n quickFilter={false}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n </CrmColumnOrderProvider>\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={rows.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n {truncated ? (\n <Alert severity=\"info\">\n {`Showing the ${LEADS_WINDOW.toLocaleString()} most recently seen ` +\n 'leads. The search box and the status filter narrow these ' +\n `${LEADS_WINDOW.toLocaleString()} only; older leads are still ` +\n 'listed in the Inbox and reached by campaign audiences.'}\n </Alert>\n ) : null}\n </Stack>\n </CardDisplay>\n <NewLeadDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n hostId={hostId ?? null}\n org={org as Record<string, unknown> | undefined}\n busy={createBusy}\n error={createError}\n roster={roster}\n orgId={orgId}\n onSubmit={(values) => void handleCreate(values)}\n />\n <AssignOwnerDialog\n lead={assigning}\n roster={roster}\n onClose={() => setAssigning(null)}\n onAssign={(uid) => {\n if (!assigning) return\n void writeLead(\n assigning,\n { ownerUid: uid || deleteField() },\n uid ? 'Owner assigned' : 'Owner cleared',\n )\n setAssigning(null)\n }}\n />\n <LeadUnqualifyDialog\n open={Boolean(unqualifying)}\n onClose={() => setUnqualifying(null)}\n hostId={hostId ?? ''}\n leadId={unqualifying?.leadId ?? ''}\n leadLabel={String(\n unqualifying?.['name'] || unqualifying?.['email'] || '',\n )}\n />\n {/* The site the conversion is filed as (AGL-2641), which since\n AGL-3275 is the first site that captured this person rather than\n \"the site the row lives under\" — a lead shared by a consent group\n lives under none of them in particular. Under a site the mounted\n one is used, and the two agree for every single-brand org. */}\n <LeadConvertDialog\n open={Boolean(converting)}\n onClose={() => setConverting(null)}\n hostId={\n hostId ??\n (Array.isArray(converting?.['capturedByHostIds'])\n ? String((converting['capturedByHostIds'] as string[])[0] ?? '')\n : '')\n }\n orgId={orgId}\n org={org as Record<string, unknown> | undefined}\n leadId={converting?.leadId ?? ''}\n lead={converting ?? {}}\n basePath={basePath ?? ''}\n roster={roster}\n />\n </>\n )\n}\nCrmLeadsSection.displayName = 'CrmLeadsSection'\n\n/**\n * The status, editable in place for a lead that is still open.\n *\n * A converted lead shows the chip alone: its status IS the conversion, and\n * the route stamped it. The select stops its click from reaching the row, or\n * every status change would also open the record.\n */\nfunction InlineStatus(props: {\n lead: LeadRow\n onChange: (next: CrmLeadStatus) => void\n}) {\n const { lead, onChange } = props\n if (lead.convertedContactId) {\n return (\n <Box sx={{ display: 'flex', alignItems: 'center', height: '100%' }}>\n <LeadStatusChip lead={lead} />\n </Box>\n )\n }\n const status = Aglyn.crmLeadStatus(lead)\n return (\n <Box\n onClick={(event) => event.stopPropagation()}\n sx={{\n display: 'flex',\n alignItems: 'center',\n height: '100%',\n width: '100%',\n }}\n >\n <Select\n size=\"small\"\n variant=\"standard\"\n disableUnderline\n value={status}\n onChange={(event) => onChange(event.target.value as CrmLeadStatus)}\n renderValue={() => <LeadStatusChip lead={lead} />}\n sx={{ width: '100%' }}\n >\n <MenuItem value=\"new\">{Aglyn.CRM_LEAD_STATUS_LABELS.new}</MenuItem>\n <MenuItem value=\"working\">\n {Aglyn.CRM_LEAD_STATUS_LABELS.working}\n </MenuItem>\n <MenuItem value=\"unqualified\">{`${Aglyn.CRM_LEAD_STATUS_LABELS.unqualified}…`}</MenuItem>\n </Select>\n </Box>\n )\n}\nInlineStatus.displayName = 'InlineStatus'\n\n/** Hand a lead to a team member. */\nfunction AssignOwnerDialog(props: {\n lead: LeadRow | null\n roster: OrgMemberOptions\n onClose: () => void\n onAssign: (uid: string) => void\n}) {\n const { lead, roster, onClose, onAssign } = props\n const [uid, setUid] = useState<string | null>(null)\n const current = uid ?? String(lead?.ownerUid ?? '')\n return (\n <Dialog\n open={Boolean(lead)}\n onClose={onClose}\n maxWidth=\"xs\"\n fullWidth\n slotProps={{ transition: { onExited: () => setUid(null) } }}\n >\n <DialogTitle>{`Assign ${String(lead?.['name'] || lead?.['email'] || 'lead')}`}</DialogTitle>\n <DialogContent>\n <Box sx={{ pt: 1 }}>\n <LeadOwnerSelect\n value={current}\n onChange={setUid}\n roster={roster}\n size=\"medium\"\n />\n </Box>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{'Cancel'}</Button>\n <Button variant=\"contained\" onClick={() => onAssign(current)}>\n {'Assign'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nAssignOwnerDialog.displayName = 'AssignOwnerDialog'\n\nexport default CrmLeadsSection\n"],"names":["Aglyn","mdiAccountArrowRight","mdiAccountCancelOutline","mdiAccountConvertOutline","mdiAccountTieOutline","mdiMagnify","CardDisplay","MdiIcon","ListPagination","ListTable","useOrgMemberOptions","useCrmOrgMount","useCrmSavedView","useCrmScope","useContactFieldDefinitions","customFieldColumns","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","useOrgLeads","CrmViewsControl","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","EmptyStateComponent","useSnackbar","useFirestore","useFirestoreCollection","useHostCampaigns","Alert","Box","Button","Dialog","DialogActions","DialogContent","DialogTitle","FormControl","InputAdornment","InputLabel","MenuItem","Select","Stack","TextField","Typography","collection","deleteField","doc","limit","orderBy","query","serverTimestamp","updateDoc","useRouter","useCallback","useEffect","useId","useMemo","useState","downloadTextFile","crmRoutes","LEAD_FILTER_LABELS","LEAD_EMAIL_FILTER_LABELS","LEAD_EMAIL_FILTERS","LEAD_FILTERS","leadMatchesCampaignFilter","leadMatchesEmailFilter","leadMatchesFilter","leadMatchesSearch","leadsCsv","LeadConvertDialog","leadSourceLabel","leadSources","leadTimeLabel","LeadImportButton","NewLeadDrawer","useCrmApi","LeadOwnerSelect","CONVERT_PENDING_ERASURE_REASON","CrmEmailStateChip","LeadStatusChip","LeadSurfacesNote","LeadUnqualifyDialog","LeadsBulkBar","OrgLeadSurfacesNote","LEADS_WINDOW","CrmLeadsSection","props","hostId","org","basePath","firestore","router","enqueueSnackbar","orgId","createHostId","mount","roster","leadFields","routes","site","idField","orgHostIds","hosts","map","host","id","orgLeads","visibleTo","windowSize","leadDocs","data","row","leadId","$id","status","hostsReady","length","truncated","window","slice","views","section","filter","value","state","filters","find","clause","field","includes","emailFilter","setFilter","next","setFilters","op","setEmailFilter","campaignFilter","setCampaignFilter","campaigns","enabled","Boolean","campaignName","options","option","label","filterLabelId","emailFilterLabelId","campaignFilterLabelId","search","setSearch","rows","lead","page","setPage","pageSize","setPageSize","pageRows","selectedIds","setSelectedIds","csvOptions","ownerEmail","emailFor","siteName","handleExport","assigning","setAssigning","unqualifying","setUnqualifying","converting","setConverting","crmApi","createOpen","setCreateOpen","createBusy","setCreateBusy","createError","setCreateError","handleCreate","values","response","payload","email","name","company","jobTitle","phone","website","leadSource","address","tags","campaignIds","ownerUid","notes","custom","ok","String","variant","persist","error","console","writeLead","fields","done","updatedAt","Error","message","columns","headerName","flex","minWidth","valueGetter","_value","renderCell","spacing","sx","justifyContent","height","noWrap","color","crmLeadStatus","InlineStatus","onChange","unqualifiedReason","readEmailState","EMAIL_STATE_LABELS","labelFor","held","Array","isArray","join","readCampaignIds","active","width","sortable","filterable","disableColumnMenu","converted","convertedContactId","erasurePending","readErasureRequestedAtMs","convertRefusal","isCrmLeadOpen","onClick","event","stopPropagation","display","alignItems","items","key","icon","path","size","href","disabled","disabledReason","undefined","grid","header","help","pluginDocsHelp","anchor","actions","direction","controller","allLabel","labelId","target","shrink","notched","displayEmpty","some","placeholder","slotProps","input","startAdornment","position","htmlInput","type","contentGutterX","contentGutterY","description","toLowerCase","trim","toLocaleString","selected","onSelectedChange","csv","columnOrder","slots","selectable","loading","onOpen","_id","push","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","quickFilter","hideFooter","rowCount","count","onPageChange","onPageSizeChange","severity","open","onClose","busy","onSubmit","AssignOwnerDialog","onAssign","uid","leadLabel","displayName","disableUnderline","renderValue","CRM_LEAD_STATUS_LABELS","new","working","unqualified","setUid","current","maxWidth","fullWidth","transition","onExited","pt"],"mappings":"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"}
@@ -19,14 +19,21 @@ import { type Firestore } from 'firebase/firestore';
19
19
  * THE LEADS STILL TO WORK, a site at a time (AGL-2624, across sites since
20
20
  * AGL-2634).
21
21
  *
22
- * A lead lives under its site `hosts/{hostId}/leads`, private by path —
23
- * so there is no org-level collection to count and no `orgId` on the
24
- * document to group by. The one figure is two server counts per site and
25
- * a subtraction: every lead, less the closed ones, because a lead nobody
22
+ * ONE PAIR OF COUNTS, not one pair per site (AGL-3275).
23
+ *
24
+ * This fanned out across the org's sites and summed, because a lead lived at
25
+ * `hosts/{hostId}/leads` and each site's collection was disjoint from every
26
+ * other's. On the org they share one collection, and a lead two brands both
27
+ * hold would be counted once for each of them — so the sum would report a
28
+ * multi-brand org more open leads than it has. A single scoped count cannot
29
+ * double-count, and costs two reads instead of two per site.
30
+ *
31
+ * The figure is still every lead less the closed ones, because a lead nobody
26
32
  * has touched carries no status and Firestore cannot select on a field's
27
- * absence (`openLeadsFromCounts`). Handed one site this is the glance
28
- * card's figure as it always was; handed the org's sites it is the org
29
- * level's total, and the fan-out is bounded by the org's site list rather
30
- * than by the data.
33
+ * absence (`openLeadsFromCounts`).
34
+ *
35
+ * `visibleTo` is the scope clause: a site's tokens for the glance card, and
36
+ * `null` at the organization level, where an org-wide member's reads carry no
37
+ * clause at all.
31
38
  */
32
- export declare function openLeadsAcrossSites(firestore: Firestore, hostIds: readonly string[]): Promise<number>;
39
+ export declare function openLeadsForScope(firestore: Firestore, orgId: string, visibleTo: readonly string[] | null): Promise<number>;
@@ -19,26 +19,37 @@ import { collection, getCountFromServer, query, where } from "firebase/firestore
19
19
  * THE LEADS STILL TO WORK, a site at a time (AGL-2624, across sites since
20
20
  * AGL-2634).
21
21
  *
22
- * A lead lives under its site `hosts/{hostId}/leads`, private by path —
23
- * so there is no org-level collection to count and no `orgId` on the
24
- * document to group by. The one figure is two server counts per site and
25
- * a subtraction: every lead, less the closed ones, because a lead nobody
22
+ * ONE PAIR OF COUNTS, not one pair per site (AGL-3275).
23
+ *
24
+ * This fanned out across the org's sites and summed, because a lead lived at
25
+ * `hosts/{hostId}/leads` and each site's collection was disjoint from every
26
+ * other's. On the org they share one collection, and a lead two brands both
27
+ * hold would be counted once for each of them — so the sum would report a
28
+ * multi-brand org more open leads than it has. A single scoped count cannot
29
+ * double-count, and costs two reads instead of two per site.
30
+ *
31
+ * The figure is still every lead less the closed ones, because a lead nobody
26
32
  * has touched carries no status and Firestore cannot select on a field's
27
- * absence (`openLeadsFromCounts`). Handed one site this is the glance
28
- * card's figure as it always was; handed the org's sites it is the org
29
- * level's total, and the fan-out is bounded by the org's site list rather
30
- * than by the data.
31
- */ export async function openLeadsAcrossSites(firestore, hostIds) {
33
+ * absence (`openLeadsFromCounts`).
34
+ *
35
+ * `visibleTo` is the scope clause: a site's tokens for the glance card, and
36
+ * `null` at the organization level, where an org-wide member's reads carry no
37
+ * clause at all.
38
+ */ export async function openLeadsForScope(firestore, orgId, visibleTo) {
32
39
  const countOf = (target)=>getCountFromServer(target).then((snapshot)=>snapshot.data().count);
33
- const perSite = await Promise.all(hostIds.map(async (hostId)=>{
34
- const leads = collection(firestore, 'hosts', hostId, 'leads');
35
- const [total, closed] = await Promise.all([
36
- countOf(query(leads)),
37
- countOf(query(leads, where('status', 'in', CRM_LEAD_CLOSED_STATUSES)))
38
- ]);
39
- return openLeadsFromCounts(total, closed);
40
- }));
41
- return perSite.reduce((sum, count)=>sum + count, 0);
40
+ const leads = collection(firestore, 'orgs', orgId, 'leads');
41
+ // An `array-contains-any` over nothing is a query Firestore refuses.
42
+ if (visibleTo && !visibleTo.length) return 0;
43
+ const scope = visibleTo ? [
44
+ where('visibleTo', 'array-contains-any', [
45
+ ...visibleTo
46
+ ])
47
+ ] : [];
48
+ const [total, closed] = await Promise.all([
49
+ countOf(query(leads, ...scope)),
50
+ countOf(query(leads, ...scope, where('status', 'in', CRM_LEAD_CLOSED_STATUSES)))
51
+ ]);
52
+ return openLeadsFromCounts(total, closed);
42
53
  }
43
54
 
44
55
  //# sourceMappingURL=lead-counts.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/lead-counts.ts"],"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\nimport { CRM_LEAD_CLOSED_STATUSES, openLeadsFromCounts } from '@aglyn/aglyn'\nimport {\n collection,\n type Firestore,\n getCountFromServer,\n query,\n where,\n} from 'firebase/firestore'\n\n/**\n * THE LEADS STILL TO WORK, a site at a time (AGL-2624, across sites since\n * AGL-2634).\n *\n * A lead lives under its site `hosts/{hostId}/leads`, private by path —\n * so there is no org-level collection to count and no `orgId` on the\n * document to group by. The one figure is two server counts per site and\n * a subtraction: every lead, less the closed ones, because a lead nobody\n * has touched carries no status and Firestore cannot select on a field's\n * absence (`openLeadsFromCounts`). Handed one site this is the glance\n * card's figure as it always was; handed the org's sites it is the org\n * level's total, and the fan-out is bounded by the org's site list rather\n * than by the data.\n */\nexport async function openLeadsAcrossSites(\n firestore: Firestore,\n hostIds: readonly string[],\n): Promise<number> {\n const countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n const perSite = await Promise.all(\n hostIds.map(async (hostId) => {\n const leads = collection(firestore, 'hosts', hostId, 'leads')\n const [total, closed] = await Promise.all([\n countOf(query(leads)),\n countOf(query(leads, where('status', 'in', CRM_LEAD_CLOSED_STATUSES))),\n ])\n return openLeadsFromCounts(total, closed)\n }),\n )\n return perSite.reduce((sum, count) => sum + count, 0)\n}\n"],"names":["CRM_LEAD_CLOSED_STATUSES","openLeadsFromCounts","collection","getCountFromServer","query","where","openLeadsAcrossSites","firestore","hostIds","countOf","target","then","snapshot","data","count","perSite","Promise","all","map","hostId","leads","total","closed","reduce","sum"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,wBAAwB,EAAEC,mBAAmB,QAAQ,eAAc;AAC5E,SACEC,UAAU,EAEVC,kBAAkB,EAClBC,KAAK,EACLC,KAAK,QACA,qBAAoB;AAE3B;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAeC,qBACpBC,SAAoB,EACpBC,OAA0B;IAE1B,MAAMC,UAAU,CAACC,SACfP,mBAAmBO,QAAQC,IAAI,CAAC,CAACC,WAAaA,SAASC,IAAI,GAAGC,KAAK;IACrE,MAAMC,UAAU,MAAMC,QAAQC,GAAG,CAC/BT,QAAQU,GAAG,CAAC,OAAOC;QACjB,MAAMC,QAAQlB,WAAWK,WAAW,SAASY,QAAQ;QACrD,MAAM,CAACE,OAAOC,OAAO,GAAG,MAAMN,QAAQC,GAAG,CAAC;YACxCR,QAAQL,MAAMgB;YACdX,QAAQL,MAAMgB,OAAOf,MAAM,UAAU,MAAML;SAC5C;QACD,OAAOC,oBAAoBoB,OAAOC;IACpC;IAEF,OAAOP,QAAQQ,MAAM,CAAC,CAACC,KAAKV,QAAUU,MAAMV,OAAO;AACrD"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/lead-counts.ts"],"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\nimport { CRM_LEAD_CLOSED_STATUSES, openLeadsFromCounts } from '@aglyn/aglyn'\nimport {\n collection,\n type Firestore,\n getCountFromServer,\n query,\n where,\n} from 'firebase/firestore'\n\n/**\n * THE LEADS STILL TO WORK, a site at a time (AGL-2624, across sites since\n * AGL-2634).\n *\n * ONE PAIR OF COUNTS, not one pair per site (AGL-3275).\n *\n * This fanned out across the org's sites and summed, because a lead lived at\n * `hosts/{hostId}/leads` and each site's collection was disjoint from every\n * other's. On the org they share one collection, and a lead two brands both\n * hold would be counted once for each of them — so the sum would report a\n * multi-brand org more open leads than it has. A single scoped count cannot\n * double-count, and costs two reads instead of two per site.\n *\n * The figure is still every lead less the closed ones, because a lead nobody\n * has touched carries no status and Firestore cannot select on a field's\n * absence (`openLeadsFromCounts`).\n *\n * `visibleTo` is the scope clause: a site's tokens for the glance card, and\n * `null` at the organization level, where an org-wide member's reads carry no\n * clause at all.\n */\nexport async function openLeadsForScope(\n firestore: Firestore,\n orgId: string,\n visibleTo: readonly string[] | null,\n): Promise<number> {\n const countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n const leads = collection(firestore, 'orgs', orgId, 'leads')\n // An `array-contains-any` over nothing is a query Firestore refuses.\n if (visibleTo && !visibleTo.length) return 0\n const scope = visibleTo\n ? [where('visibleTo', 'array-contains-any', [...visibleTo])]\n : []\n const [total, closed] = await Promise.all([\n countOf(query(leads, ...scope)),\n countOf(\n query(leads, ...scope, where('status', 'in', CRM_LEAD_CLOSED_STATUSES)),\n ),\n ])\n return openLeadsFromCounts(total, closed)\n}\n"],"names":["CRM_LEAD_CLOSED_STATUSES","openLeadsFromCounts","collection","getCountFromServer","query","where","openLeadsForScope","firestore","orgId","visibleTo","countOf","target","then","snapshot","data","count","leads","length","scope","total","closed","Promise","all"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,wBAAwB,EAAEC,mBAAmB,QAAQ,eAAc;AAC5E,SACEC,UAAU,EAEVC,kBAAkB,EAClBC,KAAK,EACLC,KAAK,QACA,qBAAoB;AAE3B;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,eAAeC,kBACpBC,SAAoB,EACpBC,KAAa,EACbC,SAAmC;IAEnC,MAAMC,UAAU,CAACC,SACfR,mBAAmBQ,QAAQC,IAAI,CAAC,CAACC,WAAaA,SAASC,IAAI,GAAGC,KAAK;IACrE,MAAMC,QAAQd,WAAWK,WAAW,QAAQC,OAAO;IACnD,qEAAqE;IACrE,IAAIC,aAAa,CAACA,UAAUQ,MAAM,EAAE,OAAO;IAC3C,MAAMC,QAAQT,YACV;QAACJ,MAAM,aAAa,sBAAsB;eAAII;SAAU;KAAE,GAC1D,EAAE;IACN,MAAM,CAACU,OAAOC,OAAO,GAAG,MAAMC,QAAQC,GAAG,CAAC;QACxCZ,QAAQN,MAAMY,UAAUE;QACxBR,QACEN,MAAMY,UAAUE,OAAOb,MAAM,UAAU,MAAML;KAEhD;IACD,OAAOC,oBAAoBkB,OAAOC;AACpC"}
@@ -52,8 +52,19 @@ const LEAD_FUNNEL_HELP = Aglyn.pluginDocsHelp('crmReports', {
52
52
  anchor: '#lead-funnel',
53
53
  excerpt: 'Of the leads this site captured in the period, how many are ' + 'still new, being worked, qualified or unqualified — and the ' + 'reasons the unqualified ones were closed. Placed by when each ' + 'lead was first seen.'
54
54
  });
55
- /** One site's leads first seen in a range — a single-field range, no composite index. */ function leadsBetween(firestore, hostId, from, to) {
56
- return query(collection(firestore, 'hosts', hostId, 'leads'), where('firstSeenAtMs', '>=', from), where('firstSeenAtMs', '<', to));
55
+ /**
56
+ * The leads a scope may see, first seen in a range (AGL-3275).
57
+ *
58
+ * `visibleTo` is the site's tokens under a site, and `null` at the
59
+ * organization level where an org-wide member reads without a clause. The
60
+ * range stays a single-field one; with the scope clause it composes with the
61
+ * `visibleTo` + `firstSeenAtMs` entry in `cloud/firebase-firestore.indexes.json`.
62
+ */ function leadsBetween(firestore, orgId, visibleTo, from, to) {
63
+ return query(collection(firestore, 'orgs', orgId, 'leads'), ...visibleTo ? [
64
+ where('visibleTo', 'array-contains-any', [
65
+ ...visibleTo
66
+ ])
67
+ ] : [], where('firstSeenAtMs', '>=', from), where('firstSeenAtMs', '<', to));
57
68
  }
58
69
  const countOf = (target)=>getCountFromServer(target).then((snapshot)=>snapshot.data().count);
59
70
  /**
@@ -111,8 +122,8 @@ const countOf = (target)=>getCountFromServer(target).then((snapshot)=>snapshot.d
111
122
  const { range } = report;
112
123
  const firestore = useFirestore();
113
124
  const captured = useAggregateRead(()=>Promise.all([
114
- countOf(leadsBetween(firestore, hostId, range.from, range.to)),
115
- countOf(leadsBetween(firestore, hostId, range.previousFrom, range.previousTo))
125
+ countOf(leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to)),
126
+ countOf(leadsBetween(firestore, report.scope[1], report.tokens, range.previousFrom, range.previousTo))
116
127
  ]).then(([current, previous])=>({
117
128
  current,
118
129
  previous
@@ -123,7 +134,7 @@ const countOf = (target)=>getCountFromServer(target).then((snapshot)=>snapshot.d
123
134
  ], {
124
135
  cacheKey: reportCacheKey(report, `leads:counts:${hostId}`)
125
136
  });
126
- const window = useWindowRead(()=>query(leadsBetween(firestore, hostId, range.from, range.to), orderBy('firstSeenAtMs', 'desc'), limit(LEAD_CEILING + 1)), LEAD_CEILING, [
137
+ const window = useWindowRead(()=>query(leadsBetween(firestore, report.scope[1], report.tokens, range.from, range.to), orderBy('firstSeenAtMs', 'desc'), limit(LEAD_CEILING + 1)), LEAD_CEILING, [
127
138
  firestore,
128
139
  hostId,
129
140
  range
@@ -142,17 +153,14 @@ const countOf = (target)=>getCountFromServer(target).then((snapshot)=>snapshot.d
142
153
  }
143
154
  SiteLeadFunnelCard.displayName = 'SiteLeadFunnelCard';
144
155
  /**
145
- * THE CROSS-SITE FUNNEL (AGL-2634): every site of the organization, one
146
- * window each, totaled.
156
+ * THE ORGANIZATION'S FUNNEL (AGL-2634, AGL-3275): every lead the org holds.
147
157
  *
148
- * The counts are one server aggregate per site per period, summed. The
149
- * windows are one bounded read per site the site card's own query, the
150
- * same ceiling merged newest-seen first and NOT cut again: the funnel
151
- * is a placement of leads by status, and cutting the merged list would
152
- * place a busy site's leads and drop a quiet site's. What the ceiling
153
- * bounds is the read per site, and the caption says so when any site had
154
- * more than its window. Held until the mount's site list has settled, so
155
- * a first round against a partial list is not answered and remembered.
158
+ * This totaled one aggregate and one window PER SITE, because the sites'
159
+ * lead collections were disjoint. They share one collection now, so a sum
160
+ * would count a lead that two brands in a consent group both hold once for
161
+ * each of them and the merge that existed so a busy site could not crowd
162
+ * out a quiet one has nothing left to balance. One aggregate and one
163
+ * bounded read, unscoped, as an org-wide member's reads are.
156
164
  */ function OrgLeadFunnelCard(props) {
157
165
  var _ref, _ref1;
158
166
  const { report } = props;
@@ -163,36 +171,32 @@ SiteLeadFunnelCard.displayName = 'SiteLeadFunnelCard';
163
171
  mount
164
172
  ]);
165
173
  const key = (_ref = hostIds == null ? void 0 : hostIds.join('\n')) != null ? _ref : '';
166
- const captured = useAggregateRead(()=>hostIds ? Promise.all(hostIds.map((hostId)=>Promise.all([
167
- countOf(leadsBetween(firestore, hostId, range.from, range.to)),
168
- countOf(leadsBetween(firestore, hostId, range.previousFrom, range.previousTo))
169
- ]))).then((pairs)=>pairs.reduce((sum, [current, previous])=>({
170
- current: sum.current + current,
171
- previous: sum.previous + previous
172
- }), {
173
- current: 0,
174
- previous: 0
175
- })) : null, [
174
+ /*
175
+ * ONE READ, not one per site (AGL-3275). The sites share a collection now,
176
+ * so a lead two brands in a consent group both hold would be counted once
177
+ * for each of them — and the org's funnel would report more leads than the
178
+ * org has. `null` tokens is the org-wide read that carries no clause.
179
+ */ const captured = useAggregateRead(()=>Promise.all([
180
+ countOf(leadsBetween(firestore, report.scope[1], null, range.from, range.to)),
181
+ countOf(leadsBetween(firestore, report.scope[1], null, range.previousFrom, range.previousTo))
182
+ ]).then(([current, previous])=>({
183
+ current,
184
+ previous
185
+ })), [
176
186
  firestore,
177
- key,
187
+ report.scope,
178
188
  range
179
189
  ], {
180
- cacheKey: reportCacheKey(report, `leads:counts:org:${key}`)
190
+ cacheKey: reportCacheKey(report, 'leads:counts:org')
181
191
  });
182
- const merged = useAggregateRead(()=>hostIds ? Promise.all(hostIds.map((hostId)=>getDocs(query(leadsBetween(firestore, hostId, range.from, range.to), orderBy('firstSeenAtMs', 'desc'), limit(LEAD_CEILING + 1))).then((snapshot)=>ceilingedWindow(snapshot.docs.map((document)=>_extends({}, document.data(), {
183
- $id: `${hostId}/${document.id}`
184
- })), LEAD_CEILING)))).then((windows)=>({
185
- rows: windows.flatMap((window)=>window.rows).sort((a, b)=>{
186
- var _b_firstSeenAtMs, _a_firstSeenAtMs;
187
- return Number((_b_firstSeenAtMs = b['firstSeenAtMs']) != null ? _b_firstSeenAtMs : 0) - Number((_a_firstSeenAtMs = a['firstSeenAtMs']) != null ? _a_firstSeenAtMs : 0);
188
- }),
189
- truncated: windows.some((window)=>window.truncated)
190
- })) : null, [
192
+ const merged = useAggregateRead(()=>getDocs(query(leadsBetween(firestore, report.scope[1], null, range.from, range.to), orderBy('firstSeenAtMs', 'desc'), limit(LEAD_CEILING + 1))).then((snapshot)=>ceilingedWindow(snapshot.docs.map((document)=>_extends({}, document.data(), {
193
+ $id: document.id
194
+ })), LEAD_CEILING)), [
191
195
  firestore,
192
- key,
196
+ report.scope,
193
197
  range
194
198
  ], {
195
- cacheKey: reportCacheKey(report, `leads:window:org:${key}`)
199
+ cacheKey: reportCacheKey(report, 'leads:window:org')
196
200
  });
197
201
  const window = useMemo(()=>{
198
202
  var _ref, _ref1;
@@ -213,8 +217,8 @@ SiteLeadFunnelCard.displayName = 'SiteLeadFunnelCard';
213
217
  subheader: `Every site (${sites.toLocaleString()})`,
214
218
  captured: captured,
215
219
  window: window,
216
- caption: sampled ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` + ` across ${plural(sites, 'site')} — at most ${LEAD_CEILING.toLocaleString()} per site` + (figures !== null ? ` — of ${figures.current.toLocaleString()}` : '') + ' in the period; the captured tile is counted on the server.' : undefined,
217
- errorText: 'Some site’s leads could not be read.'
220
+ caption: sampled ? `Placed from the ${window.rows.length.toLocaleString()} most recently captured leads` + ` — at most ${LEAD_CEILING.toLocaleString()}` + (figures !== null ? ` — of ${figures.current.toLocaleString()}` : '') + ' in the period; the captured tile is counted on the server.' : undefined,
221
+ errorText: 'The organization’s leads could not be read.'
218
222
  });
219
223
  }
220
224
  OrgLeadFunnelCard.displayName = 'OrgLeadFunnelCard';