@aglyn/plugins-crm 1.0.0-beta.195 → 1.0.0-beta.197
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/lib/components/contacts-section.js +18 -3
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/leads-section.js +2 -2
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-contact-drawer.d.ts +6 -0
- package/src/lib/components/new-contact-drawer.js +3 -2
- package/src/lib/components/new-contact-drawer.js.map +1 -1
- package/src/lib/constants/contact-filters.js +6 -0
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/declarations.server.js +21 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-crm-scope.d.ts +10 -0
- package/src/lib/hooks/use-crm-scope.js +32 -4
- package/src/lib/hooks/use-crm-scope.js.map +1 -1
- package/src/lib/model/consent-group-summary.d.ts +25 -0
- package/src/lib/model/consent-group-summary.js +32 -0
- package/src/lib/model/consent-group-summary.js.map +1 -0
- package/src/lib/server/capture-contact.js +16 -2
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/consent-group-participant.d.ts +81 -0
- package/src/lib/server/consent-group-participant.js +605 -0
- package/src/lib/server/consent-group-participant.js.map +1 -0
- package/src/lib/server.d.ts +2 -1
- package/src/lib/server.js +10 -2
- package/src/lib/server.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 CrmViewFilterClause,\n CrmLeadStatus,\n} 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 { useCrmCampaigns } from '../hooks/use-crm-campaigns'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\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 { CrmListActions, CrmListToolbar } from './crm-list-toolbar'\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 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 where,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n LEAD_EMAIL_FILTER_OPTIONS,\n LEAD_FILTER_CODECS,\n LEAD_LIST_FILTER_FIELDS,\n LEAD_LIST_FILTER_HEADERS,\n LEAD_SOURCE_FILTER_NONE,\n LEAD_STATUS_FILTER_OPTIONS,\n leadClausesForGrid,\n leadClausesToStore,\n leadMatchesClauses,\n leadMatchesSearch,\n} from '../model/lead-filters'\nimport {\n type ListFilterOption,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\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 `orgs/{orgId}/leads` narrowed by `visibleTo` to the sites\n * this viewer may see (AGL-3275) — the same collection and the same clause at\n * both levels, which is what lets ONE listener serve a section that used to\n * open one per site.\n *\n * Under a site the clause names that site; at the ORGANIZATION level an\n * org-wide member reads without one, since the rules short-circuit on\n * `isOrgWideMember()` and a clause would only narrow what they may already\n * read. The per-site notes — which of a site's forms file a lead — belong to\n * 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 // The org's lead source values (AGL-3298): the filter's menu and the\n // column's sort order.\n const leadSourceList = useLeadSourcePicklist(orgId)\n const routes = crmRoutes(basePath ?? '')\n\n /*\n * Under a site: the ORG collection, narrowed to what this site may see\n * (AGL-3275). It read `hosts/{hostId}/leads` until the silo moved, and\n * leaving it there would have shown a site its pre-migration rows and\n * nothing captured since — then nothing at all, once AGL-3276 emptied the\n * path it was reading.\n */\n const site = useFirestoreCollection<\n Record<string, unknown> & CrmLeadFields & { $id: string }\n >(\n () =>\n hostId && orgId\n ? query(\n collection(firestore, 'orgs', orgId, 'leads'),\n where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId)),\n orderBy('lastSeenAtMs', 'desc'),\n limit(LEADS_WINDOW + 1),\n )\n : null,\n [firestore, hostId, orgId],\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 * What the list is narrowed by is the saved VIEW'S (AGL-2617), and the\n * grid's own Filters panel and quick search edit it (AGL-3313): Status,\n * Email, Owner, Lead source and Campaign are select columns of the grid.\n * The clauses keep the shape the old dropdowns stored — see\n * `leadClausesForGrid` — so no saved view of leads loses its filter.\n */\n const views = useCrmSavedView({\n section: 'leads',\n hostId,\n org: props.org,\n basePath: basePath ?? '',\n })\n const clauses = useMemo(\n () => leadClausesForGrid(views.state.filters),\n [views.state.filters],\n )\n const setClauses = useCallback(\n (next: CrmViewFilterClause[]) => views.setFilters(leadClausesToStore(next)),\n [views.setFilters],\n )\n const gridFilter = useListGridFilter({\n clauses,\n onChange: setClauses,\n selectFields: ['status', 'emailState', 'ownerUid'],\n codecs: LEAD_FILTER_CODECS,\n })\n const campaigns = useCrmCampaigns({ hostId, orgId }, { enabled: true })\n const campaignName = useCallback(\n (id: string) =>\n campaigns.options.find((option) => option.value === id)?.label ?? id,\n [campaigns.options],\n )\n /*\n * The choices each select column offers. A stored clause naming a value\n * no longer listed — a retired campaign, a lead source since removed —\n * stays a choice, so the panel can show it and clear it.\n */\n const filterOptions = useMemo<Record<string, ListFilterOption[]>>(() => {\n const stale = (field: string, known: readonly { value: string }[]) =>\n clauses\n .filter((clause) => clause.field === field && clause.op !== 'isEmpty')\n .flatMap((clause) => clause.value.split(','))\n .map((value) => value.trim())\n .filter((value) => value && !known.some((option) => option.value === value))\n .map((value) => ({ value, label: value }))\n const campaignOptions = campaigns.options.map((option) => ({\n value: option.value,\n label: option.label,\n }))\n // Salesforce's Lead Source (AGL-3298): every value the org keeps, inactive ones marked.\n const sourceOptions = [\n ...leadSourceList.picklist.values.map((value) => ({\n value: value.label,\n label: value.active ? value.label : `${value.label} (inactive)`,\n })),\n { value: LEAD_SOURCE_FILTER_NONE, label: 'No lead source' },\n ]\n return {\n status: LEAD_STATUS_FILTER_OPTIONS,\n emailState: LEAD_EMAIL_FILTER_OPTIONS,\n ownerUid: roster.options.map((option) => ({\n value: option.uid,\n label: Aglyn.crmMemberPickerLabel(option),\n })),\n campaignIds: [...campaignOptions, ...stale('campaignIds', campaignOptions)],\n leadSource: [...sourceOptions, ...stale('leadSource', sourceOptions)],\n }\n }, [clauses, campaigns.options, leadSourceList.picklist, roster.options])\n /*\n * Every clause and the search narrow the whole loaded WINDOW here, before\n * the footer's count and the page slice (AGL-3246). The grid holds one\n * page of it, so a filter or a quick search the grid ran itself would\n * answer \"no match\" for a lead on page three.\n */\n const searchKey = gridFilter.searchWords.join(' ')\n const rows = useMemo(\n () =>\n window.filter(\n (lead) =>\n leadMatchesClauses(lead, clauses) &&\n leadMatchesSearch(lead, searchKey),\n ),\n [window, clauses, searchKey],\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 }, [views.state.filters, searchKey])\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 [views.state.filters, searchKey],\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 if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\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 * Salesforce's Lead Source (AGL-3298), beside the surfaces that\n * captured the person. Sorted in the order the org keeps its values,\n * as a picklist sorts, with a value the list does not hold after\n * every listed one and a lead with none last.\n */\n {\n field: 'leadSource',\n headerName: 'Lead source',\n flex: 1,\n minWidth: 150,\n valueGetter: (_value, row: LeadRow) => String(row.leadSource ?? ''),\n sortComparator: (a: unknown, b: unknown) =>\n Aglyn.crmPicklistRank(leadSourceList.picklist, a) -\n Aglyn.crmPicklistRank(leadSourceList.picklist, b) ||\n String(a ?? '').localeCompare(String(b ?? '')),\n } satisfies GridColDef,\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.\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 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 [\n roster,\n routes,\n writeLead,\n hostId,\n mount,\n campaignName,\n leadFields.active,\n leadSourceList.picklist,\n ],\n )\n /*\n * The column and sort models are the view's (AGL-2617); the filterable\n * columns are the declared fields, as selects over their choices (AGL-3313).\n */\n const filterColumns = useMemo(\n () =>\n listFilterGridColumns(columns, LEAD_LIST_FILTER_FIELDS, filterOptions, LEAD_LIST_FILTER_HEADERS),\n [columns, filterOptions],\n )\n const grid = useCrmViewGrid(views, filterColumns)\n\n return (\n <>\n <CardDisplay\n header={'Leads'}\n help={Aglyn.pluginDocsHelp('contacts', {\n anchor: '#whats-in-the-crm-area',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n // The record actions, top right and never clipped (AGL-3311).\n action: (\n <CrmListActions>\n <LeadImportButton hostId={hostId} orgId={orgId} />\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 </CrmListActions>\n ),\n }}\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 {/*\n The saved view, and every clause it is narrowed by as a chip\n (AGL-3313). The clauses are set in the grid's own Filters panel;\n the chips show the whole set, which that panel shows one of.\n */}\n <CrmListToolbar label=\"Lead filters\">\n <CrmViewsControl controller={views} allLabel=\"All leads\" />\n <ListFilterChips\n fields={LEAD_LIST_FILTER_FIELDS}\n headers={LEAD_LIST_FILTER_HEADERS}\n clauses={clauses}\n onChange={setClauses}\n options={filterOptions}\n marksServed={false}\n />\n </CrmListToolbar>\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 ) : (\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 panel and the search are the grid's; the section\n // answers both over the whole window (AGL-3313).\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel={`No leads match among the ${window.length.toLocaleString()} most recently seen`}\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 filters and the search 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 ?? null}\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 (Aglyn.leadPrimaryGroup(converting, org as Record<string, unknown>).hostId || null)\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","CardDisplay","MdiIcon","ListPagination","ListTable","useOrgMemberOptions","useCrmOrgMount","useCrmSavedView","useCrmScope","useCrmCampaigns","scopeTokensForHost","useContactFieldDefinitions","customFieldColumns","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","useOrgLeads","CrmViewsControl","CrmListActions","CrmListToolbar","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","EmptyStateComponent","useSnackbar","useFirestore","useFirestoreCollection","Alert","Box","Button","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Select","Stack","Typography","collection","deleteField","doc","limit","orderBy","query","serverTimestamp","updateDoc","where","useRouter","useCallback","useEffect","useMemo","useState","downloadTextFile","crmRoutes","LEAD_EMAIL_FILTER_OPTIONS","LEAD_FILTER_CODECS","LEAD_LIST_FILTER_FIELDS","LEAD_LIST_FILTER_HEADERS","LEAD_SOURCE_FILTER_NONE","LEAD_STATUS_FILTER_OPTIONS","leadClausesForGrid","leadClausesToStore","leadMatchesClauses","leadMatchesSearch","listFilterGridColumns","useListGridFilter","ListFilterChips","useLeadSourcePicklist","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","leadSourceList","routes","site","idField","orgHostIds","hosts","map","host","id","orgLeads","visibleTo","windowSize","leadDocs","data","row","leadId","$id","status","hostsReady","length","truncated","window","slice","views","section","clauses","state","filters","setClauses","next","setFilters","gridFilter","onChange","selectFields","codecs","campaigns","enabled","campaignName","options","find","option","value","label","filterOptions","stale","field","known","filter","clause","op","flatMap","split","trim","some","campaignOptions","sourceOptions","picklist","values","active","emailState","ownerUid","uid","crmMemberPickerLabel","campaignIds","leadSource","searchKey","searchWords","join","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","response","payload","email","name","company","jobTitle","phone","website","address","tags","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","unqualifiedReason","readEmailState","EMAIL_STATE_LABELS","labelFor","held","Array","isArray","sortComparator","a","b","crmPicklistRank","localeCompare","readCampaignIds","width","sortable","filterable","disableColumnMenu","converted","Boolean","convertedContactId","erasurePending","readErasureRequestedAtMs","convertRefusal","isCrmLeadOpen","onClick","event","stopPropagation","display","alignItems","items","key","icon","path","size","href","disabled","disabledReason","undefined","filterColumns","grid","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","controller","allLabel","headers","marksServed","description","selected","onSelectedChange","csv","columnOrder","slots","selectable","loading","onOpen","_id","push","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","toLocaleString","hideFooter","rowCount","count","onPageChange","onPageSizeChange","severity","open","onClose","busy","onSubmit","AssignOwnerDialog","onAssign","leadLabel","leadPrimaryGroup","displayName","disableUnderline","target","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 {\n ConsolePluginPageProps,\n CrmLeadFields,\n CrmViewFilterClause,\n CrmLeadStatus,\n} 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 { useCrmCampaigns } from '../hooks/use-crm-campaigns'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\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 { CrmListActions, CrmListToolbar } from './crm-list-toolbar'\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 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 where,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n LEAD_EMAIL_FILTER_OPTIONS,\n LEAD_FILTER_CODECS,\n LEAD_LIST_FILTER_FIELDS,\n LEAD_LIST_FILTER_HEADERS,\n LEAD_SOURCE_FILTER_NONE,\n LEAD_STATUS_FILTER_OPTIONS,\n leadClausesForGrid,\n leadClausesToStore,\n leadMatchesClauses,\n leadMatchesSearch,\n} from '../model/lead-filters'\nimport {\n type ListFilterOption,\n listFilterGridColumns,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\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 `orgs/{orgId}/leads` narrowed by `visibleTo` to the sites\n * this viewer may see (AGL-3275) — the same collection and the same clause at\n * both levels, which is what lets ONE listener serve a section that used to\n * open one per site.\n *\n * Under a site the clause names that site; at the ORGANIZATION level an\n * org-wide member reads without one, since the rules short-circuit on\n * `isOrgWideMember()` and a clause would only narrow what they may already\n * read. The per-site notes — which of a site's forms file a lead — belong to\n * 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 // The org's lead source values (AGL-3298): the filter's menu and the\n // column's sort order.\n const leadSourceList = useLeadSourcePicklist(orgId)\n const routes = crmRoutes(basePath ?? '')\n\n /*\n * Under a site: the ORG collection, narrowed to what this site may see\n * (AGL-3275). It read `hosts/{hostId}/leads` until the silo moved, and\n * leaving it there would have shown a site its pre-migration rows and\n * nothing captured since — then nothing at all, once AGL-3276 emptied the\n * path it was reading.\n */\n const site = useFirestoreCollection<\n Record<string, unknown> & CrmLeadFields & { $id: string }\n >(\n () =>\n hostId && orgId\n ? query(\n collection(firestore, 'orgs', orgId, 'leads'),\n where('visibleTo', 'array-contains-any', scopeTokensForHost(hostId)),\n orderBy('lastSeenAtMs', 'desc'),\n limit(LEADS_WINDOW + 1),\n )\n : null,\n [firestore, hostId, orgId],\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 * What the list is narrowed by is the saved VIEW'S (AGL-2617), and the\n * grid's own Filters panel and quick search edit it (AGL-3313): Status,\n * Email, Owner, Lead source and Campaign are select columns of the grid.\n * The clauses keep the shape the old dropdowns stored — see\n * `leadClausesForGrid` — so no saved view of leads loses its filter.\n */\n const views = useCrmSavedView({\n section: 'leads',\n hostId,\n org: props.org,\n basePath: basePath ?? '',\n })\n const clauses = useMemo(\n () => leadClausesForGrid(views.state.filters),\n [views.state.filters],\n )\n const setClauses = useCallback(\n (next: CrmViewFilterClause[]) => views.setFilters(leadClausesToStore(next)),\n [views.setFilters],\n )\n const gridFilter = useListGridFilter({\n clauses,\n onChange: setClauses,\n selectFields: ['status', 'emailState', 'ownerUid'],\n codecs: LEAD_FILTER_CODECS,\n })\n const campaigns = useCrmCampaigns({ hostId, orgId }, { enabled: true })\n const campaignName = useCallback(\n (id: string) =>\n campaigns.options.find((option) => option.value === id)?.label ?? id,\n [campaigns.options],\n )\n /*\n * The choices each select column offers. A stored clause naming a value\n * no longer listed — a retired campaign, a lead source since removed —\n * stays a choice, so the panel can show it and clear it.\n */\n const filterOptions = useMemo<Record<string, ListFilterOption[]>>(() => {\n const stale = (field: string, known: readonly { value: string }[]) =>\n clauses\n .filter((clause) => clause.field === field && clause.op !== 'isEmpty')\n .flatMap((clause) => clause.value.split(','))\n .map((value) => value.trim())\n .filter((value) => value && !known.some((option) => option.value === value))\n .map((value) => ({ value, label: value }))\n const campaignOptions = campaigns.options.map((option) => ({\n value: option.value,\n label: option.label,\n }))\n // Salesforce's Lead Source (AGL-3298): every value the org keeps, inactive ones marked.\n const sourceOptions = [\n ...leadSourceList.picklist.values.map((value) => ({\n value: value.label,\n label: value.active ? value.label : `${value.label} (inactive)`,\n })),\n { value: LEAD_SOURCE_FILTER_NONE, label: 'No lead source' },\n ]\n return {\n status: LEAD_STATUS_FILTER_OPTIONS,\n emailState: LEAD_EMAIL_FILTER_OPTIONS,\n ownerUid: roster.options.map((option) => ({\n value: option.uid,\n label: Aglyn.crmMemberPickerLabel(option),\n })),\n campaignIds: [...campaignOptions, ...stale('campaignIds', campaignOptions)],\n leadSource: [...sourceOptions, ...stale('leadSource', sourceOptions)],\n }\n }, [clauses, campaigns.options, leadSourceList.picklist, roster.options])\n /*\n * Every clause and the search narrow the whole loaded WINDOW here, before\n * the footer's count and the page slice (AGL-3246). The grid holds one\n * page of it, so a filter or a quick search the grid ran itself would\n * answer \"no match\" for a lead on page three.\n */\n const searchKey = gridFilter.searchWords.join(' ')\n const rows = useMemo(\n () =>\n window.filter(\n (lead) =>\n leadMatchesClauses(lead, clauses) &&\n leadMatchesSearch(lead, searchKey),\n ),\n [window, clauses, searchKey],\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 }, [views.state.filters, searchKey])\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 [views.state.filters, searchKey],\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 if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\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 * Salesforce's Lead Source (AGL-3298), beside the surfaces that\n * captured the person. Sorted in the order the org keeps its values,\n * as a picklist sorts, with a value the list does not hold after\n * every listed one and a lead with none last.\n */\n {\n field: 'leadSource',\n headerName: 'Lead source',\n flex: 1,\n minWidth: 150,\n valueGetter: (_value, row: LeadRow) => String(row.leadSource ?? ''),\n sortComparator: (a: unknown, b: unknown) =>\n Aglyn.crmPicklistRank(leadSourceList.picklist, a) -\n Aglyn.crmPicklistRank(leadSourceList.picklist, b) ||\n String(a ?? '').localeCompare(String(b ?? '')),\n } satisfies GridColDef,\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.\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 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 [\n roster,\n routes,\n writeLead,\n hostId,\n mount,\n campaignName,\n leadFields.active,\n leadSourceList.picklist,\n ],\n )\n /*\n * The column and sort models are the view's (AGL-2617); the filterable\n * columns are the declared fields, as selects over their choices (AGL-3313).\n */\n const filterColumns = useMemo(\n () =>\n listFilterGridColumns(columns, LEAD_LIST_FILTER_FIELDS, filterOptions, LEAD_LIST_FILTER_HEADERS),\n [columns, filterOptions],\n )\n const grid = useCrmViewGrid(views, filterColumns)\n\n return (\n <>\n <CardDisplay\n header={'Leads'}\n help={Aglyn.pluginDocsHelp('crmLeads', {\n anchor: '#filter-the-leads',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n // The record actions, top right and never clipped (AGL-3311).\n action: (\n <CrmListActions>\n <LeadImportButton hostId={hostId} orgId={orgId} />\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 </CrmListActions>\n ),\n }}\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 {/*\n The saved view, and every clause it is narrowed by as a chip\n (AGL-3313). The clauses are set in the grid's own Filters panel;\n the chips show the whole set, which that panel shows one of.\n */}\n <CrmListToolbar label=\"Lead filters\">\n <CrmViewsControl controller={views} allLabel=\"All leads\" />\n <ListFilterChips\n fields={LEAD_LIST_FILTER_FIELDS}\n headers={LEAD_LIST_FILTER_HEADERS}\n clauses={clauses}\n onChange={setClauses}\n options={filterOptions}\n marksServed={false}\n />\n </CrmListToolbar>\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 ) : (\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 panel and the search are the grid's; the section\n // answers both over the whole window (AGL-3313).\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n noRowsLabel={`No leads match among the ${window.length.toLocaleString()} most recently seen`}\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 filters and the search 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 ?? null}\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 (Aglyn.leadPrimaryGroup(converting, org as Record<string, unknown>).hostId || null)\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","CardDisplay","MdiIcon","ListPagination","ListTable","useOrgMemberOptions","useCrmOrgMount","useCrmSavedView","useCrmScope","useCrmCampaigns","scopeTokensForHost","useContactFieldDefinitions","customFieldColumns","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","useOrgLeads","CrmViewsControl","CrmListActions","CrmListToolbar","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","EmptyStateComponent","useSnackbar","useFirestore","useFirestoreCollection","Alert","Box","Button","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Select","Stack","Typography","collection","deleteField","doc","limit","orderBy","query","serverTimestamp","updateDoc","where","useRouter","useCallback","useEffect","useMemo","useState","downloadTextFile","crmRoutes","LEAD_EMAIL_FILTER_OPTIONS","LEAD_FILTER_CODECS","LEAD_LIST_FILTER_FIELDS","LEAD_LIST_FILTER_HEADERS","LEAD_SOURCE_FILTER_NONE","LEAD_STATUS_FILTER_OPTIONS","leadClausesForGrid","leadClausesToStore","leadMatchesClauses","leadMatchesSearch","listFilterGridColumns","useListGridFilter","ListFilterChips","useLeadSourcePicklist","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","leadSourceList","routes","site","idField","orgHostIds","hosts","map","host","id","orgLeads","visibleTo","windowSize","leadDocs","data","row","leadId","$id","status","hostsReady","length","truncated","window","slice","views","section","clauses","state","filters","setClauses","next","setFilters","gridFilter","onChange","selectFields","codecs","campaigns","enabled","campaignName","options","find","option","value","label","filterOptions","stale","field","known","filter","clause","op","flatMap","split","trim","some","campaignOptions","sourceOptions","picklist","values","active","emailState","ownerUid","uid","crmMemberPickerLabel","campaignIds","leadSource","searchKey","searchWords","join","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","response","payload","email","name","company","jobTitle","phone","website","address","tags","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","unqualifiedReason","readEmailState","EMAIL_STATE_LABELS","labelFor","held","Array","isArray","sortComparator","a","b","crmPicklistRank","localeCompare","readCampaignIds","width","sortable","filterable","disableColumnMenu","converted","Boolean","convertedContactId","erasurePending","readErasureRequestedAtMs","convertRefusal","isCrmLeadOpen","onClick","event","stopPropagation","display","alignItems","items","key","icon","path","size","href","disabled","disabledReason","undefined","filterColumns","grid","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","controller","allLabel","headers","marksServed","description","selected","onSelectedChange","csv","columnOrder","slots","selectable","loading","onOpen","_id","push","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","toLocaleString","hideFooter","rowCount","count","onPageChange","onPageSizeChange","severity","open","onClose","busy","onSubmit","AssignOwnerDialog","onAssign","leadLabel","leadPrimaryGroup","displayName","disableUnderline","target","renderValue","CRM_LEAD_STATUS_LABELS","new","working","unqualified","setUid","current","maxWidth","fullWidth","slotProps","transition","onExited","pt"],"mappings":"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"}
|
|
@@ -15,6 +15,12 @@ export interface NewContactValues {
|
|
|
15
15
|
tags: string[];
|
|
16
16
|
address: AglynPostalAddress | null;
|
|
17
17
|
marketingConsent: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* The key of the consent-group sentence shown beside the box, when one was
|
|
20
|
+
* (AGL-3320) — what lets the route record the opt-in for the whole group
|
|
21
|
+
* the sentence named. `null` when the site sends on its own.
|
|
22
|
+
*/
|
|
23
|
+
disclosedConsentGroup: string | null;
|
|
18
24
|
}
|
|
19
25
|
export interface NewContactDrawerProps {
|
|
20
26
|
open: boolean;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
-
import { CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_LIFECYCLE_STAGES, consentGroupDisclosure, normalizeAddress, normalizeContactEmail, normalizePhone, crmMemberPickerLabel } from "@aglyn/aglyn";
|
|
18
|
+
import { CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_LIFECYCLE_STAGES, consentGroupDisclosure, consentGroupDisclosureKey, normalizeAddress, normalizeContactEmail, normalizePhone, crmMemberPickerLabel } from "@aglyn/aglyn";
|
|
19
19
|
import { ICON_VARIANT_CLOSE } from "@aglyn/shared-data-enums";
|
|
20
20
|
import { Container, MdiIcon, SrOnly } from "@aglyn/shared-ui-jsx";
|
|
21
21
|
import { NavigationDrawerComponent } from "@aglyn/shared-ui-jsx/components/navigation-drawer.component";
|
|
@@ -135,7 +135,8 @@ import { CrmSitePicker } from "./crm-site-picker.js";
|
|
|
135
135
|
lifecycleStage,
|
|
136
136
|
tags: parseContactTags(tags),
|
|
137
137
|
address: normalizeAddress(address),
|
|
138
|
-
marketingConsent
|
|
138
|
+
marketingConsent,
|
|
139
|
+
disclosedConsentGroup: consentGroup ? consentGroupDisclosureKey(consentGroup) : null
|
|
139
140
|
});
|
|
140
141
|
};
|
|
141
142
|
const disclosure = consentGroup ? consentGroupDisclosure(consentGroup) : null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/new-contact-drawer.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 {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n consentGroupDisclosure,\n type AglynOrgBilling,\n type AglynPostalAddress,\n type ConsentGroup,\n type ContactLifecycleStage,\n normalizeAddress,\n normalizeContactEmail,\n normalizePhone,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport {\n Alert,\n Button,\n Checkbox,\n FormControlLabel,\n FormHelperText,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { parseContactTags } from '../model/contact-record'\nimport {\n CompanyPicker,\n type CompanyOption,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n ContactAddressFields,\n EMPTY_ADDRESS,\n type AddressDraft,\n} from './contact-address-fields'\nimport { CrmSitePicker } from './crm-site-picker'\nimport type { OrgMemberOption } from './use-org-members'\n\n/** What the drawer hands back — already normalized where the route would. */\nexport interface NewContactValues {\n email: string\n name: string\n phone: string\n jobTitle: string\n /** The picked company's name — the label the list column and the search read. */\n companyName: string\n /** The picked company, or `null` for none (AGL-2613). */\n companyId: string | null\n ownerUid: string\n lifecycleStage: ContactLifecycleStage | ''\n tags: string[]\n address: AglynPostalAddress | null\n marketingConsent: boolean\n}\n\nexport interface NewContactDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the contact is being added from — what scopes the company list\n * and the controller the consent checkbox records a basis for. `null` at\n * the organization level (AGL-2630), where the drawer asks which site\n * with a picker and holds its submit until one is named.\n */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** The request is in flight — the form holds still and the button says so. */\n busy?: boolean\n /** What the route answered when it refused, shown above the form. */\n error?: string | null\n /** The team, for the owner picker. */\n owners: OrgMemberOption[]\n /** The roster has answered — an empty list is then \"nobody to pick\". */\n ownersReady: boolean\n onSubmit: (values: NewContactValues) => void\n}\n\n/**\n * ADDING ONE PERSON BY HAND, IN A DRAWER (AGL-2596).\n *\n * A drawer and not a form above the list: a create form sitting on top of a\n * table is the shape every list in this console was built to stop. The\n * button in the list's header opens this, and the list is exactly as it was\n * behind it.\n *\n * ## What is checked here and what is not\n *\n * The two fields a person can mistype in a way the record cannot hold — the\n * email and the phone number — are checked before the request leaves, with\n * the same normalizers the route runs, so the refusal lands under the field\n * rather than as a sentence from the server. Everything else is the route's\n * to decide: whether the address already belongs to somebody (it merges),\n * whether the plan's band has room (it refuses, and its sentence is shown\n * above the form unchanged).\n *\n * ## The consent box is a claim, and says so\n *\n * The forms plugin's rule for the opt-in field is that submitting a form is\n * never itself consent. Typing somebody into the CRM is not either — so the\n * checkbox is worded as the person's act, not the operator's, and the helper\n * carries the group disclosure the form would have shown them.\n *\n * ## The company is a record, picked or created here\n *\n * The Company field is the picker (AGL-2613): a choice from the companies\n * this site may see, with a \"Create …\" row for a name none of them carry, so\n * the person filing a contact never leaves the drawer to make the account.\n * What leaves the drawer is the company's id AND its name — the id is the\n * link the route writes into the facet, the name is the label the list\n * column and the global search keep reading. The company listen opens with\n * the drawer and closes with it, because the drawer is mounted only while\n * it is open.\n */\nexport function NewContactDrawer(props: NewContactDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n busy,\n error,\n owners,\n ownersReady,\n onSubmit,\n } = props\n /*\n * The controller the consent checkbox records a basis FOR: the mounted\n * site's group, or at the organization level the picked site's — the one\n * the route will capture the contact under. `null` until a site is\n * picked, and the submit waits on it.\n */\n const { createHostId, createGroup } = useCrmScope({ hostId, org })\n const consentGroup: ConsentGroup | null = createGroup\n const companies = useCompanyOptions({ hostId, org, enabled: open })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [email, setEmail] = useState('')\n const [name, setName] = useState('')\n const [phone, setPhone] = useState('')\n const [jobTitle, setJobTitle] = useState('')\n const [company, setCompany] = useState<CompanyOption | null>(null)\n const [ownerUid, setOwnerUid] = useState('')\n const [lifecycleStage, setLifecycleStage] = useState<\n ContactLifecycleStage | ''\n >('')\n const [tags, setTags] = useState('')\n const [address, setAddress] = useState<AddressDraft>(EMPTY_ADDRESS)\n const [marketingConsent, setMarketingConsent] = useState(false)\n const [emailError, setEmailError] = useState('')\n const [phoneError, setPhoneError] = useState('')\n\n /*\n * A fresh form on every opening. The drawer stays mounted behind the list\n * between uses, and a person typed and then abandoned must not reappear\n * half-filled the next time somebody reaches for \"New contact\".\n */\n useEffect(() => {\n if (!open) return\n setEmail('')\n setName('')\n setPhone('')\n setJobTitle('')\n setCompany(null)\n setOwnerUid('')\n setLifecycleStage('')\n setTags('')\n setAddress(EMPTY_ADDRESS)\n setMarketingConsent(false)\n setEmailError('')\n setPhoneError('')\n }, [open])\n\n const handleSubmit = () => {\n const normalizedEmail = normalizeContactEmail(email)\n const trimmedPhone = phone.trim()\n const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : ''\n setEmailError(normalizedEmail ? '' : 'Enter a valid email address.')\n setPhoneError(\n trimmedPhone && !normalizedPhone\n ? 'Enter it with its country code, like +1 512 555 0107.'\n : '',\n )\n if (!normalizedEmail || (trimmedPhone && !normalizedPhone)) return\n onSubmit({\n email: normalizedEmail,\n name: name.trim().slice(0, 120),\n phone: normalizedPhone ?? '',\n jobTitle: jobTitle.trim().slice(0, 120),\n companyName: company ? company.name.trim().slice(0, 120) : '',\n companyId: company?.id ?? null,\n ownerUid,\n lifecycleStage,\n tags: parseContactTags(tags),\n address: normalizeAddress(address),\n marketingConsent,\n })\n }\n\n const disclosure = consentGroup ? consentGroupDisclosure(consentGroup) : null\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n sx={{ '& .MuiDrawer-paper': { width: { xs: '100%', sm: 480 } } }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={onClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {'New contact'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n {/*\n First, because everything below it is filed under the answer: the\n site decides which of the org's sites may see the person and whose\n consent the checkbox at the foot records. Renders nothing under a\n site (AGL-2630).\n */}\n <CrmSitePicker\n hostId={hostId}\n disabled={Boolean(busy)}\n helperText=\"The site that captures this person — it decides which sites may see them and whose marketing consent the checkbox below records.\"\n />\n <TextField\n size=\"small\"\n label=\"Email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n error={Boolean(emailError)}\n helperText={\n emailError ||\n 'The one field that is required — it is what makes two captures one person.'\n }\n required\n fullWidth\n autoFocus\n />\n <TextField\n size=\"small\"\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={Boolean(phoneError)}\n helperText={phoneError || 'With the country code, like +1 512 555 0107'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => setJobTitle(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={company?.id ?? null}\n onChange={(_id, picked) => setCompany(picked)}\n onCreate={createCompany}\n email={email}\n disabled={Boolean(busy)}\n helperText=\"The account this person works for. Type a name nobody has filed yet to create it.\"\n />\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n setLifecycleStage(event.target.value as ContactLifecycleStage | '')\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Not placed yet'}</MenuItem>\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => setOwnerUid(event.target.value)}\n helperText={\n ownersReady && !owners.length\n ? 'The team roster could not be read, so nobody can be picked yet.'\n : 'The team member responsible for this relationship'\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Unassigned'}</MenuItem>\n {owners.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {crmMemberPickerLabel(owner)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => setTags(event.target.value)}\n fullWidth\n />\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n <ContactAddressFields value={address} onChange={setAddress} />\n <Stack>\n <FormControlLabel\n control={\n <Checkbox\n checked={marketingConsent}\n onChange={(event) => setMarketingConsent(event.target.checked)}\n />\n }\n label=\"This person opted in to marketing email\"\n />\n <FormHelperText>\n {(disclosure ? `${disclosure} ` : '') +\n 'Tick it only if they agreed. Adding a contact is never itself consent.'}\n </FormHelperText>\n </Stack>\n <Button\n variant=\"contained\"\n color=\"primary\"\n // Held until the capturing site is known: the route resolves the\n // org from it and stamps the record with it.\n disabled={Boolean(busy) || !createHostId}\n onClick={handleSubmit}\n >\n {busy ? 'Adding…' : 'Add contact'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\n\nexport default NewContactDrawer\n"],"names":["CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","consentGroupDisclosure","normalizeAddress","normalizeContactEmail","normalizePhone","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","Alert","Button","Checkbox","FormControlLabel","FormHelperText","IconButton","MenuItem","Stack","TextField","Typography","useEffect","useState","useCrmScope","parseContactTags","CompanyPicker","useCompanyOptions","useCreateCompany","ContactAddressFields","EMPTY_ADDRESS","CrmSitePicker","NewContactDrawer","props","open","onClose","hostId","org","busy","error","owners","ownersReady","onSubmit","createHostId","createGroup","consentGroup","companies","enabled","createCompany","email","setEmail","name","setName","phone","setPhone","jobTitle","setJobTitle","company","setCompany","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","address","setAddress","marketingConsent","setMarketingConsent","emailError","setEmailError","phoneError","setPhoneError","handleSubmit","normalizedEmail","trimmedPhone","trim","normalizedPhone","slice","companyName","companyId","id","disclosure","anchor","variant","AppBarProps","color","sx","width","xs","sm","appBarLeft","edge","onClick","mr","path","component","appBarRight","gutterY","spacing","severity","disabled","Boolean","helperText","size","label","type","value","onChange","event","target","required","fullWidth","autoFocus","slotProps","htmlInput","maxLength","options","ready","truncated","_id","picked","onCreate","select","map","stage","length","owner","uid","placeholder","control","checked"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/new-contact-drawer.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 {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n consentGroupDisclosure,\n consentGroupDisclosureKey,\n type AglynOrgBilling,\n type AglynPostalAddress,\n type ConsentGroup,\n type ContactLifecycleStage,\n normalizeAddress,\n normalizeContactEmail,\n normalizePhone,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport {\n Alert,\n Button,\n Checkbox,\n FormControlLabel,\n FormHelperText,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { parseContactTags } from '../model/contact-record'\nimport {\n CompanyPicker,\n type CompanyOption,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n ContactAddressFields,\n EMPTY_ADDRESS,\n type AddressDraft,\n} from './contact-address-fields'\nimport { CrmSitePicker } from './crm-site-picker'\nimport type { OrgMemberOption } from './use-org-members'\n\n/** What the drawer hands back — already normalized where the route would. */\nexport interface NewContactValues {\n email: string\n name: string\n phone: string\n jobTitle: string\n /** The picked company's name — the label the list column and the search read. */\n companyName: string\n /** The picked company, or `null` for none (AGL-2613). */\n companyId: string | null\n ownerUid: string\n lifecycleStage: ContactLifecycleStage | ''\n tags: string[]\n address: AglynPostalAddress | null\n marketingConsent: boolean\n /**\n * The key of the consent-group sentence shown beside the box, when one was\n * (AGL-3320) — what lets the route record the opt-in for the whole group\n * the sentence named. `null` when the site sends on its own.\n */\n disclosedConsentGroup: string | null\n}\n\nexport interface NewContactDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the contact is being added from — what scopes the company list\n * and the controller the consent checkbox records a basis for. `null` at\n * the organization level (AGL-2630), where the drawer asks which site\n * with a picker and holds its submit until one is named.\n */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** The request is in flight — the form holds still and the button says so. */\n busy?: boolean\n /** What the route answered when it refused, shown above the form. */\n error?: string | null\n /** The team, for the owner picker. */\n owners: OrgMemberOption[]\n /** The roster has answered — an empty list is then \"nobody to pick\". */\n ownersReady: boolean\n onSubmit: (values: NewContactValues) => void\n}\n\n/**\n * ADDING ONE PERSON BY HAND, IN A DRAWER (AGL-2596).\n *\n * A drawer and not a form above the list: a create form sitting on top of a\n * table is the shape every list in this console was built to stop. The\n * button in the list's header opens this, and the list is exactly as it was\n * behind it.\n *\n * ## What is checked here and what is not\n *\n * The two fields a person can mistype in a way the record cannot hold — the\n * email and the phone number — are checked before the request leaves, with\n * the same normalizers the route runs, so the refusal lands under the field\n * rather than as a sentence from the server. Everything else is the route's\n * to decide: whether the address already belongs to somebody (it merges),\n * whether the plan's band has room (it refuses, and its sentence is shown\n * above the form unchanged).\n *\n * ## The consent box is a claim, and says so\n *\n * The forms plugin's rule for the opt-in field is that submitting a form is\n * never itself consent. Typing somebody into the CRM is not either — so the\n * checkbox is worded as the person's act, not the operator's, and the helper\n * carries the group disclosure the form would have shown them.\n *\n * ## The company is a record, picked or created here\n *\n * The Company field is the picker (AGL-2613): a choice from the companies\n * this site may see, with a \"Create …\" row for a name none of them carry, so\n * the person filing a contact never leaves the drawer to make the account.\n * What leaves the drawer is the company's id AND its name — the id is the\n * link the route writes into the facet, the name is the label the list\n * column and the global search keep reading. The company listen opens with\n * the drawer and closes with it, because the drawer is mounted only while\n * it is open.\n */\nexport function NewContactDrawer(props: NewContactDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n busy,\n error,\n owners,\n ownersReady,\n onSubmit,\n } = props\n /*\n * The controller the consent checkbox records a basis FOR: the mounted\n * site's group, or at the organization level the picked site's — the one\n * the route will capture the contact under. `null` until a site is\n * picked, and the submit waits on it.\n */\n const { createHostId, createGroup } = useCrmScope({ hostId, org })\n const consentGroup: ConsentGroup | null = createGroup\n const companies = useCompanyOptions({ hostId, org, enabled: open })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [email, setEmail] = useState('')\n const [name, setName] = useState('')\n const [phone, setPhone] = useState('')\n const [jobTitle, setJobTitle] = useState('')\n const [company, setCompany] = useState<CompanyOption | null>(null)\n const [ownerUid, setOwnerUid] = useState('')\n const [lifecycleStage, setLifecycleStage] = useState<\n ContactLifecycleStage | ''\n >('')\n const [tags, setTags] = useState('')\n const [address, setAddress] = useState<AddressDraft>(EMPTY_ADDRESS)\n const [marketingConsent, setMarketingConsent] = useState(false)\n const [emailError, setEmailError] = useState('')\n const [phoneError, setPhoneError] = useState('')\n\n /*\n * A fresh form on every opening. The drawer stays mounted behind the list\n * between uses, and a person typed and then abandoned must not reappear\n * half-filled the next time somebody reaches for \"New contact\".\n */\n useEffect(() => {\n if (!open) return\n setEmail('')\n setName('')\n setPhone('')\n setJobTitle('')\n setCompany(null)\n setOwnerUid('')\n setLifecycleStage('')\n setTags('')\n setAddress(EMPTY_ADDRESS)\n setMarketingConsent(false)\n setEmailError('')\n setPhoneError('')\n }, [open])\n\n const handleSubmit = () => {\n const normalizedEmail = normalizeContactEmail(email)\n const trimmedPhone = phone.trim()\n const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : ''\n setEmailError(normalizedEmail ? '' : 'Enter a valid email address.')\n setPhoneError(\n trimmedPhone && !normalizedPhone\n ? 'Enter it with its country code, like +1 512 555 0107.'\n : '',\n )\n if (!normalizedEmail || (trimmedPhone && !normalizedPhone)) return\n onSubmit({\n email: normalizedEmail,\n name: name.trim().slice(0, 120),\n phone: normalizedPhone ?? '',\n jobTitle: jobTitle.trim().slice(0, 120),\n companyName: company ? company.name.trim().slice(0, 120) : '',\n companyId: company?.id ?? null,\n ownerUid,\n lifecycleStage,\n tags: parseContactTags(tags),\n address: normalizeAddress(address),\n marketingConsent,\n disclosedConsentGroup: consentGroup\n ? consentGroupDisclosureKey(consentGroup)\n : null,\n })\n }\n\n const disclosure = consentGroup ? consentGroupDisclosure(consentGroup) : null\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n sx={{ '& .MuiDrawer-paper': { width: { xs: '100%', sm: 480 } } }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={onClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {'New contact'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n {/*\n First, because everything below it is filed under the answer: the\n site decides which of the org's sites may see the person and whose\n consent the checkbox at the foot records. Renders nothing under a\n site (AGL-2630).\n */}\n <CrmSitePicker\n hostId={hostId}\n disabled={Boolean(busy)}\n helperText=\"The site that captures this person — it decides which sites may see them and whose marketing consent the checkbox below records.\"\n />\n <TextField\n size=\"small\"\n label=\"Email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n error={Boolean(emailError)}\n helperText={\n emailError ||\n 'The one field that is required — it is what makes two captures one person.'\n }\n required\n fullWidth\n autoFocus\n />\n <TextField\n size=\"small\"\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={Boolean(phoneError)}\n helperText={phoneError || 'With the country code, like +1 512 555 0107'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => setJobTitle(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={company?.id ?? null}\n onChange={(_id, picked) => setCompany(picked)}\n onCreate={createCompany}\n email={email}\n disabled={Boolean(busy)}\n helperText=\"The account this person works for. Type a name nobody has filed yet to create it.\"\n />\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n setLifecycleStage(event.target.value as ContactLifecycleStage | '')\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Not placed yet'}</MenuItem>\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => setOwnerUid(event.target.value)}\n helperText={\n ownersReady && !owners.length\n ? 'The team roster could not be read, so nobody can be picked yet.'\n : 'The team member responsible for this relationship'\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Unassigned'}</MenuItem>\n {owners.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {crmMemberPickerLabel(owner)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => setTags(event.target.value)}\n fullWidth\n />\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n <ContactAddressFields value={address} onChange={setAddress} />\n <Stack>\n <FormControlLabel\n control={\n <Checkbox\n checked={marketingConsent}\n onChange={(event) => setMarketingConsent(event.target.checked)}\n />\n }\n label=\"This person opted in to marketing email\"\n />\n <FormHelperText>\n {(disclosure ? `${disclosure} ` : '') +\n 'Tick it only if they agreed. Adding a contact is never itself consent.'}\n </FormHelperText>\n </Stack>\n <Button\n variant=\"contained\"\n color=\"primary\"\n // Held until the capturing site is known: the route resolves the\n // org from it and stamps the record with it.\n disabled={Boolean(busy) || !createHostId}\n onClick={handleSubmit}\n >\n {busy ? 'Adding…' : 'Add contact'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\n\nexport default NewContactDrawer\n"],"names":["CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","consentGroupDisclosure","consentGroupDisclosureKey","normalizeAddress","normalizeContactEmail","normalizePhone","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","Alert","Button","Checkbox","FormControlLabel","FormHelperText","IconButton","MenuItem","Stack","TextField","Typography","useEffect","useState","useCrmScope","parseContactTags","CompanyPicker","useCompanyOptions","useCreateCompany","ContactAddressFields","EMPTY_ADDRESS","CrmSitePicker","NewContactDrawer","props","open","onClose","hostId","org","busy","error","owners","ownersReady","onSubmit","createHostId","createGroup","consentGroup","companies","enabled","createCompany","email","setEmail","name","setName","phone","setPhone","jobTitle","setJobTitle","company","setCompany","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","address","setAddress","marketingConsent","setMarketingConsent","emailError","setEmailError","phoneError","setPhoneError","handleSubmit","normalizedEmail","trimmedPhone","trim","normalizedPhone","slice","companyName","companyId","id","disclosedConsentGroup","disclosure","anchor","variant","AppBarProps","color","sx","width","xs","sm","appBarLeft","edge","onClick","mr","path","component","appBarRight","gutterY","spacing","severity","disabled","Boolean","helperText","size","label","type","value","onChange","event","target","required","fullWidth","autoFocus","slotProps","htmlInput","maxLength","options","ready","truncated","_id","picked","onCreate","select","map","stage","length","owner","uid","placeholder","control","checked"],"mappings":"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"}
|
|
@@ -58,6 +58,9 @@
|
|
|
58
58
|
// array — `orderBy` on the same field an `array-contains` matches is a
|
|
59
59
|
// composite index nobody wants to explain.
|
|
60
60
|
containsOrderBy: 'updatedAt',
|
|
61
|
+
// Newest first, as the unfiltered list, which is the composite declared
|
|
62
|
+
// for it (AGL-3321).
|
|
63
|
+
containsOrderDirection: 'desc',
|
|
61
64
|
// `isAnyOf` is a saved segment's "any of these tags" (AGL-2617). The
|
|
62
65
|
// query cannot serve it — `array-contains-any` is the scope clause's
|
|
63
66
|
// — so the translator refuses it and the list matches it over the
|
|
@@ -87,6 +90,9 @@
|
|
|
87
90
|
tokensPath: 'formIds',
|
|
88
91
|
verbatimTokens: true,
|
|
89
92
|
containsOrderBy: 'updatedAt',
|
|
93
|
+
// Newest first, as the unfiltered list, which is the composite declared
|
|
94
|
+
// for it (AGL-3321).
|
|
95
|
+
containsOrderDirection: 'desc',
|
|
90
96
|
operators: [
|
|
91
97
|
'contains'
|
|
92
98
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/constants/contact-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 type ContactFieldDefinition,\n CRM_CONTACT_VIEW_FIELDS,\n crmContactCustomColumn,\n} from '@aglyn/aglyn'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\n\n/*\n * Contacts (`orgs/{orgId}/contacts`, read scoped to a host).\n *\n * The card's listener is `limit(1000)` and stays that way — nobody needs\n * forty thousand rows streamed into a table. What that cap must NOT do is\n * decide what a search can find, and it did: an org with 40,000 contacts\n * searched 1,000 of them and answered \"no contacts match\" for the rest. The\n * head-count already learned this lesson (AGL-1706, a server aggregate); the\n * search is the same mistake one field over.\n *\n * ⛔ `marketingConsent`, `ordersCount` and `ltvCents` are written only when\n * they apply, so a `false`/`0` filter would return nothing rather than\n * everyone else. `isNotEmpty` is exact on them and is what the menu offers.\n */\nexport const CONTACT_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n reversedPath: 'nameReversed',\n },\n {\n // `normalizeContactEmail` lower-cases before every write, so the stored\n // value IS its own normalized key.\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n },\n {\n /*\n * Already an array of lower-cased tags, so `contains` is a plain\n * `array-contains` — the one place the word-level caveat does not apply,\n * because a tag is matched whole rather than by word prefix.\n */\n column: 'tags',\n kind: 'text',\n path: 'tags',\n tokensPath: 'tags',\n // Ordered by the timestamp the list already sorts on, not by the tag\n // array — `orderBy` on the same field an `array-contains` matches is a\n // composite index nobody wants to explain.\n containsOrderBy: 'updatedAt',\n // `isAnyOf` is a saved segment's \"any of these tags\" (AGL-2617). The\n // query cannot serve it — `array-contains-any` is the scope clause's\n // — so the translator refuses it and the list matches it over the\n // window, which the caption says.\n operators: ['contains', 'isAnyOf', 'isNotEmpty'],\n },\n {\n /*\n * The forms a person came in through — the top-level `formIds` mirror\n * of the interactions' `formId` (AGL-2612), matched whole and as typed\n * because a form id is minted with mixed case. The form's own page links\n * here with its id; nobody types one. Ordered by the list's own\n * timestamp for the reason `tags` is, served by the\n * `(formIds CONTAINS, updatedAt DESC)` index.\n *\n * ⚠️ An `array-contains` on the mirror cannot share a query with the\n * `visibleTo` scope clause, so the list drops that clause for this one\n * filter and the rules admit the read to an org-wide member only — the\n * caveat the company contacts card states, stated again on the list.\n */\n column: 'formIds',\n kind: 'text',\n path: 'formIds',\n tokensPath: 'formIds',\n verbatimTokens: true,\n containsOrderBy: 'updatedAt',\n operators: ['contains'],\n },\n { column: 'hostId', kind: 'exact', path: 'hostId', presence: 'always' },\n { column: 'ordersCount', kind: 'number', path: 'ordersCount' },\n { column: 'ltvCents', kind: 'number', path: 'ltvCents' },\n { column: 'createdAt', kind: 'date', path: 'createdAt', presence: 'always' },\n { column: 'updatedAt', kind: 'date', path: 'updatedAt', presence: 'always' },\n /*\n * When the earliest open task is due (AGL-2661) — window-only, because\n * the value is on the SHARED row but a record written before the field\n * existed has none, and Firestore cannot query for absence. `nullable`\n * is honest: the writer stores `null` for nothing scheduled, and the\n * matcher reads absent the same way, so \"is empty\" is the \"No next\n * activity\" filter over the loaded window.\n */\n {\n column: 'nextTaskAtMs',\n kind: 'date',\n path: 'nextTaskAtMs',\n windowOnly: true,\n presence: 'nullable',\n },\n /*\n * THE FACET FIELDS (AGL-2617) — window-only, every one of them.\n *\n * An owner, a stage, a company and the capture sources live on the\n * viewing group's facet of the shared contact row, and a facet path is\n * per group: a `where` on it would need an index per group, and the\n * translator would send a scoped member a query the rules deny. So these\n * are declared for the menu and for a saved view, matched over the loaded\n * window against the flattened row (`ContactRecord` carries each at the\n * top), and never put on a query — `windowOnly` is what holds that line.\n * The paths are the ROW'S, which is what the matcher reads.\n *\n * The values are picked, not typed — a uid from the roster, a stage from\n * the fixed list, a source from its labels, a company from the picker —\n * so `equals` and `isAnyOf` are what they offer; the section supplies\n * the choices. The column names are the contract `CRM_CONTACT_VIEW_FIELDS`\n * states, because the dynamic-list translator reads a view by them.\n */\n {\n column: CRM_CONTACT_VIEW_FIELDS.owner,\n kind: 'exact',\n path: 'ownerUid',\n windowOnly: true,\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.stage,\n kind: 'exact',\n path: 'lifecycleStage',\n windowOnly: true,\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n {\n // The `sources` presence map, matched on its keys — see `keysOf`.\n column: CRM_CONTACT_VIEW_FIELDS.source,\n kind: 'exact',\n path: 'sources',\n keysOf: true,\n windowOnly: true,\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.company,\n kind: 'exact',\n path: 'companyId',\n windowOnly: true,\n operators: ['equals', 'isNotEmpty'],\n },\n /*\n * The verdict on the address (AGL-3245) — window-only, on the shared row,\n * and `nullable` because most records carry none: \"is empty\" is the\n * filter for people nothing has been said about.\n */\n {\n column: 'emailState',\n kind: 'exact',\n path: 'emailState.status',\n windowOnly: true,\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty', 'isNotEmpty'],\n },\n]\n\n/**\n * One filter field per active custom contact field (AGL-2617).\n *\n * A custom value lives under the facet's `custom` map, so every one is\n * window-only for the reason the facet fields above are, and its column is\n * the one the table shows it under — `crmContactCustomColumn(key)` — so a\n * clause and a column agree on the name and a saved view's columns and\n * filters name the same thing. The kind follows the definition's type, and\n * `nullable` is how a cleared value is stored, which is what makes both\n * empty operators honest here.\n */\nexport function contactCustomFilterFields(\n definitions: readonly Pick<ContactFieldDefinition, 'key' | 'type' | 'retiredAt'>[],\n): ListFilterField[] {\n return definitions\n .filter((definition) => !definition.retiredAt)\n .map((definition) => {\n const column = crmContactCustomColumn(definition.key)\n const path = `custom.${definition.key}`\n switch (definition.type) {\n case 'number':\n return { column, kind: 'number', path, windowOnly: true, presence: 'nullable' }\n case 'date':\n return { column, kind: 'date', path, windowOnly: true, presence: 'nullable' }\n case 'checkbox':\n return { column, kind: 'boolean', path, windowOnly: true }\n case 'select':\n return {\n column,\n kind: 'exact',\n path,\n windowOnly: true,\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty', 'isNotEmpty'],\n }\n default:\n return {\n column,\n kind: 'text',\n path,\n windowOnly: true,\n presence: 'nullable',\n operators: ['contains', 'equals', 'startsWith', 'isEmpty', 'isNotEmpty'],\n }\n }\n })\n}\n\n/** The custom fields' headers, keyed by the column each filters as. */\nexport function contactCustomFilterHeaders(\n definitions: readonly Pick<ContactFieldDefinition, 'key' | 'label'>[],\n): Record<string, string> {\n return Object.fromEntries(\n definitions.map((definition) => [\n crmContactCustomColumn(definition.key),\n definition.label || definition.key,\n ]),\n )\n}\n\n/**\n * How every filterable contact field reads — on a chip, in the add-filter\n * picker, and as the header of a filter-only hidden column.\n */\nexport const CONTACT_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Contact',\n email: 'Email',\n tags: 'Tags',\n formIds: 'Form ID',\n hostId: 'Site ID',\n ordersCount: 'Orders',\n ltvCents: 'Lifetime value (cents)',\n createdAt: 'Created',\n updatedAt: 'Updated',\n nextTaskAtMs: 'Next activity',\n [CRM_CONTACT_VIEW_FIELDS.owner]: 'Owner',\n [CRM_CONTACT_VIEW_FIELDS.stage]: 'Stage',\n [CRM_CONTACT_VIEW_FIELDS.source]: 'Source',\n [CRM_CONTACT_VIEW_FIELDS.company]: 'Company',\n emailState: 'Email',\n}\n"],"names":["CRM_CONTACT_VIEW_FIELDS","crmContactCustomColumn","CONTACT_LIST_FILTER_FIELDS","column","kind","path","lowerPath","tokensPath","reversedPath","presence","containsOrderBy","operators","verbatimTokens","windowOnly","owner","stage","source","keysOf","company","contactCustomFilterFields","definitions","filter","definition","retiredAt","map","key","type","contactCustomFilterHeaders","Object","fromEntries","label","CONTACT_LIST_FILTER_HEADERS","name","email","tags","formIds","hostId","ordersCount","ltvCents","createdAt","updatedAt","nextTaskAtMs","emailState"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/constants/contact-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 type ContactFieldDefinition,\n CRM_CONTACT_VIEW_FIELDS,\n crmContactCustomColumn,\n} from '@aglyn/aglyn'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\n\n/*\n * Contacts (`orgs/{orgId}/contacts`, read scoped to a host).\n *\n * The card's listener is `limit(1000)` and stays that way — nobody needs\n * forty thousand rows streamed into a table. What that cap must NOT do is\n * decide what a search can find, and it did: an org with 40,000 contacts\n * searched 1,000 of them and answered \"no contacts match\" for the rest. The\n * head-count already learned this lesson (AGL-1706, a server aggregate); the\n * search is the same mistake one field over.\n *\n * ⛔ `marketingConsent`, `ordersCount` and `ltvCents` are written only when\n * they apply, so a `false`/`0` filter would return nothing rather than\n * everyone else. `isNotEmpty` is exact on them and is what the menu offers.\n */\nexport const CONTACT_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n reversedPath: 'nameReversed',\n },\n {\n // `normalizeContactEmail` lower-cases before every write, so the stored\n // value IS its own normalized key.\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n },\n {\n /*\n * Already an array of lower-cased tags, so `contains` is a plain\n * `array-contains` — the one place the word-level caveat does not apply,\n * because a tag is matched whole rather than by word prefix.\n */\n column: 'tags',\n kind: 'text',\n path: 'tags',\n tokensPath: 'tags',\n // Ordered by the timestamp the list already sorts on, not by the tag\n // array — `orderBy` on the same field an `array-contains` matches is a\n // composite index nobody wants to explain.\n containsOrderBy: 'updatedAt',\n // Newest first, as the unfiltered list, which is the composite declared\n // for it (AGL-3321).\n containsOrderDirection: 'desc',\n // `isAnyOf` is a saved segment's \"any of these tags\" (AGL-2617). The\n // query cannot serve it — `array-contains-any` is the scope clause's\n // — so the translator refuses it and the list matches it over the\n // window, which the caption says.\n operators: ['contains', 'isAnyOf', 'isNotEmpty'],\n },\n {\n /*\n * The forms a person came in through — the top-level `formIds` mirror\n * of the interactions' `formId` (AGL-2612), matched whole and as typed\n * because a form id is minted with mixed case. The form's own page links\n * here with its id; nobody types one. Ordered by the list's own\n * timestamp for the reason `tags` is, served by the\n * `(formIds CONTAINS, updatedAt DESC)` index.\n *\n * ⚠️ An `array-contains` on the mirror cannot share a query with the\n * `visibleTo` scope clause, so the list drops that clause for this one\n * filter and the rules admit the read to an org-wide member only — the\n * caveat the company contacts card states, stated again on the list.\n */\n column: 'formIds',\n kind: 'text',\n path: 'formIds',\n tokensPath: 'formIds',\n verbatimTokens: true,\n containsOrderBy: 'updatedAt',\n // Newest first, as the unfiltered list, which is the composite declared\n // for it (AGL-3321).\n containsOrderDirection: 'desc',\n operators: ['contains'],\n },\n { column: 'hostId', kind: 'exact', path: 'hostId', presence: 'always' },\n { column: 'ordersCount', kind: 'number', path: 'ordersCount' },\n { column: 'ltvCents', kind: 'number', path: 'ltvCents' },\n { column: 'createdAt', kind: 'date', path: 'createdAt', presence: 'always' },\n { column: 'updatedAt', kind: 'date', path: 'updatedAt', presence: 'always' },\n /*\n * When the earliest open task is due (AGL-2661) — window-only, because\n * the value is on the SHARED row but a record written before the field\n * existed has none, and Firestore cannot query for absence. `nullable`\n * is honest: the writer stores `null` for nothing scheduled, and the\n * matcher reads absent the same way, so \"is empty\" is the \"No next\n * activity\" filter over the loaded window.\n */\n {\n column: 'nextTaskAtMs',\n kind: 'date',\n path: 'nextTaskAtMs',\n windowOnly: true,\n presence: 'nullable',\n },\n /*\n * THE FACET FIELDS (AGL-2617) — window-only, every one of them.\n *\n * An owner, a stage, a company and the capture sources live on the\n * viewing group's facet of the shared contact row, and a facet path is\n * per group: a `where` on it would need an index per group, and the\n * translator would send a scoped member a query the rules deny. So these\n * are declared for the menu and for a saved view, matched over the loaded\n * window against the flattened row (`ContactRecord` carries each at the\n * top), and never put on a query — `windowOnly` is what holds that line.\n * The paths are the ROW'S, which is what the matcher reads.\n *\n * The values are picked, not typed — a uid from the roster, a stage from\n * the fixed list, a source from its labels, a company from the picker —\n * so `equals` and `isAnyOf` are what they offer; the section supplies\n * the choices. The column names are the contract `CRM_CONTACT_VIEW_FIELDS`\n * states, because the dynamic-list translator reads a view by them.\n */\n {\n column: CRM_CONTACT_VIEW_FIELDS.owner,\n kind: 'exact',\n path: 'ownerUid',\n windowOnly: true,\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.stage,\n kind: 'exact',\n path: 'lifecycleStage',\n windowOnly: true,\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n {\n // The `sources` presence map, matched on its keys — see `keysOf`.\n column: CRM_CONTACT_VIEW_FIELDS.source,\n kind: 'exact',\n path: 'sources',\n keysOf: true,\n windowOnly: true,\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.company,\n kind: 'exact',\n path: 'companyId',\n windowOnly: true,\n operators: ['equals', 'isNotEmpty'],\n },\n /*\n * The verdict on the address (AGL-3245) — window-only, on the shared row,\n * and `nullable` because most records carry none: \"is empty\" is the\n * filter for people nothing has been said about.\n */\n {\n column: 'emailState',\n kind: 'exact',\n path: 'emailState.status',\n windowOnly: true,\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty', 'isNotEmpty'],\n },\n]\n\n/**\n * One filter field per active custom contact field (AGL-2617).\n *\n * A custom value lives under the facet's `custom` map, so every one is\n * window-only for the reason the facet fields above are, and its column is\n * the one the table shows it under — `crmContactCustomColumn(key)` — so a\n * clause and a column agree on the name and a saved view's columns and\n * filters name the same thing. The kind follows the definition's type, and\n * `nullable` is how a cleared value is stored, which is what makes both\n * empty operators honest here.\n */\nexport function contactCustomFilterFields(\n definitions: readonly Pick<ContactFieldDefinition, 'key' | 'type' | 'retiredAt'>[],\n): ListFilterField[] {\n return definitions\n .filter((definition) => !definition.retiredAt)\n .map((definition) => {\n const column = crmContactCustomColumn(definition.key)\n const path = `custom.${definition.key}`\n switch (definition.type) {\n case 'number':\n return { column, kind: 'number', path, windowOnly: true, presence: 'nullable' }\n case 'date':\n return { column, kind: 'date', path, windowOnly: true, presence: 'nullable' }\n case 'checkbox':\n return { column, kind: 'boolean', path, windowOnly: true }\n case 'select':\n return {\n column,\n kind: 'exact',\n path,\n windowOnly: true,\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty', 'isNotEmpty'],\n }\n default:\n return {\n column,\n kind: 'text',\n path,\n windowOnly: true,\n presence: 'nullable',\n operators: ['contains', 'equals', 'startsWith', 'isEmpty', 'isNotEmpty'],\n }\n }\n })\n}\n\n/** The custom fields' headers, keyed by the column each filters as. */\nexport function contactCustomFilterHeaders(\n definitions: readonly Pick<ContactFieldDefinition, 'key' | 'label'>[],\n): Record<string, string> {\n return Object.fromEntries(\n definitions.map((definition) => [\n crmContactCustomColumn(definition.key),\n definition.label || definition.key,\n ]),\n )\n}\n\n/**\n * How every filterable contact field reads — on a chip, in the add-filter\n * picker, and as the header of a filter-only hidden column.\n */\nexport const CONTACT_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Contact',\n email: 'Email',\n tags: 'Tags',\n formIds: 'Form ID',\n hostId: 'Site ID',\n ordersCount: 'Orders',\n ltvCents: 'Lifetime value (cents)',\n createdAt: 'Created',\n updatedAt: 'Updated',\n nextTaskAtMs: 'Next activity',\n [CRM_CONTACT_VIEW_FIELDS.owner]: 'Owner',\n [CRM_CONTACT_VIEW_FIELDS.stage]: 'Stage',\n [CRM_CONTACT_VIEW_FIELDS.source]: 'Source',\n [CRM_CONTACT_VIEW_FIELDS.company]: 'Company',\n emailState: 'Email',\n}\n"],"names":["CRM_CONTACT_VIEW_FIELDS","crmContactCustomColumn","CONTACT_LIST_FILTER_FIELDS","column","kind","path","lowerPath","tokensPath","reversedPath","presence","containsOrderBy","containsOrderDirection","operators","verbatimTokens","windowOnly","owner","stage","source","keysOf","company","contactCustomFilterFields","definitions","filter","definition","retiredAt","map","key","type","contactCustomFilterHeaders","Object","fromEntries","label","CONTACT_LIST_FILTER_HEADERS","name","email","tags","formIds","hostId","ordersCount","ltvCents","createdAt","updatedAt","nextTaskAtMs","emailState"],"mappings":"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"}
|