@aglyn/plugins-crm 1.0.0-beta.232 → 1.0.0-beta.234

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/companies-section.js +43 -8
  3. package/src/lib/components/companies-section.js.map +1 -1
  4. package/src/lib/components/company-edit-drawer.js +3 -1
  5. package/src/lib/components/company-edit-drawer.js.map +1 -1
  6. package/src/lib/components/contact-detail-page.js +6 -1
  7. package/src/lib/components/contact-detail-page.js.map +1 -1
  8. package/src/lib/components/contacts-section.js +37 -8
  9. package/src/lib/components/contacts-section.js.map +1 -1
  10. package/src/lib/components/crm-email-check.d.ts +14 -0
  11. package/src/lib/components/crm-email-check.js +38 -1
  12. package/src/lib/components/crm-email-check.js.map +1 -1
  13. package/src/lib/components/crm-next-activity-column.d.ts +5 -4
  14. package/src/lib/components/crm-next-activity-column.js +5 -4
  15. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  16. package/src/lib/components/deal-edit-drawer.js +3 -1
  17. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  18. package/src/lib/components/deals-section.js +50 -6
  19. package/src/lib/components/deals-section.js.map +1 -1
  20. package/src/lib/components/lead-properties-card.js +9 -3
  21. package/src/lib/components/lead-properties-card.js.map +1 -1
  22. package/src/lib/components/leads-section.js +61 -11
  23. package/src/lib/components/leads-section.js.map +1 -1
  24. package/src/lib/components/tasks-section.js +61 -9
  25. package/src/lib/components/tasks-section.js.map +1 -1
  26. package/src/lib/constants/company-filters.js +40 -2
  27. package/src/lib/constants/company-filters.js.map +1 -1
  28. package/src/lib/constants/contact-filters.d.ts +10 -1
  29. package/src/lib/constants/contact-filters.js +62 -2
  30. package/src/lib/constants/contact-filters.js.map +1 -1
  31. package/src/lib/constants/deal-filters.js +78 -0
  32. package/src/lib/constants/deal-filters.js.map +1 -1
  33. package/src/lib/hooks/use-crm-column-sort.d.ts +67 -0
  34. package/src/lib/hooks/use-crm-column-sort.js +156 -0
  35. package/src/lib/hooks/use-crm-column-sort.js.map +1 -0
  36. package/src/lib/hooks/use-crm-tasks.d.ts +6 -1
  37. package/src/lib/hooks/use-crm-tasks.js +10 -8
  38. package/src/lib/hooks/use-crm-tasks.js.map +1 -1
  39. package/src/lib/model/crm-list-query.d.ts +6 -0
  40. package/src/lib/model/crm-list-query.js +13 -0
  41. package/src/lib/model/crm-list-query.js.map +1 -1
  42. package/src/lib/model/lead-filters.d.ts +19 -2
  43. package/src/lib/model/lead-filters.js +112 -2
  44. package/src/lib/model/lead-filters.js.map +1 -1
  45. package/src/lib/model/native-vocabulary.d.ts +41 -0
  46. package/src/lib/model/native-vocabulary.js +59 -0
  47. package/src/lib/model/native-vocabulary.js.map +1 -0
  48. package/src/lib/model/task-views.d.ts +15 -2
  49. package/src/lib/model/task-views.js +50 -4
  50. package/src/lib/model/task-views.js.map +1 -1
  51. package/src/lib/server/email-check.d.ts +4 -1
  52. package/src/lib/server/email-check.js +18 -6
  53. package/src/lib/server/email-check.js.map +1 -1
  54. package/src/lib/transfer/tasks.js +2 -1
  55. package/src/lib/transfer/tasks.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contacts-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 {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n CONTACT_SOURCE_LABELS,\n CRM_CONTACT_VIEW_FIELDS,\n type CrmViewFilterClause,\n crmContactCustomColumn,\n crmViewFiltersFromSegment,\n crmViewSegmentFilters,\n newResourceScopeFields,\n ORG_SCOPE_TOKEN,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { type ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n CONTACT_LEAD_SOURCE_COLUMN,\n CONTACT_LIST_DECLARATION,\n CONTACT_LIST_FILTER_FIELDS,\n CONTACT_LIST_FILTER_HEADERS,\n CONTACT_PREFIX_SEARCH,\n contactClauseImpliesScope,\n contactSoloClause,\n contactCustomFilterFields,\n contactCustomFilterHeaders,\n contactQueryClause,\n} from '../constants/contact-filters'\nimport { crmAskClauses, crmQueryRefusals } from '../model/crm-list-query'\nimport { useCrmFoldsScope, useCrmListQuery } from '../hooks/use-crm-list-query'\nimport {\n type ContactRecord,\n contactPrimaryGroup,\n contactRecordFromDoc,\n} from '../model/contact-record'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCrmSharingRules } from '../hooks/use-crm-sharing'\nimport { crmShareChipLabel } from '../model/crm-sharing'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { contactsListSeed } from '../model/contacts-list-seed'\nimport { crmRoutes } from '../model/crm-routes'\nimport { CRM_CONTACTS_RESOURCE } from '../transfer/fields'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport ContactsBulkBar from './contacts-bulk-bar'\nimport { CrmListActions } from './crm-list-toolbar'\nimport { CrmExportButton, CrmImportButton } from './crm-transfer-buttons'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteDoc,\n doc,\n limit,\n orderBy,\n query,\n} from 'firebase/firestore'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport RecentActivityFeed from './recent-activity-feed'\nimport {\n CONTACT_FILTER_COLUMNS,\n CONTACT_OPTIONAL_COLUMNS,\n contactListColumns,\n} from './contact-list-columns'\nimport NewContactDrawer, { type NewContactValues } from './new-contact-drawer'\nimport { useCrmApi } from './use-crm-api'\nimport { useOrgMembers } from './use-org-members'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\nimport { customFieldColumns } from './contact-custom-columns'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport { useCompanyOptions } from './company-picker'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\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 CrmViewsControl, { type CrmViewPreset } from './crm-views-control'\nimport { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from './crm-suite-lock'\n\n/**\n * The shared labels, under the name this file has always called them.\n *\n * The map lives beside the `ContactSource` union so the dynamic-list rule\n * editor and this filter cannot disagree about what `order` is called.\n */\nconst SOURCE_LABELS = CONTACT_SOURCE_LABELS\n\n/**\n * What the list keeps out of sight until a view says otherwise: the\n * filter-only columns, which never show, and the optional columns\n * (AGL-2616), which start hidden and come back through the column menu or\n * a saved view's column list.\n */\nconst HIDDEN_COLUMNS: Readonly<Record<string, boolean>> = {\n ...hiddenFilterVisibility(CONTACT_LIST_FILTER_FIELDS, CONTACT_FILTER_COLUMNS),\n ...Object.fromEntries(CONTACT_OPTIONAL_COLUMNS.map((field) => [field, false])),\n}\n\n/**\n * Why a refund found no contact to record itself against (AGL-2329).\n *\n * The three reasons `recordContactRefund` distinguishes, in the operator's\n * language rather than the writer's enum. They are not interchangeable: two\n * of them are things the merchant can act on and one is a deletion nothing\n * should undo, and collapsing them into \"unmatched\" would turn an actionable\n * fact into a statistic.\n */\nconst UNMATCHED_REFUND_REASON: Record<string, string> = {\n 'no-email': 'the order carried no email address',\n 'no-contact': 'no contact record matched the buyer',\n 'contact-deleted': 'the contact was deleted between the sale and the refund',\n}\n\n/**\n * Contacts CRM (AGL-198): the unified people list fed by AGL-197's\n * ingestion — search, source badges, a profile drawer with the\n * interaction timeline plus tags/notes editing, and CSV export. Part of\n * the CRM, included from Starter (AGL-2851); the contactsPerHost band\n * counts the records in it.\n */\n/**\n * Contacts CRM (AGL-109 → AGL-395): the unified contacts list, segments,\n * and profile drawer, owned by the contacts plugin and rendered by the\n * shell's generic plugin route. The shell applies the `release_crm`\n * gate (via the nav tab) and passes the resolved `org` doc for the\n * `contactsPerHost` quota check.\n *\n * Since AGL-2595 this is the `people` SECTION of the Contacts hub rather\n * than the whole page: `crm-console-page.tsx` routes to it, and the\n * companies, deals, tasks, reports and fields sections sit beside it. The\n * body is the v1 page unchanged, and it takes the shell's full prop bag so\n * nothing the v1 page read has to be threaded through the hub by name.\n */\nexport function ContactsPeopleSection(props: ConsolePluginPageProps) {\n const { hostId, org, releaseFlag, basePath } = props\n /*\n * A row is a LINK now (AGL-2596): opening a person navigates to their own\n * page rather than a drawer over the list, so the address can be pasted\n * and every other CRM record can point at it. `basePath` is what the hub\n * hands every section; the empty fallback only exists for a direct mount\n * without the shell, where there is nowhere to navigate to anyway.\n */\n const routes = crmRoutes(basePath ?? '')\n const router = useRouter()\n // Whether the audience overage on this page is actually INVOICED\n // (AGL-1662), and whether that question has been answered yet.\n //\n // AGL-1604 stopped the usage cron putting `contactsOverageUsd` into\n // `billedCents` while `release_crm` is off for the org; `db5ecdf2b`\n // taught the console billing page's caption the same thing. This page's own\n // alert still quoted the dollar figure with no flag check — and this is the\n // surface a staff member reaches with the flag OFF, because the shell's\n // `FeatureGate` admits them on `visible` (`released || isStaff`). Support\n // then reads that number back to a customer whose invoice will not carry it.\n //\n // The shell resolves this from `released`, never `visible`: staff opening a\n // page does not put a line on the customer's bill.\n //\n // Both default to the WITHHELD answer when the prop is absent, which only\n // happens in a direct mount — the shell always supplies it for a surface\n // with a nav-tab flag. A caller that has not resolved the verdict has not\n // earned the right to quote a charge.\n const contactsBilled = releaseFlag?.released ?? false\n const releaseFlagsReady = releaseFlag?.ready ?? false\n // Org-shared data root (AGL-237). Null until the org lookup settles\n // (AGL-1061), and for a host with no owning org — the pre-migration host\n // path this used to fall back to is gone (AGL-1050), so the CRM lists\n // nothing rather than listing somewhere else. Resolved by the one scope\n // hook (AGL-2614), which also answers for the organization-level mount\n // (AGL-2630): there `hostId` is null, the org comes from the hub's mount,\n // and the viewing group and the scope clause are both `null`.\n const crmScope = useCrmScope({ hostId, org })\n const { scope: dataScope, orgId, consentGroup, visibleTo: visibleToTokens } = crmScope\n /*\n * THE CRM (AGL-2788). This list is the CRM's, and the shell mounts no CRM\n * page for a plan without it (AGL-2851). The locks below — adding a person\n * by hand, importing a file, a saved view, the owner, stage and company of\n * a selection — draw only for an org whose plan lacks the CRM. The shell\n * mounts this page only once the org has settled, so the plan read here\n * is an answer.\n */\n const suiteIncluded = crmSuiteIncluded(org)\n // The org's site list, at the organization level only — what names the\n // sites in the \"Known by\" column (AGL-2630).\n const mount = useCrmOrgMount()\n // The org's custom fields, for the optional columns below (AGL-2601).\n const customFields = useContactFieldDefinitions(dataScope?.[1] ?? null)\n // The org's lead source values, the Lead source filter's choices (AGL-3511).\n const leadSourceList = useLeadSourcePicklist(orgId)\n /*==========================================\n * THE CONTROLLER THIS PAGE IS SHOWING.\n *\n * A contact document is shared by every site in the org — one human who\n * touched two sites is one row, which is the dedupe the shared address book\n * exists for and the reason the org is billed once for them. Almost nothing\n * ON that row is shared: the notes, tags, timeline and lifetime value are\n * the holder's own business records, and while they lived at the top of the\n * document every site in an agency's account could read every client's.\n *\n * So the page resolves the group it is being viewed as — the sites declared\n * to be one sender, or this site alone — and reads that group's facet.\n * Pure, from the org document the shell already passed, so it costs no\n * read. `consentGroup` above is that group; at the ORGANIZATION level it\n * is `null`, and each row is flattened through the person's own primary\n * holder instead (`contactPrimaryGroup`), so an org-wide member reads the\n * profile the first capturing site keeps — and the \"Known by\" column says\n * which other sites hold one.\n *=========================================*/\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n\n /*\n * WHAT THE LIST WAS OPENED FOR (AGL-2612).\n *\n * Two surfaces address this list with a question rather than a record: a\n * form's own page (\"who came in through this form\") and an Inbox\n * submission row (\"open the person with this address\"). Both arrive as\n * query keys, parsed once into the same filter the grid's panel would\n * produce, so the seeded query IS a typed query and nothing here is a\n * second predicate.\n */\n const searchParams = useSearchParams()\n const seed = useMemo(() => contactsListSeed(searchParams), [searchParams])\n /*\n * The seed as CLAUSES, in front of everything else (AGL-2617).\n *\n * A view holds a list of clauses in the grammar the grid's panel speaks,\n * and the seed is two of them — the form's captures and the address, plus\n * the source the form link names. They lead every state the list works\n * from, whichever view is open, so the query serves the seed (it reaches\n * the whole collection, as it did before views) and a view narrows what\n * comes back. The controller keeps them out of what a view SAVES.\n */\n const seedFilters = useMemo<CrmViewFilterClause[]>(\n () => [\n ...(seed.filter ? [seed.filter] : []),\n // A form's captures all came in through a form: the source beside it\n // would ask a second array clause of the one query (AGL-3321).\n ...(seed.source && !seed.formId\n ? [{ field: CRM_CONTACT_VIEW_FIELDS.source, op: 'equals', value: seed.source }]\n : []),\n ],\n [seed],\n )\n const viewInitial = useMemo(() => ({ filters: seedFilters }), [seedFilters])\n const views = useCrmSavedView({\n section: 'contacts',\n hostId,\n org,\n basePath: basePath ?? '',\n initial: viewInitial,\n })\n /*\n * The grammar this list speaks: the declared fields, and one field per\n * custom contact field the org has defined — so a view can filter on\n * \"plan is enterprise\" the way it filters on a stage.\n */\n const filterFields = useMemo(\n () => [...CONTACT_LIST_FILTER_FIELDS, ...contactCustomFilterFields(customFields.active)],\n [customFields.active],\n )\n const filterHeaders = useMemo(\n () => ({\n ...CONTACT_LIST_FILTER_HEADERS,\n ...contactCustomFilterHeaders(customFields.active),\n }),\n [customFields.active],\n )\n /*\n * EVERY CLAUSE AND THE SEARCH ON ONE QUERY (AGL-3321).\n *\n * Each stored clause is asked through the field its writers keep\n * (`contactQueryClause`): a holder's owner, stage, source, company, tags,\n * custom value or \"has bought\" as that holder's facet key, the verdict as\n * `emailStatus`, the rest as stored. The query pages the list newest\n * change first and carries all of it; what one query cannot hold is\n * refused by name above the list, and nothing is matched over the rows a\n * page happened to load. Under a site the scope clause is the query's one\n * array clause — the search folds into it, and a facet or form clause\n * stands in its place, for a reader whose access is not limited to\n * particular sites.\n */\n const foldsScope = useCrmFoldsScope(orgId, visibleToTokens)\n const groupId = consentGroup?.groupId ?? null\n const asked = useMemo(\n () =>\n crmAskClauses(views.state.filters, (clause) =>\n contactQueryClause(clause, { groupId, foldsScope }),\n ),\n [views.state.filters, groupId, foldsScope],\n )\n const impliesScope = useMemo(() => contactClauseImpliesScope(groupId), [groupId])\n // The quick search is the grid's box, answered by the same query.\n const [searchWords, setSearchWords] = useState<string[]>([])\n const paged = useCrmListQuery<any>({\n scope: dataScope,\n collection: 'contacts',\n visibleTo: visibleToTokens,\n foldsScope,\n declaration: CONTACT_LIST_DECLARATION,\n clauses: asked.clauses,\n search: searchWords,\n impliesScope,\n soloClause: contactSoloClause,\n prefixSearch: CONTACT_PREFIX_SEARCH,\n })\n const contactDocs = paged.rows\n const contactsStatus = paged.status\n /*\n * OPENED FOR ONE ADDRESS: move straight on to the record (AGL-2612).\n *\n * The Inbox links here with an email because a contact's id is minted at\n * capture and nothing outside the CRM holds it; the list is the lookup.\n * Once the seeded `email equals` query has answered with exactly one row,\n * that row is the person and the list is a detour — `replace`, so Back\n * returns to the Inbox rather than to this redirect. Nothing else\n * navigates: no match leaves the filtered list on screen, which is the\n * honest answer for a submission whose contact the band dropped, and a\n * filter the reader has since changed is theirs.\n */\n const openedByEmail = useRef(false)\n useEffect(() => {\n if (openedByEmail.current || !seed.openEmail) return\n if (contactsStatus !== 'success') return\n const served = paged.plan.served.some(\n (clause) => clause.field === 'email' && clause.op === 'equals' && clause.value === seed.openEmail,\n )\n if (!served) return\n const only = contactDocs.length === 1 && !paged.hasMore ? contactDocs[0] : null\n if (!only) return\n openedByEmail.current = true\n router.replace(routes.contact(String(only.$id)))\n }, [seed.openEmail, contactsStatus, paged.plan, paged.hasMore, contactDocs, router, routes])\n /**\n * Every row, flattened through THIS group's facet.\n *\n * One projection rather than a facet read at each of the nine places that\n * touch `tags`, `notes`, `name` or `interactions`: a surface that reads one\n * of them off the top of the document is a surface showing another\n * holder's records, and nine chances to forget is nine leaks.\n *\n * The canonical `name` is the fallback and the shared identity; a holder's\n * own override wins, so one business renaming a person cannot change what\n * an unrelated business sees.\n */\n const contacts: ContactRecord[] = useMemo(\n () =>\n contactDocs.map((row) =>\n contactRecordFromDoc(\n row,\n consentGroup ?? contactPrimaryGroup(row, org as Record<string, unknown>),\n ),\n ),\n [contactDocs, consentGroup, org],\n )\n /*\n * The roster, for the Owner column — read only once a loaded row actually\n * carries an owner. A list of people nobody has assigned never pays for\n * the team list, and the create drawer asks for it in its own right.\n */\n const [createOpen, setCreateOpen] = useState(false)\n /*\n * The filter bar's pickers name people and companies, so the roster is\n * also read once the reader has reached for a filter, or is looking at a\n * view that names an owner or a company (AGL-2617).\n */\n const [filterOpened, setFilterOpened] = useState(false)\n const namesFilter = views.state.filters.some(\n (clause) =>\n clause.field === CRM_CONTACT_VIEW_FIELDS.owner ||\n clause.field === CRM_CONTACT_VIEW_FIELDS.company,\n )\n const members = useOrgMembers(orgId, {\n enabled:\n createOpen ||\n filterOpened ||\n namesFilter ||\n contacts.some((contact) => Boolean(contact.ownerUid)),\n })\n /**\n * The HEAD-COUNT, read as a server-side aggregate (AGL-1706).\n *\n * The list above reads a page at a time (AGL-3321) — nobody needs 40,000\n * rows streamed into a table. What it must not do is answer \"how many\n * contacts does this org have\", and it did when it was a `limit(1000)`\n * window: `contacts.length` saturated at 1,000, which is *exactly* the smallest paid included band\n * (`starter`). So `overageContacts = max(0, used − included)` was 0 on\n * every stock plan and the alert below could not render at all.\n *\n * Worse than the dead alert, the same capped number fed the readout in the\n * toolbar. An org with 40,000 contacts on Pro read \"1,000 contacts ·\n * 10,000 included\" — a page whose job is telling a customer where they sit\n * in their band, telling them they have room they do not have. The console\n * billing page read the truth from `getCountFromServer` on this very\n * collection, so the two surfaces disagreed about the same org's audience.\n *\n * THE LIST AND THE COUNT ARE DIFFERENT QUESTIONS and now have different\n * answers: one aggregate read per mount, the same call\n * `billing-usage.component.tsx` already makes against the same path.\n *\n * The counting RULE is untouched — `checkCrmRecordsQuota` is an\n * entitlement input and the usage cron is what bills from it. Only this\n * page's input stopped being a saturated one.\n *\n * THE BAND IS THE RECORDS BAND (AGL-2611): contacts, companies and deals\n * together, so the alert below is fed the sum of three aggregates while\n * the readout beside the toolbar keeps the people count — a workspace\n * whose hundred records are ninety companies is told it holds ten\n * contacts and that its band is full, which are both true. The three\n * reads and the fallback rule live in `useCrmRecordsQuota`, shared with\n * the company and deal drawers so every door on this hub refuses on the\n * same number.\n */\n const records = useCrmRecordsQuota(dataScope, org, {\n contactsFallback: contacts.length,\n })\n // Pending or denied, the listener length stands in. It can only UNDERSTATE\n // (it is the same collection, capped), never overstate, so no alert this\n // number gates can fire on a count larger than the truth.\n const contactCount = records.contactsCount ?? contacts.length\n // Records bands (AGL-890): a plan with an overage rate meters past the\n // included count instead of blocking; a plan without one hard-bands\n // (quota.allowed = false).\n const quota = records.quota\n // Signups whose CRM record was dropped at a hard band (AGL-891), Free's\n // included: capture still fills that band on a plan without the CRM.\n // Written by upsert-contact, host-scoped.\n // Host-scoped, so absent at the organization level (AGL-2630): the\n // counter is a fact about one site's capture, and summing thirty of them\n // would be a figure about nothing in particular.\n const { data: droppedCounter } = useFirestoreDoc<any>(\n () => (hostId ? doc(firestore, 'hosts', hostId, 'counters', 'contactsDropped') : null),\n [firestore, hostId],\n )\n const droppedTotal = Number(droppedCounter?.['total'] ?? 0)\n\n /*==========================================\n * REFUNDS THAT REACHED NO CONTACT (AGL-2329).\n *\n * `recordContactRefund` refuses to create a contact for a refund — a\n * contact holding a refund and no purchase is a phantom record with\n * negative lifetime value — and increments\n * `hosts/{hostId}/counters/contactRefundsUnmatched` instead. Its own\n * comment says the shape mirrors `contactsDropped` \"so an operator…\", and\n * there the sentence stops: `contactsDropped` had this reader and the\n * refund counter had none, so a refund that reached no contact record\n * incremented a number nobody could see.\n *\n * It sits beside the dropped-signup alert because they are the same kind\n * of fact — something that happened to this host's CRM and left no row —\n * and an operator reconciling their contact list needs both or neither.\n *=========================================*/\n const { data: unmatchedRefundCounter } = useFirestoreDoc<any>(\n () =>\n hostId\n ? doc(firestore, 'hosts', hostId, 'counters', 'contactRefundsUnmatched')\n : null,\n [firestore, hostId],\n )\n const unmatchedRefundTotal = Number(unmatchedRefundCounter?.['total'] ?? 0)\n const unmatchedRefundReason = String(\n unmatchedRefundCounter?.['lastReason'] ?? '',\n )\n\n // Saved segments (AGL-199): reusable audience filters.\n const { data: segmentDocs } = useFirestoreCollection<any>(\n () =>\n dataScope\n ? query(\n collection(\n firestore,\n dataScope[0],\n dataScope[1],\n 'contactSegments',\n ),\n // The first fifty by name — the menu's own order — rather than\n // whichever fifty the ids happen to sort first.\n orderBy('name'),\n limit(50),\n )\n : null,\n [firestore, dataScope],\n { idField: '$id' },\n )\n const segments = useMemo(\n () =>\n [...(segmentDocs ?? [])].sort((a, b) =>\n String(a.name ?? '').localeCompare(String(b.name ?? '')),\n ),\n [segmentDocs],\n )\n /*\n * Every saved segment, offered in the views menu as a view (AGL-2617):\n * choosing one puts its tag and source clauses on the filter bar. A\n * segment can only have come FROM an org scope, so the delete is\n * unreachable without one in practice — but the scope is nullable\n * (AGL-1050) and an unguarded deref would be a crash rather than a no-op\n * if that ever stopped being true.\n */\n const segmentPresets = useMemo<CrmViewPreset[]>(\n () =>\n segments.map((segment: any) => ({\n id: String(segment.$id),\n label: String(segment.name ?? ''),\n filters: crmViewFiltersFromSegment({\n tags: segment.tags ?? [],\n sources: segment.sources ?? [],\n }),\n onRemove: dataScope\n ? () =>\n deleteDoc(\n doc(\n firestore,\n dataScope[0],\n dataScope[1],\n 'contactSegments',\n String(segment.$id),\n ),\n )\n : undefined,\n })),\n [segments, dataScope, firestore],\n )\n\n const uid = user?.uid ?? ''\n /*\n * The choices the filter bar picks from. The companies are read only once\n * the reader has reached for a filter or is looking at a view that names\n * one — a read a list nobody is narrowing should not pay for, the same\n * bargain the roster gets above. A stage and a source are fixed lists and\n * cost nothing; a custom `select` field offers its own options.\n */\n const companies = useCompanyOptions({\n hostId,\n org,\n enabled: filterOpened || namesFilter,\n })\n const filterOptions = useMemo<Record<string, readonly ListFilterOption[]>>(\n () => ({\n [CRM_CONTACT_VIEW_FIELDS.owner]: [\n ...(uid ? [{ value: uid, label: 'Me' }] : []),\n ...members.options\n .filter((option) => option.uid !== uid)\n .map((option) => ({ value: option.uid, label: Aglyn.crmMemberPickerLabel(option) })),\n ],\n [CRM_CONTACT_VIEW_FIELDS.stage]: CONTACT_LIFECYCLE_STAGES.map((stage) => ({\n value: stage,\n label: CONTACT_LIFECYCLE_STAGE_LABELS[stage],\n })),\n [CRM_CONTACT_VIEW_FIELDS.source]: Object.entries(SOURCE_LABELS).map(\n ([value, label]) => ({ value, label }),\n ),\n [CRM_CONTACT_VIEW_FIELDS.company]: companies.options.map((company) => ({\n value: company.id,\n label: company.name,\n })),\n // Every value the org keeps, inactive ones marked, as the Leads list offers them.\n [CONTACT_LEAD_SOURCE_COLUMN]: leadSourceList.picklist.values.map((value) => ({\n value: value.label,\n label: value.active ? value.label : `${value.label} (inactive)`,\n })),\n // The verdict on the address (AGL-3245): \"Email is bounced\".\n emailState: Aglyn.EMAIL_STATE_STATUSES.map((status) => ({\n value: status,\n label: Aglyn.EMAIL_STATE_LABELS[status],\n })),\n ...Object.fromEntries(\n customFields.active\n .filter((definition) => definition.type === 'select' && definition.options?.length)\n .map((definition) => [\n crmContactCustomColumn(definition.key),\n (definition.options ?? []).map((option) => ({ value: option, label: option })),\n ]),\n ),\n }),\n [uid, members.options, companies.options, customFields.active, leadSourceList.picklist],\n )\n\n // What the query could not hold, named by the clause the reader set.\n const refused = useMemo(\n () =>\n crmQueryRefusals(paged.plan, asked, {\n fields: filterFields,\n headers: filterHeaders,\n options: filterOptions,\n }),\n [paged.plan, asked, filterFields, filterHeaders, filterOptions],\n )\n\n /* One row grammar, the console's (AGL-2501) — the same table everywhere. */\n // The org's sharing rules, to name the rule behind a shared row (AGL-3336).\n const sharingRules = useCrmSharingRules(org)\n const contactColumns = useMemo(\n () => [\n ...contactListColumns({\n memberName: members.memberName,\n // \"Known by\", at the organization level only (AGL-2630).\n siteName: mount ? mount.siteName : undefined,\n shareChipLabel: (chip) => crmShareChipLabel(chip, sharingRules),\n }),\n // The org's custom fields as optional columns (AGL-2601).\n ...customFieldColumns(customFields.active),\n ],\n [members.memberName, mount, customFields.active, sharingRules],\n )\n /*\n * The grid's column and sort models are the VIEW'S (AGL-2617). The\n * filter-only hidden columns — the declared fields that are not table\n * columns, never the custom ones, which are shown — stay hidden whatever\n * a view says, so a saved arrangement cannot unhide a strip of blank\n * cells.\n */\n /*\n * The grid's own Filters panel and quick search edit the view's clauses\n * (AGL-3313). The picked fields — owner, stage, source, company, email\n * verdict, a custom select — are select columns over the same choices the\n * chips name them by; the rest keep the operators the grammar declares.\n */\n const filterColumns = useMemo(\n () =>\n listFilterGridColumns(contactColumns, filterFields, filterOptions, filterHeaders),\n [contactColumns, filterFields, filterOptions, filterHeaders],\n )\n const gridFilter = useListGridFilter({\n clauses: views.state.filters,\n onChange: views.setFilters,\n selectFields: Object.keys(filterOptions),\n search: { words: searchWords, onChange: setSearchWords },\n })\n const grid = useCrmViewGrid(views, filterColumns, HIDDEN_COLUMNS)\n\n /*\n * A SEGMENT IS A VIEW'S TAG AND SOURCE CLAUSES, kept where a campaign\n * can reach them (AGL-2617).\n *\n * The segment stays: it is what a campaign audience and a dynamic list\n * resolve, and `contactMatchesSegment` is the one predicate both the\n * console and the sender read. What changed is where it comes from — the\n * filter bar's tag and source clauses, read back through\n * `crmViewSegmentFilters`, so \"Save as segment\" is offered exactly when\n * the working filters hold something a segment can hold. Each saved\n * segment is offered in the views menu as a preset, and applying one\n * puts its clauses on the bar, where they can be narrowed further and\n * saved as a view.\n */\n const segmentFilters = useMemo(\n () => crmViewSegmentFilters(views.state.filters),\n [views.state.filters],\n )\n const [segmentName, setSegmentName] = useState<string | null>(null)\n const handleSaveSegment = useCallback(async () => {\n const name = (segmentName ?? '').trim().slice(0, 60)\n // No org, no place to put it (AGL-1050). The button is disabled in\n // this state; the guard is here so the callback cannot outlive it.\n if (!name || !segmentFilters || !dataScope) return\n setSegmentName(null)\n try {\n await addDoc(\n collection(firestore, dataScope[0], dataScope[1], 'contactSegments'),\n {\n name,\n tags: segmentFilters.tags ?? [],\n sources: segmentFilters.sources ?? [],\n // `contactSegments` is in SCOPED_COLLECTIONS and was the one\n // member of it with no `array-contains-any` reader and no rules\n // `hasAny` — the rules gate it on `isOrgWideMember()` and\n // `campaign-send` checks it with `visibleToHost`, which passes on\n // a missing field. So nothing broke, which is precisely why this\n // went unnoticed (AGL-1478): a collection listed as scoped that\n // nothing enforces is a trap set for whoever wires up the first\n // scoped read, and they would inherit every segment ever saved\n // already broken. Stamped at creation instead, like its siblings.\n //\n // Org-wide, unconditionally: `useOrgDataScope` resolves to\n // `['orgs', orgId]` or to null, AGL-1061 having removed the\n // `hosts/{hostId}` branch after counting zero documents there.\n // A segment is a saved filter over org contacts, so it is exactly\n // as visible as they are.\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n createdAt: new Date(),\n },\n )\n enqueueSnackbar(\n `Segment \"${name}\" saved — usable as a campaign audience`,\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [segmentName, segmentFilters, firestore, dataScope, enqueueSnackbar])\n\n /** The rows ticked for a bulk action (AGL-2603); the bar above the table acts on them. */\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n // A filter, a search or a page is a different set of rows; a selection\n // made on the last one would be a count over rows no longer on screen.\n useEffect(() => setSelectedIds([]), [paged.plan, paged.page])\n /*==========================================\n * ADDING ONE PERSON BY HAND (AGL-2596).\n *\n * Through the plugin's server route rather than a client write, because\n * creating a contact is the one act on this surface the rules cannot fully\n * judge: the dedupe against every holder's rows is a lookup the capturing\n * site may not read, and the audience band is a count the browser cannot\n * take. The route answers `{ contactId, created }`; a merge onto somebody\n * already in the address book is not an error, and the page says which\n * happened before it opens the record.\n *=========================================*/\n // The site the route resolves the org from and stamps the record with: the\n // mounted site, or at the organization level the one the drawer's picker\n // named (AGL-2630) — the drawer holds its submit until it is known.\n const crmApi = useCrmApi(crmScope.createHostId)\n const [createBusy, setCreateBusy] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const handleCreate = useCallback(\n async (values: NewContactValues) => {\n setCreateBusy(true)\n setCreateError(null)\n try {\n const { response, payload } = await crmApi('contacts-create', {\n email: values.email,\n ...(values.name ? { name: values.name } : {}),\n // Salesforce's standard fields (AGL-3515), each only when given.\n ...(values.salutation ? { salutation: values.salutation } : {}),\n ...(values.firstName ? { firstName: values.firstName } : {}),\n ...(values.lastName ? { lastName: values.lastName } : {}),\n ...(values.mobilePhone ? { mobilePhone: values.mobilePhone } : {}),\n ...(values.department ? { department: values.department } : {}),\n ...(values.doNotCall ? { doNotCall: true } : {}),\n ...(values.phone ? { phone: values.phone } : {}),\n ...(values.jobTitle ? { jobTitle: values.jobTitle } : {}),\n ...(values.companyName ? { companyName: values.companyName } : {}),\n ...(values.companyId ? { companyId: values.companyId } : {}),\n ...(values.address ? { address: values.address } : {}),\n ...(values.ownerUid ? { ownerUid: values.ownerUid } : {}),\n ...(values.lifecycleStage\n ? { lifecycleStage: values.lifecycleStage }\n : {}),\n ...(values.tags.length ? { tags: values.tags } : {}),\n marketingConsent: values.marketingConsent,\n ...(values.marketingConsent && values.disclosedConsentGroup\n ? { disclosedConsentGroup: values.disclosedConsentGroup }\n : {}),\n })\n if (!response.ok) {\n // The route's own sentence, shown above the form unchanged: the\n // band refusal is the list's wording, and a field refusal names\n // the field.\n setCreateError(\n String(payload['error'] ?? 'The contact could not be added.'),\n )\n return\n }\n const contactId = String(payload['contactId'] ?? '')\n // The activity entry is the route's (AGL-2622): it verified the\n // caller and performed the write, so it is the one writer that\n // cannot record an add that did not happen — and a second entry\n // from here would put two rows on one act.\n enqueueSnackbar(\n payload['created']\n ? 'Contact added'\n : 'Already a contact — what you entered was merged into their record',\n { variant: 'success', persist: false },\n )\n setCreateOpen(false)\n if (contactId) router.push(routes.contact(contactId))\n } catch (error) {\n console.error(error)\n setCreateError('The contact could not be added.')\n } finally {\n setCreateBusy(false)\n }\n },\n [crmApi, enqueueSnackbar, router, routes],\n )\n\n // The list's filter, when one narrows it, for the export to read the same\n // records the list does (AGL-3527).\n const exportFilter = useMemo(\n () =>\n paged.plan.served.length || paged.plan.searched\n ? { label: 'what the list shows', plan: paged.plan }\n : null,\n [paged.plan],\n )\n\n return (\n <>\n {/*\n The list card ends at its pager; the team's recent activity is a\n card of its own under it, drawn only once there is some.\n */}\n <Stack spacing={3}>\n <CardDisplay\n header={'Contacts'}\n help={pluginDocsHelp('contacts', { anchor: '#the-contacts-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n // The record actions, top right and never clipped (AGL-3311).\n action: (\n <CrmListActions>\n {suiteIncluded ? (\n <CrmImportButton resource={CRM_CONTACTS_RESOURCE} noun=\"contacts\" hostId={hostId} mappingZone=\"contacts\" />\n ) : (\n <CrmSuiteLockedButton>{'Import'}</CrmSuiteLockedButton>\n )}\n <CrmExportButton resource={CRM_CONTACTS_RESOURCE} hostId={hostId} filter={exportFilter} />\n {/* A button that opens a drawer — never a create form above the\n list. Disabled until the org has resolved, because the route\n resolves the org from the site and a click before that has\n nowhere to write. */}\n {suiteIncluded ? (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={!dataScope}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {'New contact'}\n </Button>\n ) : (\n <CrmSuiteLockedButton variant=\"contained\" color=\"primary\">\n {'New contact'}\n </CrmSuiteLockedButton>\n )}\n </CrmListActions>\n ),\n }}\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {/* The org's audience, not the page's row count (AGL-1706) —\n two different numbers since the list stopped reading the\n whole collection. */}\n {`${contactCount.toLocaleString()} contacts · ${\n Number.isFinite(quota.included)\n ? `${quota.used.toLocaleString()} of ${quota.included.toLocaleString()} CRM records`\n : `${quota.used.toLocaleString()} CRM records`\n }`}\n </Typography>\n {suiteIncluded ? null : (\n <CrmSuiteNotice>\n {'Contacts arrive here on their own from your forms, sign-ups, ' +\n 'orders and bookings. Adding one by hand, importing a CSV, ' +\n \"saved views, and a contact's owner, stage and company are \" +\n 'part of the CRM.'}\n </CrmSuiteNotice>\n )}\n {/*\n The view this list is showing, and the clauses narrowing it\n (AGL-2617). The control names the view and holds everything a\n reader does to one; the bar holds the clauses, the seed's\n included, every one of them on the query. Segments are\n offered in the control's menu and saved from it.\n */}\n <Stack spacing={1}>\n <CrmViewsControl\n controller={views}\n allLabel=\"All contacts\"\n suiteLocked={!suiteIncluded}\n presets={segmentPresets}\n onSaveAsSegment={\n segmentFilters && dataScope ? () => setSegmentName('') : null\n }\n />\n <ListFilterChips\n fields={filterFields}\n headers={filterHeaders}\n clauses={views.state.filters}\n onChange={views.setFilters}\n options={filterOptions}\n marksServed={false}\n />\n </Stack>\n {!quota.allowed ? (\n <Alert severity=\"warning\">\n {'CRM records limit reached — new visitors are no longer ' +\n 'captured' +\n (droppedTotal > 0\n ? ` (${droppedTotal.toLocaleString()} missed so far)`\n : '') +\n '. Upgrade in Billing to keep collecting.'}\n </Alert>\n ) : quota.overageRecords > 0 &&\n quota.overageRateUsd != null &&\n // No claim about money until the verdict that decides it has\n // settled (AGL-1662). Before Remote Config activation the flag\n // reads its registry default, which is no org's verdict, so an\n // ungated alert could assert the wrong wording for one paint.\n releaseFlagsReady ? (\n <Alert severity=\"info\">\n {contactsBilled\n ? // Same sentence as the billing page's caption, and the same\n // basis (AGL-2399): the count here is LIVE, the invoice\n // charges the last reading before the month closes, so the\n // dollar figure is a projection until the month ends. Staff\n // and customer must not read different sentences about the\n // same org's money — that applies to WHEN it is measured as\n // much as to how much.\n `${quota.overageRecords.toLocaleString()} CRM records over ` +\n `your plan's included ${quota.included.toLocaleString()} — ` +\n `metered at $${quota.overageRateUsd}/1,000 per month ` +\n `(≈$${quota.overageMonthlyUsd.toFixed(2)} if your list ends ` +\n 'the month at this size). ' +\n 'Upgrade in Billing for a larger included audience.'\n : // The wording `db5ecdf2b` put on the billing page, which is\n // itself the wording `1a2aed5cb` published to\n // `billing-and-plans/overview.md` (AGL-1601/1603). Staff and\n // customer must not read different sentences about the same\n // org's money.\n //\n // THE COUNT STAYS, THE TOTAL GOES: the head-count is real —\n // ingestion captured those records — and is not a claim\n // about money. The upgrade nudge goes with the total, since\n // it prompts a purchase premised on a charge that is not\n // happening.\n `${quota.overageRecords.toLocaleString()} CRM records over ` +\n `your plan's included ${quota.included.toLocaleString()} — ` +\n 'not billed while the CRM is unavailable. ' +\n `The $${quota.overageRateUsd}/1,000 rate applies once ` +\n 'Contacts opens.'}\n </Alert>\n ) : droppedTotal > 0 ? (\n <Alert severity=\"info\">\n {`${droppedTotal.toLocaleString()} earlier visitor${\n droppedTotal === 1 ? ' was' : 's were'\n } not captured while your CRM records band was full.`}\n </Alert>\n ) : null}\n {/*\n Unlike the band alert above, this one is NOT exclusive with the\n others: a host can be over its band AND have refunds that landed\n nowhere, and the two have different remedies. Chaining it into the\n same ternary would hide whichever fact came second.\n */}\n {unmatchedRefundTotal > 0 ? (\n <Alert severity=\"warning\">\n {`${unmatchedRefundTotal.toLocaleString()} refund${\n unmatchedRefundTotal === 1 ? '' : 's'\n } could not be recorded against a contact${\n UNMATCHED_REFUND_REASON[unmatchedRefundReason]\n ? ` — most recently because ${UNMATCHED_REFUND_REASON[unmatchedRefundReason]}`\n : ''\n }. The money moved; the customer's timeline does not show it.`}\n </Alert>\n ) : null}\n <ListQueryNotices refused={refused} notices={paged.plan.notices} />\n {contacts.length === 0 &&\n !views.state.filters.length &&\n !searchWords.some((word) => word.trim()) ? (\n <EmptyStateComponent\n label={contactsStatus === 'loading' ? 'Loading contacts…' : 'No contacts yet'}\n description={\n contactsStatus === 'loading'\n ? undefined\n : suiteIncluded\n ? 'Form submissions, member sign-ups, orders and bookings become contacts on their own; add one by hand or bring a list in from a CSV.'\n : 'Form submissions, member sign-ups, orders and bookings become contacts on their own.'\n }\n action={\n contactsStatus === 'loading' ? undefined : !suiteIncluded ? (\n <Stack direction=\"row\" spacing={1}>\n <CrmSuiteLockedButton variant=\"contained\" color=\"primary\">\n {'New contact'}\n </CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Import'}</CrmSuiteLockedButton>\n </Stack>\n ) : (\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={!dataScope}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {'New contact'}\n </Button>\n <CrmImportButton resource={CRM_CONTACTS_RESOURCE} noun=\"contacts\" hostId={hostId} mappingZone=\"contacts\" />\n </Stack>\n )\n }\n />\n ) : (\n <>\n <ContactsBulkBar hostId={hostId} org={org} scope={dataScope} consentGroup={consentGroup} rows={contacts} selected={selectedIds} onSelectedChange={setSelectedIds} suiteLocked={!suiteIncluded} />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={contacts}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n onOpen={(id) => router.push(routes.contact(id))}\n /*\n * The grid's Filters panel and quick search edit the view's\n * clauses; the list's query answers them (AGL-3321), so the\n * grid must not filter again itself. Opening the panel is\n * what reads the roster and the companies its pickers name.\n * Columns are the view's, controlled so a saved arrangement\n * is what the grid shows; the query orders the rows —\n * newest change first — so the grid sorts nothing itself.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n onPreferencePanelOpen={() => setFilterOpened(true)}\n // A filter that matched nothing is said inside the grid,\n // so the toolbar that set it stays to change it.\n noRowsLabel={\n contactsStatus === 'loading' ? 'Loading…' : 'No contacts match these filters'\n }\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={grid.onColumnVisibilityModelChange}\n sortingMode=\"server\"\n disableColumnSorting\n loading={contactsStatus === 'loading'}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n </CrmColumnOrderProvider>\n <ListPagination\n page={paged.page}\n pageSize={paged.pageSize}\n rowCount={contacts.length}\n hasMore={paged.hasMore}\n onPageChange={paged.setPage}\n onPageSizeChange={paged.setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n <RecentActivityFeed hostId={hostId} org={org} basePath={props.basePath} />\n </Stack>\n {/*\n Naming a segment is a dialog, never a form above the list. Open\n while the name is a string; `null` is closed.\n */}\n <Dialog\n open={segmentName !== null}\n onClose={() => setSegmentName(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{'Save as segment'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The tag and source filters, kept as a segment an email campaign or a dynamic list can send to. The other filters stay on this view only.'}\n </Typography>\n <TextField\n autoFocus\n size=\"small\"\n label=\"Segment name\"\n value={segmentName ?? ''}\n onChange={(event) => setSegmentName(event.target.value.slice(0, 60))}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n void handleSaveSegment()\n }\n }}\n slotProps={{ htmlInput: { maxLength: 60 } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setSegmentName(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n disabled={!(segmentName ?? '').trim() || !dataScope}\n onClick={() => void handleSaveSegment()}\n >\n {'Save segment'}\n </Button>\n </DialogActions>\n </Dialog>\n {/*\n Mounted only while open, so a page that never reaches for it never\n renders a drawer's chrome, and every opening starts from a blank form.\n */}\n {createOpen ? (\n <NewContactDrawer\n open\n onClose={() => setCreateOpen(false)}\n hostId={hostId}\n org={org}\n busy={createBusy}\n error={createError}\n owners={members.options}\n ownersReady={members.ready}\n onSubmit={(values) => void handleCreate(values)}\n />\n ) : null}\n </>\n )\n}\nContactsPeopleSection.displayName = 'ContactsPeopleSection'\n\nexport default ContactsPeopleSection\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","CONTACT_SOURCE_LABELS","CRM_CONTACT_VIEW_FIELDS","crmContactCustomColumn","crmViewFiltersFromSegment","crmViewSegmentFilters","newResourceScopeFields","ORG_SCOPE_TOKEN","pluginDocsHelp","hiddenFilterVisibility","ListTable","ListPagination","ListQueryNotices","CONTACT_LEAD_SOURCE_COLUMN","CONTACT_LIST_DECLARATION","CONTACT_LIST_FILTER_FIELDS","CONTACT_LIST_FILTER_HEADERS","CONTACT_PREFIX_SEARCH","contactClauseImpliesScope","contactSoloClause","contactCustomFilterFields","contactCustomFilterHeaders","contactQueryClause","crmAskClauses","crmQueryRefusals","useCrmFoldsScope","useCrmListQuery","contactPrimaryGroup","contactRecordFromDoc","useCrmOrgMount","useCrmSharingRules","crmShareChipLabel","useCrmScope","contactsListSeed","crmRoutes","CRM_CONTACTS_RESOURCE","useCrmRecordsQuota","CardDisplay","EmptyStateComponent","ContactsBulkBar","CrmListActions","CrmExportButton","CrmImportButton","useSnackbar","useFirestore","useFirestoreCollection","useFirestoreDoc","useUser","Alert","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","addDoc","collection","deleteDoc","doc","limit","orderBy","query","useRouter","useSearchParams","useCallback","useEffect","useMemo","useRef","useState","RecentActivityFeed","CONTACT_FILTER_COLUMNS","CONTACT_OPTIONAL_COLUMNS","contactListColumns","NewContactDrawer","useCrmApi","useOrgMembers","useContactFieldDefinitions","useLeadSourcePicklist","customFieldColumns","useCrmSavedView","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","useCompanyOptions","ListFilterChips","listFilterGridColumns","useListGridFilter","CrmViewsControl","CrmSuiteLockedButton","CrmSuiteNotice","crmSuiteIncluded","SOURCE_LABELS","HIDDEN_COLUMNS","Object","fromEntries","map","field","UNMATCHED_REFUND_REASON","ContactsPeopleSection","props","records","hostId","org","releaseFlag","basePath","routes","router","contactsBilled","released","releaseFlagsReady","ready","crmScope","scope","dataScope","orgId","consentGroup","visibleTo","visibleToTokens","suiteIncluded","mount","customFields","leadSourceList","firestore","enqueueSnackbar","data","user","searchParams","seed","seedFilters","filter","source","formId","op","value","viewInitial","filters","views","section","initial","filterFields","active","filterHeaders","foldsScope","groupId","asked","state","clause","impliesScope","searchWords","setSearchWords","paged","declaration","clauses","search","soloClause","prefixSearch","contactDocs","rows","contactsStatus","status","openedByEmail","current","openEmail","served","plan","some","only","length","hasMore","replace","contact","String","$id","contacts","row","createOpen","setCreateOpen","filterOpened","setFilterOpened","namesFilter","owner","company","members","enabled","Boolean","ownerUid","contactsFallback","contactCount","contactsCount","quota","droppedCounter","droppedTotal","Number","unmatchedRefundCounter","unmatchedRefundTotal","unmatchedRefundReason","segmentDocs","idField","segments","sort","a","b","name","localeCompare","segmentPresets","segment","id","label","tags","sources","onRemove","undefined","uid","companies","filterOptions","options","option","crmMemberPickerLabel","stage","entries","picklist","values","emailState","EMAIL_STATE_STATUSES","EMAIL_STATE_LABELS","definition","type","key","refused","fields","headers","sharingRules","contactColumns","memberName","siteName","shareChipLabel","chip","filterColumns","gridFilter","onChange","setFilters","selectFields","keys","words","grid","segmentFilters","segmentName","setSegmentName","handleSaveSegment","trim","slice","createdAt","Date","variant","persist","error","console","allowDuplicate","selectedIds","setSelectedIds","page","crmApi","createHostId","createBusy","setCreateBusy","createError","setCreateError","handleCreate","payload","response","email","salutation","firstName","lastName","mobilePhone","department","doNotCall","phone","jobTitle","companyName","companyId","address","lifecycleStage","marketingConsent","disclosedConsentGroup","ok","contactId","push","exportFilter","searched","spacing","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","resource","noun","mappingZone","size","color","disabled","onClick","toLocaleString","isFinite","included","used","controller","allLabel","suiteLocked","presets","onSaveAsSegment","marksServed","allowed","severity","overageRecords","overageRateUsd","overageMonthlyUsd","toFixed","notices","word","description","direction","selected","onSelectedChange","columnOrder","columns","slots","selectable","onOpen","filterMode","filterModel","onFilterModelChange","quickFilter","onPreferencePanelOpen","noRowsLabel","columnVisibilityModel","onColumnVisibilityModelChange","sortingMode","disableColumnSorting","loading","hideFooter","pageSize","rowCount","onPageChange","setPage","onPageSizeChange","setPageSize","open","onClose","maxWidth","fullWidth","sx","pt","autoFocus","event","target","onKeyDown","preventDefault","slotProps","htmlInput","maxLength","busy","owners","ownersReady","onSubmit","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contacts-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 {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n CONTACT_SOURCE_LABELS,\n CRM_CONTACT_VIEW_FIELDS,\n type CrmViewFilterClause,\n crmContactCustomColumn,\n crmViewFiltersFromSegment,\n crmViewSegmentFilters,\n newResourceScopeFields,\n ORG_SCOPE_TOKEN,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { type ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport ListQueryNotices from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport {\n CONTACT_LEAD_SOURCE_COLUMN,\n CONTACT_LIST_DECLARATION,\n CONTACT_LIST_FILTER_FIELDS,\n CONTACT_LIST_FILTER_HEADERS,\n CONTACT_LIST_SORTS,\n CONTACT_PREFIX_SEARCH,\n contactClauseImpliesScope,\n contactSoloClause,\n contactCustomFilterFields,\n contactCustomFilterHeaders,\n contactQueryClause,\n} from '../constants/contact-filters'\nimport { crmAskClauses, crmQueryRefusals } from '../model/crm-list-query'\nimport { useCrmFoldsScope, useCrmListQuery } from '../hooks/use-crm-list-query'\nimport { crmViewQuerySort, useCrmColumnSort } from '../hooks/use-crm-column-sort'\nimport {\n type ContactRecord,\n contactPrimaryGroup,\n contactRecordFromDoc,\n} from '../model/contact-record'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCrmSharingRules } from '../hooks/use-crm-sharing'\nimport { crmShareChipLabel } from '../model/crm-sharing'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { contactsListSeed } from '../model/contacts-list-seed'\nimport { crmRoutes } from '../model/crm-routes'\nimport { CRM_CONTACTS_RESOURCE } from '../transfer/fields'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport ContactsBulkBar from './contacts-bulk-bar'\nimport { CrmListActions } from './crm-list-toolbar'\nimport { CrmExportButton, CrmImportButton } from './crm-transfer-buttons'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteDoc,\n doc,\n limit,\n orderBy,\n query,\n} from 'firebase/firestore'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport RecentActivityFeed from './recent-activity-feed'\nimport {\n CONTACT_FILTER_COLUMNS,\n CONTACT_OPTIONAL_COLUMNS,\n contactListColumns,\n} from './contact-list-columns'\nimport NewContactDrawer, { type NewContactValues } from './new-contact-drawer'\nimport { useCrmApi } from './use-crm-api'\nimport { useOrgMembers } from './use-org-members'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\nimport { customFieldColumns } from './contact-custom-columns'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport { useCompanyOptions } from './company-picker'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\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 CrmViewsControl, { type CrmViewPreset } from './crm-views-control'\nimport { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from './crm-suite-lock'\n\n/**\n * The shared labels, under the name this file has always called them.\n *\n * The map lives beside the `ContactSource` union so the dynamic-list rule\n * editor and this filter cannot disagree about what `order` is called.\n */\nconst SOURCE_LABELS = CONTACT_SOURCE_LABELS\n\n/**\n * The page sorts a column needs other than its cell's text (AGL-3680): the\n * stage by its place in the lifecycle, not its label's spelling.\n */\nconst CONTACT_PAGE_SORTS = {\n lifecycleStage: (row: ContactRecord) =>\n row.lifecycleStage ? CONTACT_LIFECYCLE_STAGES.indexOf(row.lifecycleStage) : null,\n}\n\n/**\n * What the list keeps out of sight until a view says otherwise: the\n * filter-only columns, which never show, and the optional columns\n * (AGL-2616), which start hidden and come back through the column menu or\n * a saved view's column list.\n */\nconst HIDDEN_COLUMNS: Readonly<Record<string, boolean>> = {\n ...hiddenFilterVisibility(CONTACT_LIST_FILTER_FIELDS, CONTACT_FILTER_COLUMNS),\n ...Object.fromEntries(CONTACT_OPTIONAL_COLUMNS.map((field) => [field, false])),\n}\n\n/**\n * Why a refund found no contact to record itself against (AGL-2329).\n *\n * The three reasons `recordContactRefund` distinguishes, in the operator's\n * language rather than the writer's enum. They are not interchangeable: two\n * of them are things the merchant can act on and one is a deletion nothing\n * should undo, and collapsing them into \"unmatched\" would turn an actionable\n * fact into a statistic.\n */\nconst UNMATCHED_REFUND_REASON: Record<string, string> = {\n 'no-email': 'the order carried no email address',\n 'no-contact': 'no contact record matched the buyer',\n 'contact-deleted': 'the contact was deleted between the sale and the refund',\n}\n\n/**\n * Contacts CRM (AGL-198): the unified people list fed by AGL-197's\n * ingestion — search, source badges, a profile drawer with the\n * interaction timeline plus tags/notes editing, and CSV export. Part of\n * the CRM, included from Starter (AGL-2851); the contactsPerHost band\n * counts the records in it.\n */\n/**\n * Contacts CRM (AGL-109 → AGL-395): the unified contacts list, segments,\n * and profile drawer, owned by the contacts plugin and rendered by the\n * shell's generic plugin route. The shell applies the `release_crm`\n * gate (via the nav tab) and passes the resolved `org` doc for the\n * `contactsPerHost` quota check.\n *\n * Since AGL-2595 this is the `people` SECTION of the Contacts hub rather\n * than the whole page: `crm-console-page.tsx` routes to it, and the\n * companies, deals, tasks, reports and fields sections sit beside it. The\n * body is the v1 page unchanged, and it takes the shell's full prop bag so\n * nothing the v1 page read has to be threaded through the hub by name.\n */\nexport function ContactsPeopleSection(props: ConsolePluginPageProps) {\n const { hostId, org, releaseFlag, basePath } = props\n /*\n * A row is a LINK now (AGL-2596): opening a person navigates to their own\n * page rather than a drawer over the list, so the address can be pasted\n * and every other CRM record can point at it. `basePath` is what the hub\n * hands every section; the empty fallback only exists for a direct mount\n * without the shell, where there is nowhere to navigate to anyway.\n */\n const routes = crmRoutes(basePath ?? '')\n const router = useRouter()\n // Whether the audience overage on this page is actually INVOICED\n // (AGL-1662), and whether that question has been answered yet.\n //\n // AGL-1604 stopped the usage cron putting `contactsOverageUsd` into\n // `billedCents` while `release_crm` is off for the org; `db5ecdf2b`\n // taught the console billing page's caption the same thing. This page's own\n // alert still quoted the dollar figure with no flag check — and this is the\n // surface a staff member reaches with the flag OFF, because the shell's\n // `FeatureGate` admits them on `visible` (`released || isStaff`). Support\n // then reads that number back to a customer whose invoice will not carry it.\n //\n // The shell resolves this from `released`, never `visible`: staff opening a\n // page does not put a line on the customer's bill.\n //\n // Both default to the WITHHELD answer when the prop is absent, which only\n // happens in a direct mount — the shell always supplies it for a surface\n // with a nav-tab flag. A caller that has not resolved the verdict has not\n // earned the right to quote a charge.\n const contactsBilled = releaseFlag?.released ?? false\n const releaseFlagsReady = releaseFlag?.ready ?? false\n // Org-shared data root (AGL-237). Null until the org lookup settles\n // (AGL-1061), and for a host with no owning org — the pre-migration host\n // path this used to fall back to is gone (AGL-1050), so the CRM lists\n // nothing rather than listing somewhere else. Resolved by the one scope\n // hook (AGL-2614), which also answers for the organization-level mount\n // (AGL-2630): there `hostId` is null, the org comes from the hub's mount,\n // and the viewing group and the scope clause are both `null`.\n const crmScope = useCrmScope({ hostId, org })\n const { scope: dataScope, orgId, consentGroup, visibleTo: visibleToTokens } = crmScope\n /*\n * THE CRM (AGL-2788). This list is the CRM's, and the shell mounts no CRM\n * page for a plan without it (AGL-2851). The locks below — adding a person\n * by hand, importing a file, a saved view, the owner, stage and company of\n * a selection — draw only for an org whose plan lacks the CRM. The shell\n * mounts this page only once the org has settled, so the plan read here\n * is an answer.\n */\n const suiteIncluded = crmSuiteIncluded(org)\n // The org's site list, at the organization level only — what names the\n // sites in the \"Known by\" column (AGL-2630).\n const mount = useCrmOrgMount()\n // The org's custom fields, for the optional columns below (AGL-2601).\n const customFields = useContactFieldDefinitions(dataScope?.[1] ?? null)\n // The org's lead source values, the Lead source filter's choices (AGL-3511).\n const leadSourceList = useLeadSourcePicklist(orgId)\n /*==========================================\n * THE CONTROLLER THIS PAGE IS SHOWING.\n *\n * A contact document is shared by every site in the org — one human who\n * touched two sites is one row, which is the dedupe the shared address book\n * exists for and the reason the org is billed once for them. Almost nothing\n * ON that row is shared: the notes, tags, timeline and lifetime value are\n * the holder's own business records, and while they lived at the top of the\n * document every site in an agency's account could read every client's.\n *\n * So the page resolves the group it is being viewed as — the sites declared\n * to be one sender, or this site alone — and reads that group's facet.\n * Pure, from the org document the shell already passed, so it costs no\n * read. `consentGroup` above is that group; at the ORGANIZATION level it\n * is `null`, and each row is flattened through the person's own primary\n * holder instead (`contactPrimaryGroup`), so an org-wide member reads the\n * profile the first capturing site keeps — and the \"Known by\" column says\n * which other sites hold one.\n *=========================================*/\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n\n /*\n * WHAT THE LIST WAS OPENED FOR (AGL-2612).\n *\n * Two surfaces address this list with a question rather than a record: a\n * form's own page (\"who came in through this form\") and an Inbox\n * submission row (\"open the person with this address\"). Both arrive as\n * query keys, parsed once into the same filter the grid's panel would\n * produce, so the seeded query IS a typed query and nothing here is a\n * second predicate.\n */\n const searchParams = useSearchParams()\n const seed = useMemo(() => contactsListSeed(searchParams), [searchParams])\n /*\n * The seed as CLAUSES, in front of everything else (AGL-2617).\n *\n * A view holds a list of clauses in the grammar the grid's panel speaks,\n * and the seed is two of them — the form's captures and the address, plus\n * the source the form link names. They lead every state the list works\n * from, whichever view is open, so the query serves the seed (it reaches\n * the whole collection, as it did before views) and a view narrows what\n * comes back. The controller keeps them out of what a view SAVES.\n */\n const seedFilters = useMemo<CrmViewFilterClause[]>(\n () => [\n ...(seed.filter ? [seed.filter] : []),\n // A form's captures all came in through a form: the source beside it\n // would ask a second array clause of the one query (AGL-3321).\n ...(seed.source && !seed.formId\n ? [{ field: CRM_CONTACT_VIEW_FIELDS.source, op: 'equals', value: seed.source }]\n : []),\n ],\n [seed],\n )\n const viewInitial = useMemo(() => ({ filters: seedFilters }), [seedFilters])\n const views = useCrmSavedView({\n section: 'contacts',\n hostId,\n org,\n basePath: basePath ?? '',\n initial: viewInitial,\n })\n /*\n * The grammar this list speaks: the declared fields, and one field per\n * custom contact field the org has defined — so a view can filter on\n * \"plan is enterprise\" the way it filters on a stage.\n */\n const filterFields = useMemo(\n () => [...CONTACT_LIST_FILTER_FIELDS, ...contactCustomFilterFields(customFields.active)],\n [customFields.active],\n )\n const filterHeaders = useMemo(\n () => ({\n ...CONTACT_LIST_FILTER_HEADERS,\n ...contactCustomFilterHeaders(customFields.active),\n }),\n [customFields.active],\n )\n /*\n * EVERY CLAUSE AND THE SEARCH ON ONE QUERY (AGL-3321).\n *\n * Each stored clause is asked through the field its writers keep\n * (`contactQueryClause`): a holder's owner, stage, source, company, tags,\n * custom value or \"has bought\" as that holder's facet key, the verdict as\n * `emailStatus`, the rest as stored. The query pages the list newest\n * change first and carries all of it; what one query cannot hold is\n * refused by name above the list, and nothing is matched over the rows a\n * page happened to load. Under a site the scope clause is the query's one\n * array clause — the search folds into it, and a facet or form clause\n * stands in its place, for a reader whose access is not limited to\n * particular sites.\n */\n const foldsScope = useCrmFoldsScope(orgId, visibleToTokens)\n const groupId = consentGroup?.groupId ?? null\n const asked = useMemo(\n () =>\n crmAskClauses(views.state.filters, (clause) =>\n contactQueryClause(clause, { groupId, foldsScope }),\n ),\n [views.state.filters, groupId, foldsScope],\n )\n const impliesScope = useMemo(() => contactClauseImpliesScope(groupId), [groupId])\n // The quick search is the grid's box, answered by the same query.\n const [searchWords, setSearchWords] = useState<string[]>([])\n // The header order the view asks (AGL-3680), read before the query it orders.\n const contactSort = useMemo(\n () => crmViewQuerySort(CONTACT_LIST_SORTS, views.state.sort, CONTACT_LIST_SORTS[0]),\n [views.state.sort],\n )\n const paged = useCrmListQuery<any>({\n scope: dataScope,\n collection: 'contacts',\n visibleTo: visibleToTokens,\n foldsScope,\n declaration: CONTACT_LIST_DECLARATION,\n clauses: asked.clauses,\n search: searchWords,\n impliesScope,\n soloClause: contactSoloClause,\n prefixSearch: CONTACT_PREFIX_SEARCH,\n sort: contactSort.sort,\n })\n const contactDocs = paged.rows\n const contactsStatus = paged.status\n /*\n * OPENED FOR ONE ADDRESS: move straight on to the record (AGL-2612).\n *\n * The Inbox links here with an email because a contact's id is minted at\n * capture and nothing outside the CRM holds it; the list is the lookup.\n * Once the seeded `email equals` query has answered with exactly one row,\n * that row is the person and the list is a detour — `replace`, so Back\n * returns to the Inbox rather than to this redirect. Nothing else\n * navigates: no match leaves the filtered list on screen, which is the\n * honest answer for a submission whose contact the band dropped, and a\n * filter the reader has since changed is theirs.\n */\n const openedByEmail = useRef(false)\n useEffect(() => {\n if (openedByEmail.current || !seed.openEmail) return\n if (contactsStatus !== 'success') return\n const served = paged.plan.served.some(\n (clause) => clause.field === 'email' && clause.op === 'equals' && clause.value === seed.openEmail,\n )\n if (!served) return\n const only = contactDocs.length === 1 && !paged.hasMore ? contactDocs[0] : null\n if (!only) return\n openedByEmail.current = true\n router.replace(routes.contact(String(only.$id)))\n }, [seed.openEmail, contactsStatus, paged.plan, paged.hasMore, contactDocs, router, routes])\n /**\n * Every row, flattened through THIS group's facet.\n *\n * One projection rather than a facet read at each of the nine places that\n * touch `tags`, `notes`, `name` or `interactions`: a surface that reads one\n * of them off the top of the document is a surface showing another\n * holder's records, and nine chances to forget is nine leaks.\n *\n * The canonical `name` is the fallback and the shared identity; a holder's\n * own override wins, so one business renaming a person cannot change what\n * an unrelated business sees.\n */\n const contacts: ContactRecord[] = useMemo(\n () =>\n contactDocs.map((row) =>\n contactRecordFromDoc(\n row,\n consentGroup ?? contactPrimaryGroup(row, org as Record<string, unknown>),\n ),\n ),\n [contactDocs, consentGroup, org],\n )\n /*\n * The roster, for the Owner column — read only once a loaded row actually\n * carries an owner. A list of people nobody has assigned never pays for\n * the team list, and the create drawer asks for it in its own right.\n */\n const [createOpen, setCreateOpen] = useState(false)\n /*\n * The filter bar's pickers name people and companies, so the roster is\n * also read once the reader has reached for a filter, or is looking at a\n * view that names an owner or a company (AGL-2617).\n */\n const [filterOpened, setFilterOpened] = useState(false)\n const namesFilter = views.state.filters.some(\n (clause) =>\n clause.field === CRM_CONTACT_VIEW_FIELDS.owner ||\n clause.field === CRM_CONTACT_VIEW_FIELDS.company,\n )\n const members = useOrgMembers(orgId, {\n enabled:\n createOpen ||\n filterOpened ||\n namesFilter ||\n contacts.some((contact) => Boolean(contact.ownerUid)),\n })\n /**\n * The HEAD-COUNT, read as a server-side aggregate (AGL-1706).\n *\n * The list above reads a page at a time (AGL-3321) — nobody needs 40,000\n * rows streamed into a table. What it must not do is answer \"how many\n * contacts does this org have\", and it did when it was a `limit(1000)`\n * window: `contacts.length` saturated at 1,000, which is *exactly* the smallest paid included band\n * (`starter`). So `overageContacts = max(0, used − included)` was 0 on\n * every stock plan and the alert below could not render at all.\n *\n * Worse than the dead alert, the same capped number fed the readout in the\n * toolbar. An org with 40,000 contacts on Pro read \"1,000 contacts ·\n * 10,000 included\" — a page whose job is telling a customer where they sit\n * in their band, telling them they have room they do not have. The console\n * billing page read the truth from `getCountFromServer` on this very\n * collection, so the two surfaces disagreed about the same org's audience.\n *\n * THE LIST AND THE COUNT ARE DIFFERENT QUESTIONS and now have different\n * answers: one aggregate read per mount, the same call\n * `billing-usage.component.tsx` already makes against the same path.\n *\n * The counting RULE is untouched — `checkCrmRecordsQuota` is an\n * entitlement input and the usage cron is what bills from it. Only this\n * page's input stopped being a saturated one.\n *\n * THE BAND IS THE RECORDS BAND (AGL-2611): contacts, companies and deals\n * together, so the alert below is fed the sum of three aggregates while\n * the readout beside the toolbar keeps the people count — a workspace\n * whose hundred records are ninety companies is told it holds ten\n * contacts and that its band is full, which are both true. The three\n * reads and the fallback rule live in `useCrmRecordsQuota`, shared with\n * the company and deal drawers so every door on this hub refuses on the\n * same number.\n */\n const records = useCrmRecordsQuota(dataScope, org, {\n contactsFallback: contacts.length,\n })\n // Pending or denied, the listener length stands in. It can only UNDERSTATE\n // (it is the same collection, capped), never overstate, so no alert this\n // number gates can fire on a count larger than the truth.\n const contactCount = records.contactsCount ?? contacts.length\n // Records bands (AGL-890): a plan with an overage rate meters past the\n // included count instead of blocking; a plan without one hard-bands\n // (quota.allowed = false).\n const quota = records.quota\n // Signups whose CRM record was dropped at a hard band (AGL-891), Free's\n // included: capture still fills that band on a plan without the CRM.\n // Written by upsert-contact, host-scoped.\n // Host-scoped, so absent at the organization level (AGL-2630): the\n // counter is a fact about one site's capture, and summing thirty of them\n // would be a figure about nothing in particular.\n const { data: droppedCounter } = useFirestoreDoc<any>(\n () => (hostId ? doc(firestore, 'hosts', hostId, 'counters', 'contactsDropped') : null),\n [firestore, hostId],\n )\n const droppedTotal = Number(droppedCounter?.['total'] ?? 0)\n\n /*==========================================\n * REFUNDS THAT REACHED NO CONTACT (AGL-2329).\n *\n * `recordContactRefund` refuses to create a contact for a refund — a\n * contact holding a refund and no purchase is a phantom record with\n * negative lifetime value — and increments\n * `hosts/{hostId}/counters/contactRefundsUnmatched` instead. Its own\n * comment says the shape mirrors `contactsDropped` \"so an operator…\", and\n * there the sentence stops: `contactsDropped` had this reader and the\n * refund counter had none, so a refund that reached no contact record\n * incremented a number nobody could see.\n *\n * It sits beside the dropped-signup alert because they are the same kind\n * of fact — something that happened to this host's CRM and left no row —\n * and an operator reconciling their contact list needs both or neither.\n *=========================================*/\n const { data: unmatchedRefundCounter } = useFirestoreDoc<any>(\n () =>\n hostId\n ? doc(firestore, 'hosts', hostId, 'counters', 'contactRefundsUnmatched')\n : null,\n [firestore, hostId],\n )\n const unmatchedRefundTotal = Number(unmatchedRefundCounter?.['total'] ?? 0)\n const unmatchedRefundReason = String(\n unmatchedRefundCounter?.['lastReason'] ?? '',\n )\n\n // Saved segments (AGL-199): reusable audience filters.\n const { data: segmentDocs } = useFirestoreCollection<any>(\n () =>\n dataScope\n ? query(\n collection(\n firestore,\n dataScope[0],\n dataScope[1],\n 'contactSegments',\n ),\n // The first fifty by name — the menu's own order — rather than\n // whichever fifty the ids happen to sort first.\n orderBy('name'),\n limit(50),\n )\n : null,\n [firestore, dataScope],\n { idField: '$id' },\n )\n const segments = useMemo(\n () =>\n [...(segmentDocs ?? [])].sort((a, b) =>\n String(a.name ?? '').localeCompare(String(b.name ?? '')),\n ),\n [segmentDocs],\n )\n /*\n * Every saved segment, offered in the views menu as a view (AGL-2617):\n * choosing one puts its tag and source clauses on the filter bar. A\n * segment can only have come FROM an org scope, so the delete is\n * unreachable without one in practice — but the scope is nullable\n * (AGL-1050) and an unguarded deref would be a crash rather than a no-op\n * if that ever stopped being true.\n */\n const segmentPresets = useMemo<CrmViewPreset[]>(\n () =>\n segments.map((segment: any) => ({\n id: String(segment.$id),\n label: String(segment.name ?? ''),\n filters: crmViewFiltersFromSegment({\n tags: segment.tags ?? [],\n sources: segment.sources ?? [],\n }),\n onRemove: dataScope\n ? () =>\n deleteDoc(\n doc(\n firestore,\n dataScope[0],\n dataScope[1],\n 'contactSegments',\n String(segment.$id),\n ),\n )\n : undefined,\n })),\n [segments, dataScope, firestore],\n )\n\n const uid = user?.uid ?? ''\n /*\n * The choices the filter bar picks from. The companies are read only once\n * the reader has reached for a filter or is looking at a view that names\n * one — a read a list nobody is narrowing should not pay for, the same\n * bargain the roster gets above. A stage and a source are fixed lists and\n * cost nothing; a custom `select` field offers its own options.\n */\n const companies = useCompanyOptions({\n hostId,\n org,\n enabled: filterOpened || namesFilter,\n })\n const filterOptions = useMemo<Record<string, readonly ListFilterOption[]>>(\n () => ({\n [CRM_CONTACT_VIEW_FIELDS.owner]: [\n ...(uid ? [{ value: uid, label: 'Me' }] : []),\n ...members.options\n .filter((option) => option.uid !== uid)\n .map((option) => ({ value: option.uid, label: Aglyn.crmMemberPickerLabel(option) })),\n ],\n [CRM_CONTACT_VIEW_FIELDS.stage]: CONTACT_LIFECYCLE_STAGES.map((stage) => ({\n value: stage,\n label: CONTACT_LIFECYCLE_STAGE_LABELS[stage],\n })),\n [CRM_CONTACT_VIEW_FIELDS.source]: Object.entries(SOURCE_LABELS).map(\n ([value, label]) => ({ value, label }),\n ),\n [CRM_CONTACT_VIEW_FIELDS.company]: companies.options.map((company) => ({\n value: company.id,\n label: company.name,\n })),\n // Every value the org keeps, inactive ones marked, as the Leads list offers them.\n [CONTACT_LEAD_SOURCE_COLUMN]: leadSourceList.picklist.values.map((value) => ({\n value: value.label,\n label: value.active ? value.label : `${value.label} (inactive)`,\n })),\n // The verdict on the address (AGL-3245): \"Email is bounced\".\n emailState: Aglyn.EMAIL_STATE_STATUSES.map((status) => ({\n value: status,\n label: Aglyn.EMAIL_STATE_LABELS[status],\n })),\n ...Object.fromEntries(\n customFields.active\n .filter((definition) => definition.type === 'select' && definition.options?.length)\n .map((definition) => [\n crmContactCustomColumn(definition.key),\n (definition.options ?? []).map((option) => ({ value: option, label: option })),\n ]),\n ),\n }),\n [uid, members.options, companies.options, customFields.active, leadSourceList.picklist],\n )\n\n // What the query could not hold, named by the clause the reader set.\n const refused = useMemo(\n () =>\n crmQueryRefusals(paged.plan, asked, {\n fields: filterFields,\n headers: filterHeaders,\n options: filterOptions,\n }),\n [paged.plan, asked, filterFields, filterHeaders, filterOptions],\n )\n\n /* One row grammar, the console's (AGL-2501) — the same table everywhere. */\n // The org's sharing rules, to name the rule behind a shared row (AGL-3336).\n const sharingRules = useCrmSharingRules(org)\n const contactColumns = useMemo(\n () => [\n ...contactListColumns({\n memberName: members.memberName,\n // \"Known by\", at the organization level only (AGL-2630).\n siteName: mount ? mount.siteName : undefined,\n shareChipLabel: (chip) => crmShareChipLabel(chip, sharingRules),\n }),\n // The org's custom fields as optional columns (AGL-2601).\n ...customFieldColumns(customFields.active),\n ],\n [members.memberName, mount, customFields.active, sharingRules],\n )\n /*\n * The grid's column and sort models are the VIEW'S (AGL-2617). The\n * filter-only hidden columns — the declared fields that are not table\n * columns, never the custom ones, which are shown — stay hidden whatever\n * a view says, so a saved arrangement cannot unhide a strip of blank\n * cells.\n */\n /*\n * The grid's own Filters panel and quick search edit the view's clauses\n * (AGL-3313). The picked fields — owner, stage, source, company, email\n * verdict, a custom select — are select columns over the same choices the\n * chips name them by; the rest keep the operators the grammar declares.\n */\n const filterColumns = useMemo(\n () =>\n listFilterGridColumns(contactColumns, filterFields, filterOptions, filterHeaders),\n [contactColumns, filterFields, filterOptions, filterHeaders],\n )\n const gridFilter = useListGridFilter({\n clauses: views.state.filters,\n onChange: views.setFilters,\n selectFields: Object.keys(filterOptions),\n search: { words: searchWords, onChange: setSearchWords },\n })\n const grid = useCrmViewGrid(views, filterColumns, HIDDEN_COLUMNS)\n /*\n * EVERY HEADER SORTS (AGL-3680): the stored ones on the query\n * (`CONTACT_LIST_SORTS`), the holder's facet values and the joined names\n * over the page, saying so — the stage by its place in the lifecycle.\n */\n const columnSort = useCrmColumnSort<ContactRecord>({\n views,\n sorts: CONTACT_LIST_SORTS,\n defaultSort: CONTACT_LIST_SORTS[0],\n asked: contactSort,\n orderBy: paged.plan.orderBy,\n rows: contacts,\n columns: grid.columns,\n pageSorts: CONTACT_PAGE_SORTS,\n })\n\n /*\n * A SEGMENT IS A VIEW'S TAG AND SOURCE CLAUSES, kept where a campaign\n * can reach them (AGL-2617).\n *\n * The segment stays: it is what a campaign audience and a dynamic list\n * resolve, and `contactMatchesSegment` is the one predicate both the\n * console and the sender read. What changed is where it comes from — the\n * filter bar's tag and source clauses, read back through\n * `crmViewSegmentFilters`, so \"Save as segment\" is offered exactly when\n * the working filters hold something a segment can hold. Each saved\n * segment is offered in the views menu as a preset, and applying one\n * puts its clauses on the bar, where they can be narrowed further and\n * saved as a view.\n */\n const segmentFilters = useMemo(\n () => crmViewSegmentFilters(views.state.filters),\n [views.state.filters],\n )\n const [segmentName, setSegmentName] = useState<string | null>(null)\n const handleSaveSegment = useCallback(async () => {\n const name = (segmentName ?? '').trim().slice(0, 60)\n // No org, no place to put it (AGL-1050). The button is disabled in\n // this state; the guard is here so the callback cannot outlive it.\n if (!name || !segmentFilters || !dataScope) return\n setSegmentName(null)\n try {\n await addDoc(\n collection(firestore, dataScope[0], dataScope[1], 'contactSegments'),\n {\n name,\n tags: segmentFilters.tags ?? [],\n sources: segmentFilters.sources ?? [],\n // `contactSegments` is in SCOPED_COLLECTIONS and was the one\n // member of it with no `array-contains-any` reader and no rules\n // `hasAny` — the rules gate it on `isOrgWideMember()` and\n // `campaign-send` checks it with `visibleToHost`, which passes on\n // a missing field. So nothing broke, which is precisely why this\n // went unnoticed (AGL-1478): a collection listed as scoped that\n // nothing enforces is a trap set for whoever wires up the first\n // scoped read, and they would inherit every segment ever saved\n // already broken. Stamped at creation instead, like its siblings.\n //\n // Org-wide, unconditionally: `useOrgDataScope` resolves to\n // `['orgs', orgId]` or to null, AGL-1061 having removed the\n // `hosts/{hostId}` branch after counting zero documents there.\n // A segment is a saved filter over org contacts, so it is exactly\n // as visible as they are.\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n createdAt: new Date(),\n },\n )\n enqueueSnackbar(\n `Segment \"${name}\" saved — usable as a campaign audience`,\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [segmentName, segmentFilters, firestore, dataScope, enqueueSnackbar])\n\n /** The rows ticked for a bulk action (AGL-2603); the bar above the table acts on them. */\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n // A filter, a search or a page is a different set of rows; a selection\n // made on the last one would be a count over rows no longer on screen.\n useEffect(() => setSelectedIds([]), [paged.plan, paged.page])\n /*==========================================\n * ADDING ONE PERSON BY HAND (AGL-2596).\n *\n * Through the plugin's server route rather than a client write, because\n * creating a contact is the one act on this surface the rules cannot fully\n * judge: the dedupe against every holder's rows is a lookup the capturing\n * site may not read, and the audience band is a count the browser cannot\n * take. The route answers `{ contactId, created }`; a merge onto somebody\n * already in the address book is not an error, and the page says which\n * happened before it opens the record.\n *=========================================*/\n // The site the route resolves the org from and stamps the record with: the\n // mounted site, or at the organization level the one the drawer's picker\n // named (AGL-2630) — the drawer holds its submit until it is known.\n const crmApi = useCrmApi(crmScope.createHostId)\n const [createBusy, setCreateBusy] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const handleCreate = useCallback(\n async (values: NewContactValues) => {\n setCreateBusy(true)\n setCreateError(null)\n try {\n const { response, payload } = await crmApi('contacts-create', {\n email: values.email,\n ...(values.name ? { name: values.name } : {}),\n // Salesforce's standard fields (AGL-3515), each only when given.\n ...(values.salutation ? { salutation: values.salutation } : {}),\n ...(values.firstName ? { firstName: values.firstName } : {}),\n ...(values.lastName ? { lastName: values.lastName } : {}),\n ...(values.mobilePhone ? { mobilePhone: values.mobilePhone } : {}),\n ...(values.department ? { department: values.department } : {}),\n ...(values.doNotCall ? { doNotCall: true } : {}),\n ...(values.phone ? { phone: values.phone } : {}),\n ...(values.jobTitle ? { jobTitle: values.jobTitle } : {}),\n ...(values.companyName ? { companyName: values.companyName } : {}),\n ...(values.companyId ? { companyId: values.companyId } : {}),\n ...(values.address ? { address: values.address } : {}),\n ...(values.ownerUid ? { ownerUid: values.ownerUid } : {}),\n ...(values.lifecycleStage\n ? { lifecycleStage: values.lifecycleStage }\n : {}),\n ...(values.tags.length ? { tags: values.tags } : {}),\n marketingConsent: values.marketingConsent,\n ...(values.marketingConsent && values.disclosedConsentGroup\n ? { disclosedConsentGroup: values.disclosedConsentGroup }\n : {}),\n })\n if (!response.ok) {\n // The route's own sentence, shown above the form unchanged: the\n // band refusal is the list's wording, and a field refusal names\n // the field.\n setCreateError(\n String(payload['error'] ?? 'The contact could not be added.'),\n )\n return\n }\n const contactId = String(payload['contactId'] ?? '')\n // The activity entry is the route's (AGL-2622): it verified the\n // caller and performed the write, so it is the one writer that\n // cannot record an add that did not happen — and a second entry\n // from here would put two rows on one act.\n enqueueSnackbar(\n payload['created']\n ? 'Contact added'\n : 'Already a contact — what you entered was merged into their record',\n { variant: 'success', persist: false },\n )\n setCreateOpen(false)\n if (contactId) router.push(routes.contact(contactId))\n } catch (error) {\n console.error(error)\n setCreateError('The contact could not be added.')\n } finally {\n setCreateBusy(false)\n }\n },\n [crmApi, enqueueSnackbar, router, routes],\n )\n\n // The list's filter, when one narrows it, for the export to read the same\n // records the list does (AGL-3527).\n const exportFilter = useMemo(\n () =>\n paged.plan.served.length || paged.plan.searched\n ? { label: 'what the list shows', plan: paged.plan }\n : null,\n [paged.plan],\n )\n\n return (\n <>\n {/*\n The list card ends at its pager; the team's recent activity is a\n card of its own under it, drawn only once there is some.\n */}\n <Stack spacing={3}>\n <CardDisplay\n header={'Contacts'}\n help={pluginDocsHelp('contacts', { anchor: '#the-contacts-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n // The record actions, top right and never clipped (AGL-3311).\n action: (\n <CrmListActions>\n {suiteIncluded ? (\n <CrmImportButton resource={CRM_CONTACTS_RESOURCE} noun=\"contacts\" hostId={hostId} mappingZone=\"contacts\" />\n ) : (\n <CrmSuiteLockedButton>{'Import'}</CrmSuiteLockedButton>\n )}\n <CrmExportButton resource={CRM_CONTACTS_RESOURCE} hostId={hostId} filter={exportFilter} />\n {/* A button that opens a drawer — never a create form above the\n list. Disabled until the org has resolved, because the route\n resolves the org from the site and a click before that has\n nowhere to write. */}\n {suiteIncluded ? (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={!dataScope}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {'New contact'}\n </Button>\n ) : (\n <CrmSuiteLockedButton variant=\"contained\" color=\"primary\">\n {'New contact'}\n </CrmSuiteLockedButton>\n )}\n </CrmListActions>\n ),\n }}\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {/* The org's audience, not the page's row count (AGL-1706) —\n two different numbers since the list stopped reading the\n whole collection. */}\n {`${contactCount.toLocaleString()} contacts · ${\n Number.isFinite(quota.included)\n ? `${quota.used.toLocaleString()} of ${quota.included.toLocaleString()} CRM records`\n : `${quota.used.toLocaleString()} CRM records`\n }`}\n </Typography>\n {suiteIncluded ? null : (\n <CrmSuiteNotice>\n {'Contacts arrive here on their own from your forms, sign-ups, ' +\n 'orders and bookings. Adding one by hand, importing a CSV, ' +\n \"saved views, and a contact's owner, stage and company are \" +\n 'part of the CRM.'}\n </CrmSuiteNotice>\n )}\n {/*\n The view this list is showing, and the clauses narrowing it\n (AGL-2617). The control names the view and holds everything a\n reader does to one; the bar holds the clauses, the seed's\n included, every one of them on the query. Segments are\n offered in the control's menu and saved from it.\n */}\n <Stack spacing={1}>\n <CrmViewsControl\n controller={views}\n allLabel=\"All contacts\"\n suiteLocked={!suiteIncluded}\n presets={segmentPresets}\n onSaveAsSegment={\n segmentFilters && dataScope ? () => setSegmentName('') : null\n }\n />\n <ListFilterChips\n fields={filterFields}\n headers={filterHeaders}\n clauses={views.state.filters}\n onChange={views.setFilters}\n options={filterOptions}\n marksServed={false}\n />\n </Stack>\n {!quota.allowed ? (\n <Alert severity=\"warning\">\n {'CRM records limit reached — new visitors are no longer ' +\n 'captured' +\n (droppedTotal > 0\n ? ` (${droppedTotal.toLocaleString()} missed so far)`\n : '') +\n '. Upgrade in Billing to keep collecting.'}\n </Alert>\n ) : quota.overageRecords > 0 &&\n quota.overageRateUsd != null &&\n // No claim about money until the verdict that decides it has\n // settled (AGL-1662). Before Remote Config activation the flag\n // reads its registry default, which is no org's verdict, so an\n // ungated alert could assert the wrong wording for one paint.\n releaseFlagsReady ? (\n <Alert severity=\"info\">\n {contactsBilled\n ? // Same sentence as the billing page's caption, and the same\n // basis (AGL-2399): the count here is LIVE, the invoice\n // charges the last reading before the month closes, so the\n // dollar figure is a projection until the month ends. Staff\n // and customer must not read different sentences about the\n // same org's money — that applies to WHEN it is measured as\n // much as to how much.\n `${quota.overageRecords.toLocaleString()} CRM records over ` +\n `your plan's included ${quota.included.toLocaleString()} — ` +\n `metered at $${quota.overageRateUsd}/1,000 per month ` +\n `(≈$${quota.overageMonthlyUsd.toFixed(2)} if your list ends ` +\n 'the month at this size). ' +\n 'Upgrade in Billing for a larger included audience.'\n : // The wording `db5ecdf2b` put on the billing page, which is\n // itself the wording `1a2aed5cb` published to\n // `billing-and-plans/overview.md` (AGL-1601/1603). Staff and\n // customer must not read different sentences about the same\n // org's money.\n //\n // THE COUNT STAYS, THE TOTAL GOES: the head-count is real —\n // ingestion captured those records — and is not a claim\n // about money. The upgrade nudge goes with the total, since\n // it prompts a purchase premised on a charge that is not\n // happening.\n `${quota.overageRecords.toLocaleString()} CRM records over ` +\n `your plan's included ${quota.included.toLocaleString()} — ` +\n 'not billed while the CRM is unavailable. ' +\n `The $${quota.overageRateUsd}/1,000 rate applies once ` +\n 'Contacts opens.'}\n </Alert>\n ) : droppedTotal > 0 ? (\n <Alert severity=\"info\">\n {`${droppedTotal.toLocaleString()} earlier visitor${\n droppedTotal === 1 ? ' was' : 's were'\n } not captured while your CRM records band was full.`}\n </Alert>\n ) : null}\n {/*\n Unlike the band alert above, this one is NOT exclusive with the\n others: a host can be over its band AND have refunds that landed\n nowhere, and the two have different remedies. Chaining it into the\n same ternary would hide whichever fact came second.\n */}\n {unmatchedRefundTotal > 0 ? (\n <Alert severity=\"warning\">\n {`${unmatchedRefundTotal.toLocaleString()} refund${\n unmatchedRefundTotal === 1 ? '' : 's'\n } could not be recorded against a contact${\n UNMATCHED_REFUND_REASON[unmatchedRefundReason]\n ? ` — most recently because ${UNMATCHED_REFUND_REASON[unmatchedRefundReason]}`\n : ''\n }. The money moved; the customer's timeline does not show it.`}\n </Alert>\n ) : null}\n <ListQueryNotices\n refused={refused}\n notices={[...paged.plan.notices, ...columnSort.notices]}\n />\n {contacts.length === 0 &&\n !views.state.filters.length &&\n !searchWords.some((word) => word.trim()) ? (\n <EmptyStateComponent\n label={contactsStatus === 'loading' ? 'Loading contacts…' : 'No contacts yet'}\n description={\n contactsStatus === 'loading'\n ? undefined\n : suiteIncluded\n ? 'Form submissions, member sign-ups, orders and bookings become contacts on their own; add one by hand or bring a list in from a CSV.'\n : 'Form submissions, member sign-ups, orders and bookings become contacts on their own.'\n }\n action={\n contactsStatus === 'loading' ? undefined : !suiteIncluded ? (\n <Stack direction=\"row\" spacing={1}>\n <CrmSuiteLockedButton variant=\"contained\" color=\"primary\">\n {'New contact'}\n </CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Import'}</CrmSuiteLockedButton>\n </Stack>\n ) : (\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={!dataScope}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {'New contact'}\n </Button>\n <CrmImportButton resource={CRM_CONTACTS_RESOURCE} noun=\"contacts\" hostId={hostId} mappingZone=\"contacts\" />\n </Stack>\n )\n }\n />\n ) : (\n <>\n <ContactsBulkBar hostId={hostId} org={org} scope={dataScope} consentGroup={consentGroup} rows={contacts} selected={selectedIds} onSelectedChange={setSelectedIds} suiteLocked={!suiteIncluded} />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={columnSort.rows}\n columnSort={columnSort}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n onOpen={(id) => router.push(routes.contact(id))}\n /*\n * The grid's Filters panel and quick search edit the view's\n * clauses; the list's query answers them (AGL-3321), so the\n * grid must not filter again itself. Opening the panel is\n * what reads the roster and the companies its pickers name.\n * Columns are the view's, controlled so a saved arrangement\n * is what the grid shows. The headers sort through\n * `columnSort` (AGL-3680): a stored column re-asks the\n * query, any other sorts the page and says so.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n onPreferencePanelOpen={() => setFilterOpened(true)}\n // A filter that matched nothing is said inside the grid,\n // so the toolbar that set it stays to change it.\n noRowsLabel={\n contactsStatus === 'loading' ? 'Loading…' : 'No contacts match these filters'\n }\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={grid.onColumnVisibilityModelChange}\n loading={contactsStatus === 'loading'}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n </CrmColumnOrderProvider>\n <ListPagination\n page={paged.page}\n pageSize={paged.pageSize}\n rowCount={contacts.length}\n hasMore={paged.hasMore}\n onPageChange={paged.setPage}\n onPageSizeChange={paged.setPageSize}\n />\n </>\n )}\n </Stack>\n </CardDisplay>\n <RecentActivityFeed hostId={hostId} org={org} basePath={props.basePath} />\n </Stack>\n {/*\n Naming a segment is a dialog, never a form above the list. Open\n while the name is a string; `null` is closed.\n */}\n <Dialog\n open={segmentName !== null}\n onClose={() => setSegmentName(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{'Save as segment'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The tag and source filters, kept as a segment an email campaign or a dynamic list can send to. The other filters stay on this view only.'}\n </Typography>\n <TextField\n autoFocus\n size=\"small\"\n label=\"Segment name\"\n value={segmentName ?? ''}\n onChange={(event) => setSegmentName(event.target.value.slice(0, 60))}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n void handleSaveSegment()\n }\n }}\n slotProps={{ htmlInput: { maxLength: 60 } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setSegmentName(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n disabled={!(segmentName ?? '').trim() || !dataScope}\n onClick={() => void handleSaveSegment()}\n >\n {'Save segment'}\n </Button>\n </DialogActions>\n </Dialog>\n {/*\n Mounted only while open, so a page that never reaches for it never\n renders a drawer's chrome, and every opening starts from a blank form.\n */}\n {createOpen ? (\n <NewContactDrawer\n open\n onClose={() => setCreateOpen(false)}\n hostId={hostId}\n org={org}\n busy={createBusy}\n error={createError}\n owners={members.options}\n ownersReady={members.ready}\n onSubmit={(values) => void handleCreate(values)}\n />\n ) : null}\n </>\n )\n}\nContactsPeopleSection.displayName = 'ContactsPeopleSection'\n\nexport default ContactsPeopleSection\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","CONTACT_SOURCE_LABELS","CRM_CONTACT_VIEW_FIELDS","crmContactCustomColumn","crmViewFiltersFromSegment","crmViewSegmentFilters","newResourceScopeFields","ORG_SCOPE_TOKEN","pluginDocsHelp","hiddenFilterVisibility","ListTable","ListPagination","ListQueryNotices","CONTACT_LEAD_SOURCE_COLUMN","CONTACT_LIST_DECLARATION","CONTACT_LIST_FILTER_FIELDS","CONTACT_LIST_FILTER_HEADERS","CONTACT_LIST_SORTS","CONTACT_PREFIX_SEARCH","contactClauseImpliesScope","contactSoloClause","contactCustomFilterFields","contactCustomFilterHeaders","contactQueryClause","crmAskClauses","crmQueryRefusals","useCrmFoldsScope","useCrmListQuery","crmViewQuerySort","useCrmColumnSort","contactPrimaryGroup","contactRecordFromDoc","useCrmOrgMount","useCrmSharingRules","crmShareChipLabel","useCrmScope","contactsListSeed","crmRoutes","CRM_CONTACTS_RESOURCE","useCrmRecordsQuota","CardDisplay","EmptyStateComponent","ContactsBulkBar","CrmListActions","CrmExportButton","CrmImportButton","useSnackbar","useFirestore","useFirestoreCollection","useFirestoreDoc","useUser","Alert","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","addDoc","collection","deleteDoc","doc","limit","orderBy","query","useRouter","useSearchParams","useCallback","useEffect","useMemo","useRef","useState","RecentActivityFeed","CONTACT_FILTER_COLUMNS","CONTACT_OPTIONAL_COLUMNS","contactListColumns","NewContactDrawer","useCrmApi","useOrgMembers","useContactFieldDefinitions","useLeadSourcePicklist","customFieldColumns","useCrmSavedView","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","useCompanyOptions","ListFilterChips","listFilterGridColumns","useListGridFilter","CrmViewsControl","CrmSuiteLockedButton","CrmSuiteNotice","crmSuiteIncluded","SOURCE_LABELS","CONTACT_PAGE_SORTS","lifecycleStage","row","indexOf","HIDDEN_COLUMNS","Object","fromEntries","map","field","UNMATCHED_REFUND_REASON","ContactsPeopleSection","props","records","hostId","org","releaseFlag","basePath","routes","router","contactsBilled","released","releaseFlagsReady","ready","crmScope","scope","dataScope","orgId","consentGroup","visibleTo","visibleToTokens","suiteIncluded","mount","customFields","leadSourceList","firestore","enqueueSnackbar","data","user","searchParams","seed","seedFilters","filter","source","formId","op","value","viewInitial","filters","views","section","initial","filterFields","active","filterHeaders","foldsScope","groupId","asked","state","clause","impliesScope","searchWords","setSearchWords","contactSort","sort","paged","declaration","clauses","search","soloClause","prefixSearch","contactDocs","rows","contactsStatus","status","openedByEmail","current","openEmail","served","plan","some","only","length","hasMore","replace","contact","String","$id","contacts","createOpen","setCreateOpen","filterOpened","setFilterOpened","namesFilter","owner","company","members","enabled","Boolean","ownerUid","contactsFallback","contactCount","contactsCount","quota","droppedCounter","droppedTotal","Number","unmatchedRefundCounter","unmatchedRefundTotal","unmatchedRefundReason","segmentDocs","idField","segments","a","b","name","localeCompare","segmentPresets","segment","id","label","tags","sources","onRemove","undefined","uid","companies","filterOptions","options","option","crmMemberPickerLabel","stage","entries","picklist","values","emailState","EMAIL_STATE_STATUSES","EMAIL_STATE_LABELS","definition","type","key","refused","fields","headers","sharingRules","contactColumns","memberName","siteName","shareChipLabel","chip","filterColumns","gridFilter","onChange","setFilters","selectFields","keys","words","grid","columnSort","sorts","defaultSort","columns","pageSorts","segmentFilters","segmentName","setSegmentName","handleSaveSegment","trim","slice","createdAt","Date","variant","persist","error","console","allowDuplicate","selectedIds","setSelectedIds","page","crmApi","createHostId","createBusy","setCreateBusy","createError","setCreateError","handleCreate","payload","response","email","salutation","firstName","lastName","mobilePhone","department","doNotCall","phone","jobTitle","companyName","companyId","address","marketingConsent","disclosedConsentGroup","ok","contactId","push","exportFilter","searched","spacing","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","resource","noun","mappingZone","size","color","disabled","onClick","toLocaleString","isFinite","included","used","controller","allLabel","suiteLocked","presets","onSaveAsSegment","marksServed","allowed","severity","overageRecords","overageRateUsd","overageMonthlyUsd","toFixed","notices","word","description","direction","selected","onSelectedChange","columnOrder","slots","selectable","onOpen","filterMode","filterModel","onFilterModelChange","quickFilter","onPreferencePanelOpen","noRowsLabel","columnVisibilityModel","onColumnVisibilityModelChange","loading","hideFooter","pageSize","rowCount","onPageChange","setPage","onPageSizeChange","setPageSize","open","onClose","maxWidth","fullWidth","sx","pt","autoFocus","event","target","onKeyDown","preventDefault","slotProps","htmlInput","maxLength","busy","owners","ownersReady","onSubmit","displayName"],"mappings":"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"}
@@ -28,6 +28,11 @@ export interface CrmEmailCheck {
28
28
  label: string;
29
29
  tone: ChipTone;
30
30
  } | null;
31
+ /**
32
+ * The address belongs to a locked or banned Aglyn account (AGL-3686).
33
+ * Answered on the house workspace's sites only; `null` everywhere else.
34
+ */
35
+ accountLock: 'banned' | 'locked' | null;
31
36
  }
32
37
  /**
33
38
  * The server's answer for `email`, sent from `hostId` — `null` until it
@@ -57,6 +62,15 @@ export declare function CrmEmailGatewayChip(props: CrmEmailGatewayChipProps): im
57
62
  export declare namespace CrmEmailGatewayChip {
58
63
  var displayName: string;
59
64
  }
65
+ /**
66
+ * Why a record whose consent reads as given is skipped by every send
67
+ * (AGL-3686): its address belongs to a locked or banned Aglyn account.
68
+ * Only the house workspace is told, so it draws nothing anywhere else.
69
+ */
70
+ export declare function CrmAccountLockChip(props: Omit<CrmEmailGatewayChipProps, 'emailState'>): import("react").JSX.Element;
71
+ export declare namespace CrmAccountLockChip {
72
+ var displayName: string;
73
+ }
60
74
  /**
61
75
  * The composer's warning before a one-to-one send (AGL-3328): the address's
62
76
  * domain has no mail server, or its mail gateway refused this site's
@@ -15,6 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { jsx as _jsx } from "react/jsx-runtime";
18
+ import { PLATFORM_BRAND_NAME } from "@aglyn/aglyn";
18
19
  import { Alert, Chip, Tooltip } from "@mui/material";
19
20
  import { useEffect, useState } from "react";
20
21
  import { useCrmApi } from "./use-crm-api.js";
@@ -27,6 +28,7 @@ const REMEMBER_MS = 60000;
27
28
  function readAnswer(payload) {
28
29
  const chip = payload['chip'];
29
30
  const code = payload['code'];
31
+ const accountLock = payload['accountLock'];
30
32
  return {
31
33
  checked: payload['checked'] === true,
32
34
  code: code === 'no_mx' || code === 'null_mx' || code === 'gateway_held' ? code : null,
@@ -35,7 +37,8 @@ function readAnswer(payload) {
35
37
  chip: chip && typeof chip.label === 'string' && typeof chip.tone === 'string' ? {
36
38
  label: chip.label,
37
39
  tone: chip.tone
38
- } : null
40
+ } : null,
41
+ accountLock: accountLock === 'banned' || accountLock === 'locked' ? accountLock : null
39
42
  };
40
43
  }
41
44
  /**
@@ -110,6 +113,40 @@ const TONE_COLOR = {
110
113
  });
111
114
  }
112
115
  CrmEmailGatewayChip.displayName = 'CrmEmailGatewayChip';
116
+ const ACCOUNT_LOCK_CHIP = {
117
+ banned: {
118
+ label: 'Account banned',
119
+ title: `This address belongs to a ${PLATFORM_BRAND_NAME} account banned for abuse. Nothing is sent to it — no campaign, sequence or one-to-one email — whatever its consent says. Lifting the ban restores it.`
120
+ },
121
+ locked: {
122
+ label: 'Account locked',
123
+ title: `This address belongs to a locked ${PLATFORM_BRAND_NAME} account. Our campaigns and sequences skip it until the lock is lifted; its consent is unchanged.`
124
+ }
125
+ };
126
+ /**
127
+ * Why a record whose consent reads as given is skipped by every send
128
+ * (AGL-3686): its address belongs to a locked or banned Aglyn account.
129
+ * Only the house workspace is told, so it draws nothing anywhere else.
130
+ */ export function CrmAccountLockChip(props) {
131
+ var _useCrmEmailCheck;
132
+ const { hostId, email, enabled = true, size = 'small' } = props;
133
+ const lock = (_useCrmEmailCheck = useCrmEmailCheck(hostId, email, {
134
+ enabled
135
+ })) == null ? void 0 : _useCrmEmailCheck.accountLock;
136
+ if (!lock) return null;
137
+ const { label, title } = ACCOUNT_LOCK_CHIP[lock];
138
+ return /*#__PURE__*/ _jsx(Tooltip, {
139
+ title: title,
140
+ children: /*#__PURE__*/ _jsx(Chip, {
141
+ size: size,
142
+ variant: "filled",
143
+ color: lock === 'banned' ? 'error' : 'warning',
144
+ label: label,
145
+ "data-testid": "crm-account-lock"
146
+ })
147
+ });
148
+ }
149
+ CrmAccountLockChip.displayName = 'CrmAccountLockChip';
113
150
  /**
114
151
  * The composer's warning before a one-to-one send (AGL-3328): the address's
115
152
  * domain has no mail server, or its mail gateway refused this site's
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-email-check.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\n'use client'\n\nimport { type EmailState } from '@aglyn/aglyn'\nimport { Alert, Chip, type ChipProps, Tooltip } from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { useCrmApi } from './use-crm-api'\n\n/*==========================================\n * WHAT THE PLATFORM KNOWS ABOUT ONE ADDRESS (AGL-3328).\n *\n * `crm/email-check` answers it from the server — the domain's MX from the\n * platform cache, and the ledger of what that domain's mail gateway did\n * with this site's sending domain. The browser does no DNS. Two surfaces\n * read it: the gateway chip beside a record's address, and the composer's\n * warning before a one-to-one send.\n *==========================================*/\n\n/** The chip's tone, as the server's `mailGatewayChip` names it. */\ntype ChipTone = 'refused' | 'delivered' | 'neutral' | 'blocked'\n\n/** What `crm/email-check` answers. */\nexport interface CrmEmailCheck {\n checked: boolean\n code: 'no_mx' | 'null_mx' | 'gateway_held' | null\n message: string | null\n gateway: string | null\n chip: { label: string; tone: ChipTone } | null\n}\n\n/**\n * Answers already fetched, by site and address, for a minute: a record page\n * draws the chip and then opens the composer, and both ask about the same\n * address.\n */\nconst remembered = new Map<string, { atMs: number; answer: Promise<CrmEmailCheck | null> }>()\nconst REMEMBER_MS = 60_000\n\nfunction readAnswer(payload: Record<string, unknown>): CrmEmailCheck {\n const chip = payload['chip'] as { label?: unknown; tone?: unknown } | null\n const code = payload['code']\n return {\n checked: payload['checked'] === true,\n code: code === 'no_mx' || code === 'null_mx' || code === 'gateway_held' ? code : null,\n message: typeof payload['message'] === 'string' ? payload['message'] : null,\n gateway: typeof payload['gateway'] === 'string' ? payload['gateway'] : null,\n chip:\n chip && typeof chip.label === 'string' && typeof chip.tone === 'string'\n ? { label: chip.label, tone: chip.tone as ChipTone }\n : null,\n }\n}\n\n/**\n * The server's answer for `email`, sent from `hostId` — `null` until it\n * arrives, and `null` when it could not be had: a check that fails says\n * nothing rather than something wrong.\n */\nexport function useCrmEmailCheck(\n hostId: string | null,\n email: string | null | undefined,\n options: { enabled?: boolean } = {},\n): CrmEmailCheck | null {\n const enabled = options.enabled !== false\n const address = String(email ?? '')\n .trim()\n .toLowerCase()\n const api = useCrmApi(hostId)\n const [answer, setAnswer] = useState<CrmEmailCheck | null>(null)\n useEffect(() => {\n setAnswer(null)\n if (!enabled || !address) return undefined\n let cancelled = false\n const key = `${hostId ?? ''}|${address}`\n const now = Date.now()\n let entry = remembered.get(key)\n if (!entry || entry.atMs + REMEMBER_MS < now) {\n entry = {\n atMs: now,\n answer: api('email-check', { email: address })\n .then(({ response, payload }) => (response.ok ? readAnswer(payload) : null))\n .catch(() => null),\n }\n remembered.set(key, entry)\n }\n void entry.answer.then((value) => {\n if (!cancelled) setAnswer(value)\n })\n return () => {\n cancelled = true\n }\n }, [api, hostId, address, enabled])\n return answer\n}\n\nconst TONE_COLOR: Record<ChipTone, ChipProps['color']> = {\n refused: 'error',\n blocked: 'error',\n delivered: 'success',\n neutral: 'default',\n}\n\nexport interface CrmEmailGatewayChipProps {\n hostId: string | null\n email: string | null | undefined\n /** The record's email state: a \"Would bounce\" already says what a no-MX chip would. */\n emailState?: EmailState | null\n /** Off where the page cannot ask — a plan without the CRM. */\n enabled?: boolean\n size?: ChipProps['size']\n}\n\n/**\n * The mail gateway in front of a record's address, and what it did with\n * this site's mail this week (AGL-3328): \"Barracuda · 2 of 2 sends refused\n * this week\", \"Microsoft 365 · 5 of 5 delivered this week\", or just the\n * gateway's name. Nothing for an unrecognized exchange with no verdict, a\n * public mailbox provider, or an address nothing could be read about.\n */\nexport function CrmEmailGatewayChip(props: CrmEmailGatewayChipProps) {\n const { hostId, email, emailState, enabled = true, size = 'small' } = props\n const check = useCrmEmailCheck(hostId, email, { enabled })\n const chip = check?.chip\n if (!chip) return null\n if (chip.tone === 'blocked' && emailState?.status === 'undeliverable') return null\n return (\n <Tooltip title={check?.message ?? 'The mail gateway in front of this address'}>\n <Chip\n size={size}\n variant=\"outlined\"\n label={chip.label}\n color={TONE_COLOR[chip.tone]}\n data-testid=\"crm-email-gateway\"\n />\n </Tooltip>\n )\n}\nCrmEmailGatewayChip.displayName = 'CrmEmailGatewayChip'\n\n/**\n * The composer's warning before a one-to-one send (AGL-3328): the address's\n * domain has no mail server, or its mail gateway refused this site's\n * sending domain twice this month and delivered nothing. The send itself\n * decides what leaves; this says what is likely to happen before it does.\n */\nexport function crmEmailCheckWarning(check: CrmEmailCheck | null): string | null {\n if (!check?.code || !check.message) return null\n if (check.code === 'gateway_held') {\n return `${check.message} A one-to-one email still goes, but it is likely to be refused too.`\n }\n return `${check.message} Sending will not reach them.`\n}\n\nexport function CrmEmailCheckAlert(props: { check: CrmEmailCheck | null }) {\n const warning = crmEmailCheckWarning(props.check)\n if (!warning) return null\n return (\n <Alert severity=\"warning\" data-testid=\"crm-email-check-warning\">\n {warning}\n </Alert>\n )\n}\n"],"names":["Alert","Chip","Tooltip","useEffect","useState","useCrmApi","remembered","Map","REMEMBER_MS","readAnswer","payload","chip","code","checked","message","gateway","label","tone","useCrmEmailCheck","hostId","email","options","enabled","address","String","trim","toLowerCase","api","answer","setAnswer","undefined","cancelled","key","now","Date","entry","get","atMs","then","response","ok","catch","set","value","TONE_COLOR","refused","blocked","delivered","neutral","CrmEmailGatewayChip","props","emailState","size","check","status","title","variant","color","data-testid","displayName","crmEmailCheckWarning","CrmEmailCheckAlert","warning","severity"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-email-check.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\n'use client'\n\nimport { PLATFORM_BRAND_NAME, type EmailState } from '@aglyn/aglyn'\nimport { Alert, Chip, type ChipProps, Tooltip } from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { useCrmApi } from './use-crm-api'\n\n/*==========================================\n * WHAT THE PLATFORM KNOWS ABOUT ONE ADDRESS (AGL-3328).\n *\n * `crm/email-check` answers it from the server — the domain's MX from the\n * platform cache, and the ledger of what that domain's mail gateway did\n * with this site's sending domain. The browser does no DNS. Two surfaces\n * read it: the gateway chip beside a record's address, and the composer's\n * warning before a one-to-one send.\n *==========================================*/\n\n/** The chip's tone, as the server's `mailGatewayChip` names it. */\ntype ChipTone = 'refused' | 'delivered' | 'neutral' | 'blocked'\n\n/** What `crm/email-check` answers. */\nexport interface CrmEmailCheck {\n checked: boolean\n code: 'no_mx' | 'null_mx' | 'gateway_held' | null\n message: string | null\n gateway: string | null\n chip: { label: string; tone: ChipTone } | null\n /**\n * The address belongs to a locked or banned Aglyn account (AGL-3686).\n * Answered on the house workspace's sites only; `null` everywhere else.\n */\n accountLock: 'banned' | 'locked' | null\n}\n\n/**\n * Answers already fetched, by site and address, for a minute: a record page\n * draws the chip and then opens the composer, and both ask about the same\n * address.\n */\nconst remembered = new Map<string, { atMs: number; answer: Promise<CrmEmailCheck | null> }>()\nconst REMEMBER_MS = 60_000\n\nfunction readAnswer(payload: Record<string, unknown>): CrmEmailCheck {\n const chip = payload['chip'] as { label?: unknown; tone?: unknown } | null\n const code = payload['code']\n const accountLock = payload['accountLock']\n return {\n checked: payload['checked'] === true,\n code: code === 'no_mx' || code === 'null_mx' || code === 'gateway_held' ? code : null,\n message: typeof payload['message'] === 'string' ? payload['message'] : null,\n gateway: typeof payload['gateway'] === 'string' ? payload['gateway'] : null,\n chip:\n chip && typeof chip.label === 'string' && typeof chip.tone === 'string'\n ? { label: chip.label, tone: chip.tone as ChipTone }\n : null,\n accountLock: accountLock === 'banned' || accountLock === 'locked' ? accountLock : null,\n }\n}\n\n/**\n * The server's answer for `email`, sent from `hostId` — `null` until it\n * arrives, and `null` when it could not be had: a check that fails says\n * nothing rather than something wrong.\n */\nexport function useCrmEmailCheck(\n hostId: string | null,\n email: string | null | undefined,\n options: { enabled?: boolean } = {},\n): CrmEmailCheck | null {\n const enabled = options.enabled !== false\n const address = String(email ?? '')\n .trim()\n .toLowerCase()\n const api = useCrmApi(hostId)\n const [answer, setAnswer] = useState<CrmEmailCheck | null>(null)\n useEffect(() => {\n setAnswer(null)\n if (!enabled || !address) return undefined\n let cancelled = false\n const key = `${hostId ?? ''}|${address}`\n const now = Date.now()\n let entry = remembered.get(key)\n if (!entry || entry.atMs + REMEMBER_MS < now) {\n entry = {\n atMs: now,\n answer: api('email-check', { email: address })\n .then(({ response, payload }) => (response.ok ? readAnswer(payload) : null))\n .catch(() => null),\n }\n remembered.set(key, entry)\n }\n void entry.answer.then((value) => {\n if (!cancelled) setAnswer(value)\n })\n return () => {\n cancelled = true\n }\n }, [api, hostId, address, enabled])\n return answer\n}\n\nconst TONE_COLOR: Record<ChipTone, ChipProps['color']> = {\n refused: 'error',\n blocked: 'error',\n delivered: 'success',\n neutral: 'default',\n}\n\nexport interface CrmEmailGatewayChipProps {\n hostId: string | null\n email: string | null | undefined\n /** The record's email state: a \"Would bounce\" already says what a no-MX chip would. */\n emailState?: EmailState | null\n /** Off where the page cannot ask — a plan without the CRM. */\n enabled?: boolean\n size?: ChipProps['size']\n}\n\n/**\n * The mail gateway in front of a record's address, and what it did with\n * this site's mail this week (AGL-3328): \"Barracuda · 2 of 2 sends refused\n * this week\", \"Microsoft 365 · 5 of 5 delivered this week\", or just the\n * gateway's name. Nothing for an unrecognized exchange with no verdict, a\n * public mailbox provider, or an address nothing could be read about.\n */\nexport function CrmEmailGatewayChip(props: CrmEmailGatewayChipProps) {\n const { hostId, email, emailState, enabled = true, size = 'small' } = props\n const check = useCrmEmailCheck(hostId, email, { enabled })\n const chip = check?.chip\n if (!chip) return null\n if (chip.tone === 'blocked' && emailState?.status === 'undeliverable') return null\n return (\n <Tooltip title={check?.message ?? 'The mail gateway in front of this address'}>\n <Chip\n size={size}\n variant=\"outlined\"\n label={chip.label}\n color={TONE_COLOR[chip.tone]}\n data-testid=\"crm-email-gateway\"\n />\n </Tooltip>\n )\n}\nCrmEmailGatewayChip.displayName = 'CrmEmailGatewayChip'\n\nconst ACCOUNT_LOCK_CHIP: Record<'banned' | 'locked', { label: string; title: string }> = {\n banned: {\n label: 'Account banned',\n title:\n `This address belongs to a ${PLATFORM_BRAND_NAME} account banned for abuse. Nothing is sent to it — no campaign, sequence or one-to-one email — whatever its consent says. Lifting the ban restores it.`,\n },\n locked: {\n label: 'Account locked',\n title:\n `This address belongs to a locked ${PLATFORM_BRAND_NAME} account. Our campaigns and sequences skip it until the lock is lifted; its consent is unchanged.`,\n },\n}\n\n/**\n * Why a record whose consent reads as given is skipped by every send\n * (AGL-3686): its address belongs to a locked or banned Aglyn account.\n * Only the house workspace is told, so it draws nothing anywhere else.\n */\nexport function CrmAccountLockChip(props: Omit<CrmEmailGatewayChipProps, 'emailState'>) {\n const { hostId, email, enabled = true, size = 'small' } = props\n const lock = useCrmEmailCheck(hostId, email, { enabled })?.accountLock\n if (!lock) return null\n const { label, title } = ACCOUNT_LOCK_CHIP[lock]\n return (\n <Tooltip title={title}>\n <Chip\n size={size}\n variant=\"filled\"\n color={lock === 'banned' ? 'error' : 'warning'}\n label={label}\n data-testid=\"crm-account-lock\"\n />\n </Tooltip>\n )\n}\nCrmAccountLockChip.displayName = 'CrmAccountLockChip'\n\n/**\n * The composer's warning before a one-to-one send (AGL-3328): the address's\n * domain has no mail server, or its mail gateway refused this site's\n * sending domain twice this month and delivered nothing. The send itself\n * decides what leaves; this says what is likely to happen before it does.\n */\nexport function crmEmailCheckWarning(check: CrmEmailCheck | null): string | null {\n if (!check?.code || !check.message) return null\n if (check.code === 'gateway_held') {\n return `${check.message} A one-to-one email still goes, but it is likely to be refused too.`\n }\n return `${check.message} Sending will not reach them.`\n}\n\nexport function CrmEmailCheckAlert(props: { check: CrmEmailCheck | null }) {\n const warning = crmEmailCheckWarning(props.check)\n if (!warning) return null\n return (\n <Alert severity=\"warning\" data-testid=\"crm-email-check-warning\">\n {warning}\n </Alert>\n )\n}\n"],"names":["PLATFORM_BRAND_NAME","Alert","Chip","Tooltip","useEffect","useState","useCrmApi","remembered","Map","REMEMBER_MS","readAnswer","payload","chip","code","accountLock","checked","message","gateway","label","tone","useCrmEmailCheck","hostId","email","options","enabled","address","String","trim","toLowerCase","api","answer","setAnswer","undefined","cancelled","key","now","Date","entry","get","atMs","then","response","ok","catch","set","value","TONE_COLOR","refused","blocked","delivered","neutral","CrmEmailGatewayChip","props","emailState","size","check","status","title","variant","color","data-testid","displayName","ACCOUNT_LOCK_CHIP","banned","locked","CrmAccountLockChip","lock","crmEmailCheckWarning","CrmEmailCheckAlert","warning","severity"],"mappings":"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"}
@@ -8,10 +8,11 @@ import type { GridColDef } from '@mui/x-data-grid';
8
8
  * color, today emphasized — because it IS that date, seen from the record.
9
9
  * A dash for a record with nothing scheduled, and the same dash for one
10
10
  * written before the field existed: the reader's question is "is anything
11
- * planned", and to both the answer is no. Sorting is off for the reason
12
- * the custom columns' is — the value is on the row, and a sort over the
13
- * loaded window would look like a sort over the collection. Filtering is
14
- * the list's to switch on, through `CRM_NEXT_ACTIVITY_FILTER_FIELD`.
11
+ * planned", and to both the answer is no. The grid's own sort is off — a
12
+ * sort over the loaded window would look like a sort over the collection —
13
+ * and the list sorts the header instead, on its query, through its
14
+ * `columnSort` (AGL-3680). Filtering is the list's to switch on, through
15
+ * `CRM_NEXT_ACTIVITY_FILTER_FIELD`.
15
16
  */
16
17
  export declare function nextActivityColumn(nowMs: number): GridColDef;
17
18
  export { CRM_NEXT_ACTIVITY_FILTER_FIELD, CRM_NEXT_ACTIVITY_FILTER_HEADER, } from '../model/crm-next-activity';
@@ -27,10 +27,11 @@ import { CRM_NEXT_ACTIVITY_FIELD, readNextTaskAtMs } from "../model/crm-next-act
27
27
  * color, today emphasized — because it IS that date, seen from the record.
28
28
  * A dash for a record with nothing scheduled, and the same dash for one
29
29
  * written before the field existed: the reader's question is "is anything
30
- * planned", and to both the answer is no. Sorting is off for the reason
31
- * the custom columns' is — the value is on the row, and a sort over the
32
- * loaded window would look like a sort over the collection. Filtering is
33
- * the list's to switch on, through `CRM_NEXT_ACTIVITY_FILTER_FIELD`.
30
+ * planned", and to both the answer is no. The grid's own sort is off — a
31
+ * sort over the loaded window would look like a sort over the collection —
32
+ * and the list sorts the header instead, on its query, through its
33
+ * `columnSort` (AGL-3680). Filtering is the list's to switch on, through
34
+ * `CRM_NEXT_ACTIVITY_FILTER_FIELD`.
34
35
  */ export function nextActivityColumn(nowMs) {
35
36
  return {
36
37
  field: CRM_NEXT_ACTIVITY_FIELD,