@aglyn/plugins-crm 1.0.0-beta.160 → 1.0.0-beta.161

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-crm",
3
- "version": "1.0.0-beta.160",
3
+ "version": "1.0.0-beta.161",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,18 +25,18 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.160",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.160",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.160",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.160",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.160",
33
- "@aglyn/shared-ui-next": "1.0.0-beta.160",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.160",
35
- "@aglyn/shared-util-email": "1.0.0-beta.160",
36
- "@aglyn/shared-util-http": "1.0.0-beta.160",
37
- "@aglyn/tenant-data-admin": "1.0.0-beta.160",
38
- "@aglyn/tenant-feature-instance": "1.0.0-beta.160",
39
- "@aglyn/tenant-runtime": "1.0.0-beta.160",
28
+ "@aglyn/aglyn": "1.0.0-beta.161",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.161",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.161",
31
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.161",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.161",
33
+ "@aglyn/shared-ui-next": "1.0.0-beta.161",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.161",
35
+ "@aglyn/shared-util-email": "1.0.0-beta.161",
36
+ "@aglyn/shared-util-http": "1.0.0-beta.161",
37
+ "@aglyn/tenant-data-admin": "1.0.0-beta.161",
38
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.161",
39
+ "@aglyn/tenant-runtime": "1.0.0-beta.161",
40
40
  "@dnd-kit/core": "^6.3.1",
41
41
  "@dnd-kit/utilities": "^3.2.2",
42
42
  "@swc/helpers": "0.5.23",
@@ -17,7 +17,7 @@
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
19
  import * as Aglyn from "@aglyn/aglyn";
20
- import { mdiAccountArrowRight, mdiAccountCancelOutline, mdiAccountConvertOutline, mdiAccountTieOutline } from "@aglyn/shared-data-mdi";
20
+ import { mdiAccountArrowRight, mdiAccountCancelOutline, mdiAccountConvertOutline, mdiAccountTieOutline, mdiMagnify } from "@aglyn/shared-data-mdi";
21
21
  import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
22
22
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
23
23
  import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
@@ -34,13 +34,13 @@ import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagina
34
34
  import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.component";
35
35
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
36
36
  import { useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
37
- import { Alert, Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, InputLabel, MenuItem, Select, Stack, Typography } from "@mui/material";
37
+ import { Alert, Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, InputAdornment, InputLabel, MenuItem, Select, Stack, TextField, Typography } from "@mui/material";
38
38
  import { collection, deleteField, doc, limit, orderBy, query, serverTimestamp, updateDoc } from "firebase/firestore";
39
39
  import { useRouter } from "next/navigation";
40
40
  import { useCallback, useEffect, useId, useMemo, useState } from "react";
41
41
  import { downloadTextFile } from "../model/contacts-csv.js";
42
42
  import { crmRoutes } from "../model/crm-routes.js";
43
- import { LEAD_FILTER_LABELS, LEAD_FILTERS, leadMatchesFilter } from "../model/lead-filters.js";
43
+ import { LEAD_FILTER_LABELS, LEAD_FILTERS, leadMatchesFilter, leadMatchesSearch } from "../model/lead-filters.js";
44
44
  import { leadsCsv } from "../model/leads-csv.js";
45
45
  import { LeadConvertDialog } from "./lead-convert-dialog.js";
46
46
  import { leadSourceLabel, leadSources, leadTimeLabel } from "./lead-history-card.js";
@@ -167,18 +167,30 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
167
167
  // The label's id, so the filter's combobox is named "Show" rather than
168
168
  // after the option it shows — see `LeadOwnerSelect`.
169
169
  const filterLabelId = useId();
170
- const rows = useMemo(()=>window.filter((lead)=>leadMatchesFilter(lead, filter)), [
170
+ /*
171
+ * The search box is the SECTION'S, not the grid's (AGL-3246). The grid's
172
+ * quick filter runs over the rows the grid holds, and the grid holds one
173
+ * PAGE of the window — so a lead on page three answered "no match" while
174
+ * the footer below went on counting the unfiltered window. The term
175
+ * narrows the whole loaded window here, beside the status filter and
176
+ * before the footer's count and the page slice, over the fields a person
177
+ * types to find a lead: name, email, company, title and tags.
178
+ */ const [search, setSearch] = useState('');
179
+ const rows = useMemo(()=>window.filter((lead)=>leadMatchesFilter(lead, filter) && leadMatchesSearch(lead, search)), [
171
180
  window,
172
- filter
181
+ filter,
182
+ search
173
183
  ]);
174
184
  const [page, setPage] = useState(0);
175
185
  const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
176
- // A new filter starts on page one: page three of the open leads is not a
177
- // page of the unqualified ones, and an out-of-range page renders empty.
186
+ // A new filter or search term starts on page one: page three of the open
187
+ // leads is not a page of the unqualified ones, and an out-of-range page
188
+ // renders empty.
178
189
  useEffect(()=>{
179
190
  setPage(0);
180
191
  }, [
181
- filter
192
+ filter,
193
+ search
182
194
  ]);
183
195
  const pageRows = useMemo(()=>rows.slice(page * pageSize, (page + 1) * pageSize), [
184
196
  rows,
@@ -186,13 +198,14 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
186
198
  pageSize
187
199
  ]);
188
200
  /*
189
- * The ticked rows, for the bulk bar (AGL-2662). Cleared when the filter
190
- * changes: a selection made on the open leads is not a selection of the
191
- * unqualified ones, and the bar's count would be over rows no longer
192
- * listed.
201
+ * The ticked rows, for the bulk bar (AGL-2662). Cleared when the filter or
202
+ * the search term changes: a selection made on the open leads is not a
203
+ * selection of the unqualified ones, and the bar's count would be over
204
+ * rows no longer listed.
193
205
  */ const [selectedIds, setSelectedIds] = useState([]);
194
206
  useEffect(()=>setSelectedIds([]), [
195
- filter
207
+ filter,
208
+ search
196
209
  ]);
197
210
  // How the file names the owner and, at the org level, the site.
198
211
  const csvOptions = useMemo(()=>_extends({
@@ -204,7 +217,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
204
217
  hostId,
205
218
  mount
206
219
  ]);
207
- // The listed window — every row the filter admits, not just the page.
220
+ // The listed window — every row the filter and the search admit, not just
221
+ // the page.
208
222
  const handleExport = useCallback(()=>{
209
223
  downloadTextFile('leads.csv', 'text/csv', leadsCsv(rows, csvOptions));
210
224
  }, [
@@ -538,6 +552,30 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
538
552
  })
539
553
  ]
540
554
  }),
555
+ /*#__PURE__*/ _jsx(TextField, {
556
+ size: "small",
557
+ value: search,
558
+ onChange: (event)=>setSearch(event.target.value),
559
+ placeholder: "Search leads",
560
+ slotProps: {
561
+ input: {
562
+ startAdornment: /*#__PURE__*/ _jsx(InputAdornment, {
563
+ position: "start",
564
+ children: /*#__PURE__*/ _jsx(MdiIcon, {
565
+ path: mdiMagnify.path,
566
+ size: 0.8
567
+ })
568
+ })
569
+ },
570
+ htmlInput: {
571
+ 'aria-label': 'Search leads',
572
+ type: 'search'
573
+ }
574
+ },
575
+ sx: {
576
+ minWidth: 200
577
+ }
578
+ }),
541
579
  /*#__PURE__*/ _jsx(LeadImportButton, {
542
580
  hostId: hostId
543
581
  }),
@@ -569,7 +607,7 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
569
607
  }) : status === 'success' && rows.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
570
608
  variant: "body2",
571
609
  color: "text.secondary",
572
- children: `No ${LEAD_FILTER_LABELS[filter].toLowerCase()} leads among the ` + `${window.length.toLocaleString()} most recently seen.`
610
+ children: `No ${LEAD_FILTER_LABELS[filter].toLowerCase()} leads` + (search.trim() ? ` match “${search.trim()}”` : '') + ` among the ${window.length.toLocaleString()} most recently seen.`
573
611
  }) : /*#__PURE__*/ _jsxs(_Fragment, {
574
612
  children: [
575
613
  /*#__PURE__*/ _jsx(LeadsBulkBar, {
@@ -598,6 +636,9 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
598
636
  onColumnVisibilityModelChange: grid.onColumnVisibilityModelChange,
599
637
  sortModel: grid.sortModel,
600
638
  onSortModelChange: grid.onSortModelChange,
639
+ // The search is the section's, above: the grid's own box
640
+ // would search this page alone.
641
+ quickFilter: false,
601
642
  // Paged by the footer below, so the grid must not also slice.
602
643
  hideFooter: true
603
644
  })
@@ -614,7 +655,7 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
614
655
  }),
615
656
  truncated ? /*#__PURE__*/ _jsx(Alert, {
616
657
  severity: "info",
617
- children: `Showing the ${LEADS_WINDOW.toLocaleString()} most recently seen ` + 'leads. The status filter narrows these; older leads are still ' + 'listed in the Inbox and reached by campaign audiences.'
658
+ children: `Showing the ${LEADS_WINDOW.toLocaleString()} most recently seen ` + 'leads. The search box and the status filter narrow these ' + `${LEADS_WINDOW.toLocaleString()} only; older leads are still ` + 'listed in the Inbox and reached by campaign audiences.'
618
659
  }) : null
619
660
  ]
620
661
  })
@@ -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 { ConsolePluginPageProps, CrmLeadFields, CrmLeadStatus } from '@aglyn/aglyn'\nimport {\n mdiAccountArrowRight,\n mdiAccountCancelOutline,\n mdiAccountConvertOutline,\n mdiAccountTieOutline,\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 { 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} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Box,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControl,\n InputLabel,\n MenuItem,\n Select,\n Stack,\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_FILTERS,\n type LeadFilter,\n leadMatchesFilter,\n} from '../model/lead-filters'\nimport { type LeadCsvOptions, leadsCsv } from '../model/leads-csv'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport { leadSourceLabel, leadSources, leadTimeLabel } 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 { 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 const routes = crmRoutes(basePath ?? '')\n\n // Under a site: the site's own window, rows keyed by document id.\n const site = useFirestoreCollection<Record<string, unknown> & CrmLeadFields & { $id: string }>(\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({ hostIds: orgHostIds, windowSize: LEADS_WINDOW })\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((clause) => clause.field === 'status')?.value\n return (LEAD_FILTERS as readonly string[]).includes(value ?? '')\n ? (value as LeadFilter)\n : 'open'\n }, [views.state.filters])\n const setFilter = useCallback(\n (next: LeadFilter) =>\n views.setFilters(\n next === 'open' ? [] : [{ field: 'status', op: 'equals', value: next }],\n ),\n [views.setFilters],\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 rows = useMemo(\n () => window.filter((lead) => leadMatchesFilter(lead, filter)),\n [window, filter],\n )\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n // A new filter starts on page one: page three of the open leads is not a\n // page of the unqualified ones, and an out-of-range page renders empty.\n useEffect(() => {\n setPage(0)\n }, [filter])\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\n * changes: a selection made on the open leads is not a selection of the\n * unqualified ones, and the bar's count would be over rows no longer\n * listed.\n */\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n useEffect(() => setSelectedIds([]), [filter])\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 admits, not just 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.ownerUid ? { ownerUid: values.ownerUid } : {}),\n ...(values.notes ? { notes: values.notes } : {}),\n status: values.status,\n })\n if (!response.ok) {\n // The route's own sentence, shown above the form unchanged.\n setCreateError(String(payload['error'] ?? 'The lead could not be added.'))\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(doc(firestore, 'hosts', lead.hostId, 'leads', lead.leadId), {\n ...fields,\n updatedAt: serverTimestamp(),\n })\n enqueueSnackbar(done, { variant: 'success', persist: false })\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : '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) => String(row['name'] || row['email'] || ''),\n renderCell: ({ row }: { row: LeadRow }) => (\n <Stack spacing={0} sx={{ minWidth: 0, justifyContent: 'center', height: '100%' }}>\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 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 {\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 {\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: <MdiIcon path={mdiAccountArrowRight.path} size={0.8} />,\n href: routes.lead(row.leadId, hostId ? null : row.hostId),\n },\n {\n key: 'convert',\n label: 'Convert…',\n icon: <MdiIcon path={mdiAccountConvertOutline.path} size={0.8} />,\n onClick: () => setConverting(row),\n disabled: convertRefusal !== null,\n disabledReason: convertRefusal ?? undefined,\n },\n {\n key: 'assign',\n label: 'Assign owner',\n icon: <MdiIcon path={mdiAccountTieOutline.path} size={0.8} />,\n onClick: () => setAssigning(row),\n },\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />,\n onClick: () => setUnqualifying(row),\n disabled: !Aglyn.isCrmLeadOpen(row) || 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],\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', { anchor: '#whats-in-the-crm-area' })}\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) => setFilter(event.target.value as LeadFilter)}\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 <LeadImportButton hostId={hostId} />\n <Button size=\"small\" onClick={handleExport} disabled={!rows.length}>\n {'Export CSV'}\n </Button>\n <Button size=\"small\" variant=\"contained\" onClick={() => setCreateOpen(true)}>\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 ? <LeadSurfacesNote hostId={hostId} /> : <OrgLeadSurfacesNote />}\n {status === 'success' && window.length === 0 ? (\n <EmptyStateComponent\n label={'No leads yet'}\n description={'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 ) : status === 'success' && rows.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`No ${LEAD_FILTER_LABELS[filter].toLowerCase()} leads among the ` +\n `${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 />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={pageRows}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n loading={status === 'loading'}\n onOpen={(_id, row: LeadRow) =>\n router.push(routes.lead(row.leadId, hostId ? null : row.hostId))\n }\n // Columns and sort are the view's, controlled (AGL-2617).\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={grid.onColumnVisibilityModelChange}\n sortModel={grid.sortModel}\n onSortModelChange={grid.onSortModelChange}\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 status filter narrows these; 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 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(unqualifying?.['name'] || unqualifying?.['email'] || '')}\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={{ display: 'flex', alignItems: 'center', height: '100%', width: '100%' }}\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\">{Aglyn.CRM_LEAD_STATUS_LABELS.working}</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 value={current} onChange={setUid} roster={roster} size=\"medium\" />\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","CardDisplay","MdiIcon","ListPagination","ListTable","useOrgMemberOptions","useCrmOrgMount","useCrmSavedView","useCrmScope","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","useOrgLeads","CrmViewsControl","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","EmptyStateComponent","useSnackbar","useFirestore","useFirestoreCollection","Alert","Box","Button","Dialog","DialogActions","DialogContent","DialogTitle","FormControl","InputLabel","MenuItem","Select","Stack","Typography","collection","deleteField","doc","limit","orderBy","query","serverTimestamp","updateDoc","useRouter","useCallback","useEffect","useId","useMemo","useState","downloadTextFile","crmRoutes","LEAD_FILTER_LABELS","LEAD_FILTERS","leadMatchesFilter","leadsCsv","LeadConvertDialog","leadSourceLabel","leadSources","leadTimeLabel","LeadImportButton","NewLeadDrawer","useCrmApi","LeadOwnerSelect","CONVERT_PENDING_ERASURE_REASON","LeadStatusChip","LeadSurfacesNote","LeadUnqualifyDialog","LeadsBulkBar","OrgLeadSurfacesNote","LEADS_WINDOW","CrmLeadsSection","props","unqualifying","converting","hostId","org","basePath","firestore","router","enqueueSnackbar","orgId","createHostId","mount","roster","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","setFilter","next","setFilters","op","filterLabelId","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","ownerUid","notes","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","labelFor","join","width","sortable","filterable","disableColumnMenu","converted","Boolean","convertedContactId","erasurePending","readErasureRequestedAtMs","convertRefusal","isCrmLeadOpen","onClick","event","stopPropagation","display","alignItems","label","items","key","icon","path","size","href","disabled","disabledReason","undefined","grid","header","help","pluginDocsHelp","anchor","actions","direction","controller","allLabel","labelId","target","option","contentGutterX","contentGutterY","description","toLowerCase","toLocaleString","selected","onSelectedChange","csv","columnOrder","slots","selectable","loading","onOpen","_id","push","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","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","slotProps","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 { ConsolePluginPageProps, CrmLeadFields, CrmLeadStatus } 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 { 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} 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_FILTERS,\n type LeadFilter,\n leadMatchesFilter,\n leadMatchesSearch,\n} from '../model/lead-filters'\nimport { type LeadCsvOptions, leadsCsv } from '../model/leads-csv'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport { leadSourceLabel, leadSources, leadTimeLabel } 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 { 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 const routes = crmRoutes(basePath ?? '')\n\n // Under a site: the site's own window, rows keyed by document id.\n const site = useFirestoreCollection<Record<string, unknown> & CrmLeadFields & { $id: string }>(\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({ hostIds: orgHostIds, windowSize: LEADS_WINDOW })\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((clause) => clause.field === 'status')?.value\n return (LEAD_FILTERS as readonly string[]).includes(value ?? '')\n ? (value as LeadFilter)\n : 'open'\n }, [views.state.filters])\n const setFilter = useCallback(\n (next: LeadFilter) =>\n views.setFilters(\n next === 'open' ? [] : [{ field: 'status', op: 'equals', value: next }],\n ),\n [views.setFilters],\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 /*\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) => leadMatchesFilter(lead, filter) && leadMatchesSearch(lead, search),\n ),\n [window, filter, 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, 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(() => setSelectedIds([]), [filter, search])\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.ownerUid ? { ownerUid: values.ownerUid } : {}),\n ...(values.notes ? { notes: values.notes } : {}),\n status: values.status,\n })\n if (!response.ok) {\n // The route's own sentence, shown above the form unchanged.\n setCreateError(String(payload['error'] ?? 'The lead could not be added.'))\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(doc(firestore, 'hosts', lead.hostId, 'leads', lead.leadId), {\n ...fields,\n updatedAt: serverTimestamp(),\n })\n enqueueSnackbar(done, { variant: 'success', persist: false })\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : '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) => String(row['name'] || row['email'] || ''),\n renderCell: ({ row }: { row: LeadRow }) => (\n <Stack spacing={0} sx={{ minWidth: 0, justifyContent: 'center', height: '100%' }}>\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 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 {\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 {\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: <MdiIcon path={mdiAccountArrowRight.path} size={0.8} />,\n href: routes.lead(row.leadId, hostId ? null : row.hostId),\n },\n {\n key: 'convert',\n label: 'Convert…',\n icon: <MdiIcon path={mdiAccountConvertOutline.path} size={0.8} />,\n onClick: () => setConverting(row),\n disabled: convertRefusal !== null,\n disabledReason: convertRefusal ?? undefined,\n },\n {\n key: 'assign',\n label: 'Assign owner',\n icon: <MdiIcon path={mdiAccountTieOutline.path} size={0.8} />,\n onClick: () => setAssigning(row),\n },\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />,\n onClick: () => setUnqualifying(row),\n disabled: !Aglyn.isCrmLeadOpen(row) || 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],\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', { anchor: '#whats-in-the-crm-area' })}\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) => setFilter(event.target.value as LeadFilter)}\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 <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 size=\"small\" variant=\"contained\" onClick={() => setCreateOpen(true)}>\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 ? <LeadSurfacesNote hostId={hostId} /> : <OrgLeadSurfacesNote />}\n {status === 'success' && window.length === 0 ? (\n <EmptyStateComponent\n label={'No leads yet'}\n description={'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 ) : 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 />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={pageRows}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n loading={status === 'loading'}\n onOpen={(_id, row: LeadRow) =>\n router.push(routes.lead(row.leadId, hostId ? null : row.hostId))\n }\n // Columns and sort are the view's, controlled (AGL-2617).\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={grid.onColumnVisibilityModelChange}\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 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(unqualifying?.['name'] || unqualifying?.['email'] || '')}\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={{ display: 'flex', alignItems: 'center', height: '100%', width: '100%' }}\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\">{Aglyn.CRM_LEAD_STATUS_LABELS.working}</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 value={current} onChange={setUid} roster={roster} size=\"medium\" />\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","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","useOrgLeads","CrmViewsControl","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","EmptyStateComponent","useSnackbar","useFirestore","useFirestoreCollection","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_FILTERS","leadMatchesFilter","leadMatchesSearch","leadsCsv","LeadConvertDialog","leadSourceLabel","leadSources","leadTimeLabel","LeadImportButton","NewLeadDrawer","useCrmApi","LeadOwnerSelect","CONVERT_PENDING_ERASURE_REASON","LeadStatusChip","LeadSurfacesNote","LeadUnqualifyDialog","LeadsBulkBar","OrgLeadSurfacesNote","LEADS_WINDOW","CrmLeadsSection","props","unqualifying","converting","hostId","org","basePath","firestore","router","enqueueSnackbar","orgId","createHostId","mount","roster","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","setFilter","next","setFilters","op","filterLabelId","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","ownerUid","notes","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","labelFor","join","width","sortable","filterable","disableColumnMenu","converted","Boolean","convertedContactId","erasurePending","readErasureRequestedAtMs","convertRefusal","isCrmLeadOpen","onClick","event","stopPropagation","display","alignItems","label","items","key","icon","path","size","href","disabled","disabledReason","undefined","grid","header","help","pluginDocsHelp","anchor","actions","direction","controller","allLabel","labelId","target","option","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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"}
@@ -27,3 +27,12 @@ export declare const LEAD_FILTERS: readonly LeadFilter[];
27
27
  export declare const LEAD_FILTER_LABELS: Record<LeadFilter, string>;
28
28
  /** Whether a lead belongs in a filter's view. An absent status is `new`. */
29
29
  export declare function leadMatchesFilter(lead: Pick<CrmLeadFields, 'status'>, filter: LeadFilter): boolean;
30
+ /**
31
+ * Whether a lead answers a search term.
32
+ *
33
+ * Case-insensitive, and every whitespace-separated word of the term must
34
+ * appear in SOME searched field: `morgan lamphere` and `lamphere sal-15`
35
+ * both find Morgan Lamphere, `morgan smith` does not. A blank term matches
36
+ * every lead, so the box emptied is the list unfiltered.
37
+ */
38
+ export declare function leadMatchesSearch(lead: Readonly<Record<string, unknown>>, term: string): boolean;
@@ -35,5 +35,33 @@ export const LEAD_FILTER_LABELS = {
35
35
  if (filter === 'open') return isCrmLeadOpen(lead);
36
36
  return crmLeadStatus(lead) === filter;
37
37
  }
38
+ /**
39
+ * The fields the Leads section's search box reads (AGL-3246): who the lead
40
+ * is and where they work — the words a person types to find one — plus the
41
+ * tags, which are how an import names its batch (`sal-15`). `name` and
42
+ * `email` are the capture door's fields rather than the CRM's, so the row is
43
+ * read as a record rather than through `CrmLeadFields`.
44
+ */ const LEAD_SEARCH_FIELDS = [
45
+ 'name',
46
+ 'email',
47
+ 'company',
48
+ 'jobTitle'
49
+ ];
50
+ /**
51
+ * Whether a lead answers a search term.
52
+ *
53
+ * Case-insensitive, and every whitespace-separated word of the term must
54
+ * appear in SOME searched field: `morgan lamphere` and `lamphere sal-15`
55
+ * both find Morgan Lamphere, `morgan smith` does not. A blank term matches
56
+ * every lead, so the box emptied is the list unfiltered.
57
+ */ export function leadMatchesSearch(lead, term) {
58
+ const words = term.trim().toLowerCase().split(/\s+/).filter(Boolean);
59
+ if (!words.length) return true;
60
+ const values = [
61
+ ...LEAD_SEARCH_FIELDS.map((field)=>lead[field]),
62
+ ...Array.isArray(lead['tags']) ? lead['tags'] : []
63
+ ].filter((value)=>typeof value === 'string' && value !== '').map((value)=>value.toLowerCase());
64
+ return words.every((word)=>values.some((value)=>value.includes(word)));
65
+ }
38
66
 
39
67
  //# sourceMappingURL=lead-filters.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/lead-filters.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 {\n CRM_LEAD_STATUS_LABELS,\n type CrmLeadFields,\n type CrmLeadStatus,\n crmLeadStatus,\n isCrmLeadOpen,\n} from '@aglyn/aglyn'\n\n/**\n * What the Leads section's `Show` control offers (AGL-2608): the two\n * aggregate views and each status on its own. `open` is the default — the\n * list opens on the work — and it is the one view a Firestore query cannot\n * express, because a lead nobody has touched carries no status field at all;\n * see the section for why the filter runs over a loaded window.\n */\nexport type LeadFilter = 'open' | 'all' | CrmLeadStatus\n\nexport const LEAD_FILTERS: readonly LeadFilter[] = [\n 'open',\n 'new',\n 'working',\n 'qualified',\n 'unqualified',\n 'all',\n]\n\nexport const LEAD_FILTER_LABELS: Record<LeadFilter, string> = {\n open: 'Open',\n all: 'All',\n new: CRM_LEAD_STATUS_LABELS.new,\n working: CRM_LEAD_STATUS_LABELS.working,\n qualified: CRM_LEAD_STATUS_LABELS.qualified,\n unqualified: CRM_LEAD_STATUS_LABELS.unqualified,\n}\n\n/** Whether a lead belongs in a filter's view. An absent status is `new`. */\nexport function leadMatchesFilter(\n lead: Pick<CrmLeadFields, 'status'>,\n filter: LeadFilter,\n): boolean {\n if (filter === 'all') return true\n if (filter === 'open') return isCrmLeadOpen(lead)\n return crmLeadStatus(lead) === filter\n}\n"],"names":["CRM_LEAD_STATUS_LABELS","crmLeadStatus","isCrmLeadOpen","LEAD_FILTERS","LEAD_FILTER_LABELS","open","all","new","working","qualified","unqualified","leadMatchesFilter","lead","filter"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,sBAAsB,EAGtBC,aAAa,EACbC,aAAa,QACR,eAAc;AAWrB,OAAO,MAAMC,eAAsC;IACjD;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAED,OAAO,MAAMC,qBAAiD;IAC5DC,MAAM;IACNC,KAAK;IACLC,KAAKP,uBAAuBO,GAAG;IAC/BC,SAASR,uBAAuBQ,OAAO;IACvCC,WAAWT,uBAAuBS,SAAS;IAC3CC,aAAaV,uBAAuBU,WAAW;AACjD,EAAC;AAED,0EAA0E,GAC1E,OAAO,SAASC,kBACdC,IAAmC,EACnCC,MAAkB;IAElB,IAAIA,WAAW,OAAO,OAAO;IAC7B,IAAIA,WAAW,QAAQ,OAAOX,cAAcU;IAC5C,OAAOX,cAAcW,UAAUC;AACjC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/lead-filters.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 {\n CRM_LEAD_STATUS_LABELS,\n type CrmLeadFields,\n type CrmLeadStatus,\n crmLeadStatus,\n isCrmLeadOpen,\n} from '@aglyn/aglyn'\n\n/**\n * What the Leads section's `Show` control offers (AGL-2608): the two\n * aggregate views and each status on its own. `open` is the default — the\n * list opens on the work — and it is the one view a Firestore query cannot\n * express, because a lead nobody has touched carries no status field at all;\n * see the section for why the filter runs over a loaded window.\n */\nexport type LeadFilter = 'open' | 'all' | CrmLeadStatus\n\nexport const LEAD_FILTERS: readonly LeadFilter[] = [\n 'open',\n 'new',\n 'working',\n 'qualified',\n 'unqualified',\n 'all',\n]\n\nexport const LEAD_FILTER_LABELS: Record<LeadFilter, string> = {\n open: 'Open',\n all: 'All',\n new: CRM_LEAD_STATUS_LABELS.new,\n working: CRM_LEAD_STATUS_LABELS.working,\n qualified: CRM_LEAD_STATUS_LABELS.qualified,\n unqualified: CRM_LEAD_STATUS_LABELS.unqualified,\n}\n\n/** Whether a lead belongs in a filter's view. An absent status is `new`. */\nexport function leadMatchesFilter(\n lead: Pick<CrmLeadFields, 'status'>,\n filter: LeadFilter,\n): boolean {\n if (filter === 'all') return true\n if (filter === 'open') return isCrmLeadOpen(lead)\n return crmLeadStatus(lead) === filter\n}\n\n/**\n * The fields the Leads section's search box reads (AGL-3246): who the lead\n * is and where they work — the words a person types to find one — plus the\n * tags, which are how an import names its batch (`sal-15`). `name` and\n * `email` are the capture door's fields rather than the CRM's, so the row is\n * read as a record rather than through `CrmLeadFields`.\n */\nconst LEAD_SEARCH_FIELDS = ['name', 'email', 'company', 'jobTitle'] as const\n\n/**\n * Whether a lead answers a search term.\n *\n * Case-insensitive, and every whitespace-separated word of the term must\n * appear in SOME searched field: `morgan lamphere` and `lamphere sal-15`\n * both find Morgan Lamphere, `morgan smith` does not. A blank term matches\n * every lead, so the box emptied is the list unfiltered.\n */\nexport function leadMatchesSearch(\n lead: Readonly<Record<string, unknown>>,\n term: string,\n): boolean {\n const words = term.trim().toLowerCase().split(/\\s+/).filter(Boolean)\n if (!words.length) return true\n const values = [\n ...LEAD_SEARCH_FIELDS.map((field) => lead[field]),\n ...(Array.isArray(lead['tags']) ? lead['tags'] : []),\n ]\n .filter((value): value is string => typeof value === 'string' && value !== '')\n .map((value) => value.toLowerCase())\n return words.every((word) => values.some((value) => value.includes(word)))\n}\n"],"names":["CRM_LEAD_STATUS_LABELS","crmLeadStatus","isCrmLeadOpen","LEAD_FILTERS","LEAD_FILTER_LABELS","open","all","new","working","qualified","unqualified","leadMatchesFilter","lead","filter","LEAD_SEARCH_FIELDS","leadMatchesSearch","term","words","trim","toLowerCase","split","Boolean","length","values","map","field","Array","isArray","value","every","word","some","includes"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,sBAAsB,EAGtBC,aAAa,EACbC,aAAa,QACR,eAAc;AAWrB,OAAO,MAAMC,eAAsC;IACjD;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAED,OAAO,MAAMC,qBAAiD;IAC5DC,MAAM;IACNC,KAAK;IACLC,KAAKP,uBAAuBO,GAAG;IAC/BC,SAASR,uBAAuBQ,OAAO;IACvCC,WAAWT,uBAAuBS,SAAS;IAC3CC,aAAaV,uBAAuBU,WAAW;AACjD,EAAC;AAED,0EAA0E,GAC1E,OAAO,SAASC,kBACdC,IAAmC,EACnCC,MAAkB;IAElB,IAAIA,WAAW,OAAO,OAAO;IAC7B,IAAIA,WAAW,QAAQ,OAAOX,cAAcU;IAC5C,OAAOX,cAAcW,UAAUC;AACjC;AAEA;;;;;;CAMC,GACD,MAAMC,qBAAqB;IAAC;IAAQ;IAAS;IAAW;CAAW;AAEnE;;;;;;;CAOC,GACD,OAAO,SAASC,kBACdH,IAAuC,EACvCI,IAAY;IAEZ,MAAMC,QAAQD,KAAKE,IAAI,GAAGC,WAAW,GAAGC,KAAK,CAAC,OAAOP,MAAM,CAACQ;IAC5D,IAAI,CAACJ,MAAMK,MAAM,EAAE,OAAO;IAC1B,MAAMC,SAAS;WACVT,mBAAmBU,GAAG,CAAC,CAACC,QAAUb,IAAI,CAACa,MAAM;WAC5CC,MAAMC,OAAO,CAACf,IAAI,CAAC,OAAO,IAAIA,IAAI,CAAC,OAAO,GAAG,EAAE;KACpD,CACEC,MAAM,CAAC,CAACe,QAA2B,OAAOA,UAAU,YAAYA,UAAU,IAC1EJ,GAAG,CAAC,CAACI,QAAUA,MAAMT,WAAW;IACnC,OAAOF,MAAMY,KAAK,CAAC,CAACC,OAASP,OAAOQ,IAAI,CAAC,CAACH,QAAUA,MAAMI,QAAQ,CAACF;AACrE"}