@aglyn/plugins-crm 1.0.0-beta.199 → 1.0.0-beta.200

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 (105) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/companies-section.d.ts +6 -6
  3. package/src/lib/components/companies-section.js +54 -84
  4. package/src/lib/components/companies-section.js.map +1 -1
  5. package/src/lib/components/company-edit-drawer.js +17 -4
  6. package/src/lib/components/company-edit-drawer.js.map +1 -1
  7. package/src/lib/components/company-picker.js +6 -2
  8. package/src/lib/components/company-picker.js.map +1 -1
  9. package/src/lib/components/contact-detail-page.js +14 -2
  10. package/src/lib/components/contact-detail-page.js.map +1 -1
  11. package/src/lib/components/contact-list-columns.d.ts +3 -5
  12. package/src/lib/components/contact-list-columns.js +6 -8
  13. package/src/lib/components/contact-list-columns.js.map +1 -1
  14. package/src/lib/components/contacts-bulk-bar.js +10 -1
  15. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  16. package/src/lib/components/contacts-section.js +115 -163
  17. package/src/lib/components/contacts-section.js.map +1 -1
  18. package/src/lib/components/crm-next-activity-column.d.ts +4 -2
  19. package/src/lib/components/crm-next-activity-column.js +6 -3
  20. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  21. package/src/lib/components/deal-edit-drawer.js +8 -4
  22. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  23. package/src/lib/components/deals-section.d.ts +2 -2
  24. package/src/lib/components/deals-section.js +59 -84
  25. package/src/lib/components/deals-section.js.map +1 -1
  26. package/src/lib/components/lead-campaigns-card.js +11 -3
  27. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  28. package/src/lib/components/lead-properties-card.js +9 -1
  29. package/src/lib/components/lead-properties-card.js.map +1 -1
  30. package/src/lib/components/leads-bulk-bar.js +16 -3
  31. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  32. package/src/lib/components/leads-section.d.ts +2 -1
  33. package/src/lib/components/leads-section.js +110 -148
  34. package/src/lib/components/leads-section.js.map +1 -1
  35. package/src/lib/components/tasks-section.js +128 -121
  36. package/src/lib/components/tasks-section.js.map +1 -1
  37. package/src/lib/constants/company-filters.d.ts +13 -0
  38. package/src/lib/constants/company-filters.js +47 -17
  39. package/src/lib/constants/company-filters.js.map +1 -1
  40. package/src/lib/constants/contact-filters.d.ts +61 -10
  41. package/src/lib/constants/contact-filters.js +373 -141
  42. package/src/lib/constants/contact-filters.js.map +1 -1
  43. package/src/lib/constants/deal-filters.d.ts +44 -0
  44. package/src/lib/constants/deal-filters.js +78 -0
  45. package/src/lib/constants/deal-filters.js.map +1 -0
  46. package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
  47. package/src/lib/hooks/use-crm-list-query.js +182 -0
  48. package/src/lib/hooks/use-crm-list-query.js.map +1 -0
  49. package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
  50. package/src/lib/hooks/use-crm-tasks.js +44 -42
  51. package/src/lib/hooks/use-crm-tasks.js.map +1 -1
  52. package/src/lib/hooks/use-deals.d.ts +2 -34
  53. package/src/lib/hooks/use-deals.js +3 -62
  54. package/src/lib/hooks/use-deals.js.map +1 -1
  55. package/src/lib/model/crm-list-query.d.ts +91 -0
  56. package/src/lib/model/crm-list-query.js +182 -0
  57. package/src/lib/model/crm-list-query.js.map +1 -0
  58. package/src/lib/model/lead-filters.d.ts +49 -43
  59. package/src/lib/model/lead-filters.js +194 -88
  60. package/src/lib/model/lead-filters.js.map +1 -1
  61. package/src/lib/model/list-fields-api.d.ts +26 -0
  62. package/src/lib/model/list-fields-api.js +47 -0
  63. package/src/lib/model/list-fields-api.js.map +1 -0
  64. package/src/lib/model/list-fields.d.ts +47 -0
  65. package/src/lib/model/list-fields.js +39 -0
  66. package/src/lib/model/list-fields.js.map +1 -0
  67. package/src/lib/model/task-views.d.ts +15 -10
  68. package/src/lib/model/task-views.js +68 -9
  69. package/src/lib/model/task-views.js.map +1 -1
  70. package/src/lib/server/companies-import.js +3 -3
  71. package/src/lib/server/companies-import.js.map +1 -1
  72. package/src/lib/server/company-delete.js +3 -1
  73. package/src/lib/server/company-delete.js.map +1 -1
  74. package/src/lib/server/consent-group-participant.js +4 -0
  75. package/src/lib/server/consent-group-participant.js.map +1 -1
  76. package/src/lib/server/contact-update.js +14 -8
  77. package/src/lib/server/contact-update.js.map +1 -1
  78. package/src/lib/server/contacts-import.js +4 -2
  79. package/src/lib/server/contacts-import.js.map +1 -1
  80. package/src/lib/server/deals-import.js +4 -2
  81. package/src/lib/server/deals-import.js.map +1 -1
  82. package/src/lib/server/erase-person.js +2 -1
  83. package/src/lib/server/erase-person.js.map +1 -1
  84. package/src/lib/server/lead-create.js +4 -1
  85. package/src/lib/server/lead-create.js.map +1 -1
  86. package/src/lib/server/lead-source-values.js +9 -5
  87. package/src/lib/server/lead-source-values.js.map +1 -1
  88. package/src/lib/server/leads-import.js +4 -1
  89. package/src/lib/server/leads-import.js.map +1 -1
  90. package/src/lib/server/list-fields-routes.d.ts +29 -0
  91. package/src/lib/server/list-fields-routes.js +85 -0
  92. package/src/lib/server/list-fields-routes.js.map +1 -0
  93. package/src/lib/server/record-email-state.js +5 -2
  94. package/src/lib/server/record-email-state.js.map +1 -1
  95. package/src/lib/server/record-timeline.js +3 -2
  96. package/src/lib/server/record-timeline.js.map +1 -1
  97. package/src/lib/server/task-routes.js +6 -3
  98. package/src/lib/server/task-routes.js.map +1 -1
  99. package/src/lib/server/tasks-import.js +5 -2
  100. package/src/lib/server/tasks-import.js.map +1 -1
  101. package/src/lib/server.js +10 -1
  102. package/src/lib/server.js.map +1 -1
  103. package/src/lib/hooks/use-org-leads.d.ts +0 -50
  104. package/src/lib/hooks/use-org-leads.js +0 -104
  105. package/src/lib/hooks/use-org-leads.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-edit-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n type AglynPostalAddress,\n createResourceUid,\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n type CrmCompany,\n pluginDocsHelp,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, HelpTip, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n deleteField,\n doc,\n serverTimestamp,\n setDoc,\n updateDoc,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport {\n type CompanyDraft,\n companyDraftFields,\n companyDraftFrom,\n EMPTY_COMPANY_DRAFT,\n} from '../model/companies'\nimport {\n type CrmCustomDraft,\n crmCustomDraftDocument,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmSitePicker } from './crm-site-picker'\n\nexport interface CompanyEditDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the drawer is opened under, or `null` at the organization\n * level (AGL-2630), where a NEW company asks which site captures it and\n * an edit keeps the company's own.\n */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n /**\n * The company being EDITED, or nothing to create one.\n *\n * The two modes differ in what the fields start as and in which write\n * runs: an edit opens on the stored values and updates the document, a\n * create opens empty and stamps the scope, provenance and timestamps a new\n * record must carry.\n */\n company?: (Partial<CrmCompany> & { $id: string }) | null\n /**\n * Whether the stored values the edit opened on can be trusted.\n *\n * Every optional field is written on every save — a blank one is a\n * DELETE — so a form seeded from a cached read would write the cache's\n * blanks over the server's values. The card that holds the listen reports\n * its own `fromCache` and `status`; the drawer refuses to save on a seed\n * the server never confirmed, and says so.\n */\n seed?: { fromCache: boolean; unreadable: boolean }\n /**\n * The team, for the owner picker.\n *\n * Handed in rather than fetched here, because the surface opening this\n * drawer already holds the roster to name owners in its rows, and one\n * fetch per surface is the bargain.\n */\n members: OrgMemberOptions\n /** Called with the record's id after a successful write. */\n onSaved: (companyId: string) => void\n}\n\nconst ADDRESS_FIELDS: ReadonlyArray<{\n key: keyof AglynPostalAddress\n label: string\n}> = [\n { key: 'line1', label: 'Address line 1' },\n { key: 'line2', label: 'Address line 2' },\n { key: 'city', label: 'City' },\n { key: 'state', label: 'State or region' },\n { key: 'postalCode', label: 'Postal code' },\n { key: 'country', label: 'Country (two-letter code)' },\n]\n\n/**\n * ONE COMPANY, created or edited (AGL-2597).\n *\n * A drawer rather than a form above the list, and the same drawer for both\n * jobs: a company is eight fields and an address, which is too much to wedge\n * over a table and exactly the same set whether the record exists yet. The\n * list opens it empty; the company's page opens it on the record.\n *\n * Nothing is written per keystroke. The draft is held here and turned into a\n * document by `companyDraftFields` on Save, which is where a domain typed as\n * `acme` or a phone number with no country code is refused — as a message\n * beside the button, with the typed values left where they are.\n */\nexport function CompanyEditDrawer(props: CompanyEditDrawerProps) {\n const { open, onClose, hostId, org, company, seed, members, onSaved } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { scope, createTokens, createHostId } = useCrmScope({ hostId, org })\n // The feed the act is logged in is the level it was PERFORMED at\n // (AGL-2738): this site's under a site, the organization's at the org\n // hub. `createHostId` is where the record is STAMPED, which at the hub is\n // a site the reader picked and may hold no role on — a different question\n // from where the line belongs, and a gate the record write never faced.\n const logActivity = useCrmActivityLogger(hostId)\n\n const [draft, setDraft] = useState<CompanyDraft>(EMPTY_COMPANY_DRAFT)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState('')\n /*\n * THE CUSTOM FIELDS (AGL-2661): the org's company definitions, and the\n * keys the reader has touched over what the company stores. Saved as the\n * difference — see `crm-custom-draft` — beside the fixed fields, in the\n * same write, so a company is never half-saved.\n */\n const fields = useContactFieldDefinitions(scope?.[1] ?? null, 'company')\n const storedCustom = useMemo(() => company?.custom ?? {}, [company?.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n /*\n * THE RECORDS BAND (AGL-2611), read only for a CREATE: a company is a\n * record of the band the contacts list is banded by, and on an org at a\n * full hard band this drawer refuses the way `upsertHostContact` refuses a\n * capture — same number, same sentence. An edit is not a record and pays\n * for no aggregate. Only a SETTLED refusal refuses: three reads that have\n * not answered are not a full band.\n */\n const records = useCrmRecordsQuota(open && !company ? scope : null, org)\n const bandFull = !company && records.ready && !records.quota.allowed\n\n /*\n * Seeded when the drawer OPENS, and not on every render of the card\n * behind it: the company listen is live, so re-seeding on each snapshot\n * would take a field away from whoever is typing in it — including on the\n * snapshot their own save produces. The create case seeds the owner to the\n * person filing the record, which is the answer nine times in ten and one\n * click to change.\n */\n const companyId = company?.$id\n useEffect(() => {\n if (!open) return\n setDraft(\n company\n ? companyDraftFrom(company)\n : { ...EMPTY_COMPANY_DRAFT, ownerUid: user?.uid ?? '' },\n )\n setCustom({})\n setError('')\n // The stored fields are read once per opening; `company` is a new object\n // on every snapshot and must not re-seed.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, companyId])\n\n const patch = useCallback(\n (field: keyof Omit<CompanyDraft, 'address'>, value: string) =>\n setDraft((current) => ({ ...current, [field]: value })),\n [],\n )\n const patchAddress = useCallback(\n (field: keyof AglynPostalAddress, value: string) =>\n setDraft((current) => ({\n ...current,\n address: { ...current.address, [field]: value },\n })),\n [],\n )\n\n const handleSave = useCallback(async () => {\n if (busy || !scope) return\n const result = companyDraftFields(draft)\n if (result.ok === false) return setError(result.error)\n const missing = crmCustomDraftMissingRequired(\n fields.active,\n storedCustom,\n custom,\n company ? 'edit' : 'create',\n )\n if (missing.length) return setError(`${missing.join(', ')} is required.`)\n const customDocument = crmCustomDraftDocument(custom)\n setError('')\n setBusy(true)\n try {\n if (company) {\n const ref = doc(\n firestore,\n scope[0],\n scope[1],\n CRM_COLLECTIONS.companies,\n company.$id,\n )\n // The guard WRAPS the write — an early return is a shape you can\n // keep while losing the protection.\n const verdict = await writeGuardedBySeed(\n {\n subject: 'company',\n unreadable: seed?.unreadable ?? false,\n fromCache: seed?.fromCache ?? false,\n },\n async () => {\n await updateDoc(ref, {\n ...result.set,\n // A blank optional field is a DELETE on an edit, or the old\n // domain would stay stored and keep matching people by email.\n ...Object.fromEntries(\n result.cleared.map((field) => [field, deleteField()]),\n ),\n // The custom keys that changed, merged into the stored map.\n ...crmCustomDraftWrites(storedCustom, custom),\n updatedAt: serverTimestamp(),\n })\n },\n )\n if (!verdict.ok) return setError(verdict.message)\n enqueueSnackbar('Company saved', { variant: 'success', persist: false })\n onSaved(company.$id)\n } else {\n if (bandFull) return setError(CRM_RECORDS_BAND_FULL_MESSAGE)\n const id = createResourceUid()\n // Held by the button until the capturing site is known; checked\n // again here because a callback can outlive the render that held it.\n if (!createHostId) return\n await setDoc(\n doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id),\n {\n ...result.set,\n ...(customDocument ? { custom: customDocument } : {}),\n /*\n * The scope every CRM creator stamps: the whole org when the org\n * shares by default, and otherwise the sites that present as one\n * sender. Without it the record is seen by NOBODY — an absent\n * `visibleTo` matches no reader's predicate — and the rules\n * refuse a scoped member creating outside their own tokens.\n */\n visibleTo: [...createTokens],\n // Provenance: the site whose console filed it, never rewritten.\n hostId: createHostId,\n createdByUid: user?.uid ?? '',\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n },\n )\n // Setup → Activity shows CRM work (AGL-2622): the company is org\n // data, but the act happened in this site's console and belongs in\n // its feed. Creation only — an edit is a save the card reports.\n logActivity('Added company', { type: 'company', id, name: draft.name.trim() })\n enqueueSnackbar(`Company \"${draft.name.trim()}\" created`, {\n variant: 'success',\n persist: false,\n })\n onSaved(id)\n }\n onClose()\n } catch (caught) {\n console.error(caught)\n setError('The company could not be saved.')\n } finally {\n setBusy(false)\n }\n }, [\n busy,\n scope,\n draft,\n company,\n bandFull,\n firestore,\n seed,\n createTokens,\n createHostId,\n user,\n logActivity,\n enqueueSnackbar,\n onSaved,\n onClose,\n fields.active,\n storedCustom,\n custom,\n ])\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={onClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>{'close drawer'}</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {company ? 'Edit company' : 'New company'}\n </Typography>\n <HelpTip\n {...pluginDocsHelp('companies', { anchor: '#create-a-company' })}\n />\n </>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {/*\n First, because the scope of everything below follows from it: at\n the organization level a new company names the site that\n captures it. Nothing under a site, and nothing on an edit — a\n company keeps the site it was made on (AGL-2630).\n */}\n {company ? null : (\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText=\"The site this company belongs to — it decides which of your sites may see it.\"\n />\n )}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The organization behind one or more of your contacts. The ' +\n 'domain is what suggests this company for a person from their ' +\n 'email address, so enter the bare hostname.'}\n </Typography>\n {bandFull ? (\n <Alert severity=\"warning\">{CRM_RECORDS_BAND_FULL_MESSAGE}</Alert>\n ) : null}\n <TextField\n size=\"small\"\n label=\"Name\"\n required\n value={draft.name}\n onChange={(event) => patch('name', event.target.value)}\n autoFocus\n />\n <TextField\n size=\"small\"\n label=\"Domain\"\n placeholder=\"example.com\"\n value={draft.domain}\n onChange={(event) => patch('domain', event.target.value)}\n />\n <TextField\n size=\"small\"\n label=\"Website\"\n placeholder=\"https://www.example.com\"\n value={draft.website}\n onChange={(event) => patch('website', event.target.value)}\n />\n <TextField\n size=\"small\"\n label=\"Phone\"\n placeholder=\"+1 512 555 0123\"\n value={draft.phone}\n onChange={(event) => patch('phone', event.target.value)}\n />\n <TextField\n size=\"small\"\n label=\"Industry\"\n value={draft.industry}\n onChange={(event) => patch('industry', event.target.value)}\n />\n {/*\n The owner is chosen from the team, never typed: the value is a\n uid, and a picker that is still loading says so rather than\n offering an empty list that reads as \"nobody can own this\".\n */}\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={draft.ownerUid}\n disabled={!members.ready}\n helperText={\n !members.ready\n ? 'Loading your team…'\n : (members.error ?? 'The team member responsible for this account.')\n }\n onChange={(event) => patch('ownerUid', event.target.value)}\n >\n <MenuItem value=\"\">{'Nobody yet'}</MenuItem>\n {draft.ownerUid &&\n !members.options.some((option) => option.uid === draft.ownerUid) ? (\n <MenuItem value={draft.ownerUid}>\n {members.labelFor(draft.ownerUid)}\n </MenuItem>\n ) : null}\n {members.options.map((option) => (\n <MenuItem key={option.uid} value={option.uid}>\n {crmMemberPickerLabel(option)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"enterprise, west\"\n helperText=\"Comma-separated, lowercased — the same tags the bulk bar adds\"\n value={draft.tags}\n onChange={(event) => patch('tags', event.target.value)}\n />\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n {ADDRESS_FIELDS.map((field) => (\n <TextField\n key={field.key}\n size=\"small\"\n label={field.label}\n value={draft.address[field.key] ?? ''}\n onChange={(event) => patchAddress(field.key, event.target.value)}\n />\n ))}\n <TextField\n size=\"small\"\n label=\"Notes\"\n value={draft.notes}\n onChange={(event) => patch('notes', event.target.value)}\n multiline\n minRows={3}\n />\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'Custom fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={busy}\n />\n ))}\n </>\n ) : null}\n {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n // A new company waits for its capturing site.\n disabled={busy || !scope || !draft.name.trim() || (!company && !createHostId)}\n onClick={() => void handleSave()}\n >\n {busy\n ? 'Saving…'\n : company\n ? 'Save company'\n : 'Create company'}\n </Button>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nCompanyEditDrawer.displayName = 'CompanyEditDrawer'\n\nexport default CompanyEditDrawer\n"],"names":["createResourceUid","CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","pluginDocsHelp","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","HelpTip","MdiIcon","SrOnly","NavigationDrawerComponent","useSnackbar","useFirestore","useUser","writeGuardedBySeed","Alert","Button","IconButton","MenuItem","Stack","TextField","Typography","deleteField","doc","serverTimestamp","setDoc","updateDoc","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","useCrmRecordsQuota","useCrmScope","companyDraftFields","companyDraftFrom","EMPTY_COMPANY_DRAFT","crmCustomDraftDocument","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","CrmSitePicker","ADDRESS_FIELDS","key","label","CompanyEditDrawer","props","members","open","onClose","hostId","org","company","seed","onSaved","firestore","data","user","enqueueSnackbar","scope","createTokens","createHostId","logActivity","draft","setDraft","busy","setBusy","error","setError","fields","storedCustom","custom","setCustom","records","bandFull","ready","quota","allowed","companyId","$id","ownerUid","uid","patch","field","value","current","patchAddress","address","handleSave","result","ok","missing","active","length","join","customDocument","ref","companies","verdict","subject","unreadable","fromCache","set","Object","fromEntries","cleared","map","updatedAt","message","variant","persist","id","visibleTo","createdByUid","createdAt","type","name","trim","caught","console","anchor","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","gutterY","spacing","disabled","helperText","severity","size","required","onChange","event","target","autoFocus","placeholder","domain","website","phone","industry","select","options","some","option","labelFor","tags","notes","multiline","minRows","definition","direction","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-edit-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n type AglynPostalAddress,\n createResourceUid,\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n type CrmCompany,\n crmNewRecordListFields,\n pluginDocsHelp,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, HelpTip, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n deleteField,\n doc,\n serverTimestamp,\n setDoc,\n updateDoc,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport {\n type CompanyDraft,\n companyDraftFields,\n companyDraftFrom,\n EMPTY_COMPANY_DRAFT,\n} from '../model/companies'\nimport {\n type CrmCustomDraft,\n crmCustomDraftDocument,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmSitePicker } from './crm-site-picker'\nimport { CRM_CLIENT_SEARCH_FIELDS, crmClientListFields } from '../model/crm-list-query'\n\nexport interface CompanyEditDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the drawer is opened under, or `null` at the organization\n * level (AGL-2630), where a NEW company asks which site captures it and\n * an edit keeps the company's own.\n */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n /**\n * The company being EDITED, or nothing to create one.\n *\n * The two modes differ in what the fields start as and in which write\n * runs: an edit opens on the stored values and updates the document, a\n * create opens empty and stamps the scope, provenance and timestamps a new\n * record must carry.\n */\n company?: (Partial<CrmCompany> & { $id: string }) | null\n /**\n * Whether the stored values the edit opened on can be trusted.\n *\n * Every optional field is written on every save — a blank one is a\n * DELETE — so a form seeded from a cached read would write the cache's\n * blanks over the server's values. The card that holds the listen reports\n * its own `fromCache` and `status`; the drawer refuses to save on a seed\n * the server never confirmed, and says so.\n */\n seed?: { fromCache: boolean; unreadable: boolean }\n /**\n * The team, for the owner picker.\n *\n * Handed in rather than fetched here, because the surface opening this\n * drawer already holds the roster to name owners in its rows, and one\n * fetch per surface is the bargain.\n */\n members: OrgMemberOptions\n /** Called with the record's id after a successful write. */\n onSaved: (companyId: string) => void\n}\n\nconst ADDRESS_FIELDS: ReadonlyArray<{\n key: keyof AglynPostalAddress\n label: string\n}> = [\n { key: 'line1', label: 'Address line 1' },\n { key: 'line2', label: 'Address line 2' },\n { key: 'city', label: 'City' },\n { key: 'state', label: 'State or region' },\n { key: 'postalCode', label: 'Postal code' },\n { key: 'country', label: 'Country (two-letter code)' },\n]\n\n/**\n * ONE COMPANY, created or edited (AGL-2597).\n *\n * A drawer rather than a form above the list, and the same drawer for both\n * jobs: a company is eight fields and an address, which is too much to wedge\n * over a table and exactly the same set whether the record exists yet. The\n * list opens it empty; the company's page opens it on the record.\n *\n * Nothing is written per keystroke. The draft is held here and turned into a\n * document by `companyDraftFields` on Save, which is where a domain typed as\n * `acme` or a phone number with no country code is refused — as a message\n * beside the button, with the typed values left where they are.\n */\nexport function CompanyEditDrawer(props: CompanyEditDrawerProps) {\n const { open, onClose, hostId, org, company, seed, members, onSaved } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { scope, createTokens, createHostId } = useCrmScope({ hostId, org })\n // The feed the act is logged in is the level it was PERFORMED at\n // (AGL-2738): this site's under a site, the organization's at the org\n // hub. `createHostId` is where the record is STAMPED, which at the hub is\n // a site the reader picked and may hold no role on — a different question\n // from where the line belongs, and a gate the record write never faced.\n const logActivity = useCrmActivityLogger(hostId)\n\n const [draft, setDraft] = useState<CompanyDraft>(EMPTY_COMPANY_DRAFT)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState('')\n /*\n * THE CUSTOM FIELDS (AGL-2661): the org's company definitions, and the\n * keys the reader has touched over what the company stores. Saved as the\n * difference — see `crm-custom-draft` — beside the fixed fields, in the\n * same write, so a company is never half-saved.\n */\n const fields = useContactFieldDefinitions(scope?.[1] ?? null, 'company')\n const storedCustom = useMemo(() => company?.custom ?? {}, [company?.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n /*\n * THE RECORDS BAND (AGL-2611), read only for a CREATE: a company is a\n * record of the band the contacts list is banded by, and on an org at a\n * full hard band this drawer refuses the way `upsertHostContact` refuses a\n * capture — same number, same sentence. An edit is not a record and pays\n * for no aggregate. Only a SETTLED refusal refuses: three reads that have\n * not answered are not a full band.\n */\n const records = useCrmRecordsQuota(open && !company ? scope : null, org)\n const bandFull = !company && records.ready && !records.quota.allowed\n\n /*\n * Seeded when the drawer OPENS, and not on every render of the card\n * behind it: the company listen is live, so re-seeding on each snapshot\n * would take a field away from whoever is typing in it — including on the\n * snapshot their own save produces. The create case seeds the owner to the\n * person filing the record, which is the answer nine times in ten and one\n * click to change.\n */\n const companyId = company?.$id\n useEffect(() => {\n if (!open) return\n setDraft(\n company\n ? companyDraftFrom(company)\n : { ...EMPTY_COMPANY_DRAFT, ownerUid: user?.uid ?? '' },\n )\n setCustom({})\n setError('')\n // The stored fields are read once per opening; `company` is a new object\n // on every snapshot and must not re-seed.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, companyId])\n\n const patch = useCallback(\n (field: keyof Omit<CompanyDraft, 'address'>, value: string) =>\n setDraft((current) => ({ ...current, [field]: value })),\n [],\n )\n const patchAddress = useCallback(\n (field: keyof AglynPostalAddress, value: string) =>\n setDraft((current) => ({\n ...current,\n address: { ...current.address, [field]: value },\n })),\n [],\n )\n\n const handleSave = useCallback(async () => {\n if (busy || !scope) return\n const result = companyDraftFields(draft)\n if (result.ok === false) return setError(result.error)\n const missing = crmCustomDraftMissingRequired(\n fields.active,\n storedCustom,\n custom,\n company ? 'edit' : 'create',\n )\n if (missing.length) return setError(`${missing.join(', ')} is required.`)\n const customDocument = crmCustomDraftDocument(custom)\n setError('')\n setBusy(true)\n try {\n if (company) {\n const ref = doc(\n firestore,\n scope[0],\n scope[1],\n CRM_COLLECTIONS.companies,\n company.$id,\n )\n // The guard WRAPS the write — an early return is a shape you can\n // keep while losing the protection.\n const verdict = await writeGuardedBySeed(\n {\n subject: 'company',\n unreadable: seed?.unreadable ?? false,\n fromCache: seed?.fromCache ?? false,\n },\n async () => {\n const cleared = Object.fromEntries(\n result.cleared.map((field) => [field, deleteField()]),\n )\n // The stored record the list fields are computed from: the row\n // without the id the listener stamped on it.\n const { $id: _rowId, ...stored } = company\n const searchFields = crmClientListFields(\n 'companies',\n stored,\n { ...result.set, ...cleared },\n CRM_CLIENT_SEARCH_FIELDS,\n )\n await updateDoc(ref, {\n ...result.set,\n // A blank optional field is a DELETE on an edit, or the old\n // domain would stay stored and keep matching people by email.\n ...cleared,\n // The name and domain are what the Companies list searches\n // (AGL-3321), from the listener's row with the edit over it.\n ...searchFields,\n // The custom keys that changed, merged into the stored map.\n ...crmCustomDraftWrites(storedCustom, custom),\n updatedAt: serverTimestamp(),\n })\n },\n )\n if (!verdict.ok) return setError(verdict.message)\n enqueueSnackbar('Company saved', { variant: 'success', persist: false })\n onSaved(company.$id)\n } else {\n if (bandFull) return setError(CRM_RECORDS_BAND_FULL_MESSAGE)\n const id = createResourceUid()\n // Held by the button until the capturing site is known; checked\n // again here because a callback can outlive the render that held it.\n if (!createHostId) return\n await setDoc(\n doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id),\n {\n ...result.set,\n ...(customDocument ? { custom: customDocument } : {}),\n // What the Companies list searches and filters by (AGL-3321).\n ...crmNewRecordListFields('companies', {\n ...result.set,\n visibleTo: [...createTokens],\n }),\n /*\n * The scope every CRM creator stamps: the whole org when the org\n * shares by default, and otherwise the sites that present as one\n * sender. Without it the record is seen by NOBODY — an absent\n * `visibleTo` matches no reader's predicate — and the rules\n * refuse a scoped member creating outside their own tokens.\n */\n visibleTo: [...createTokens],\n // Provenance: the site whose console filed it, never rewritten.\n hostId: createHostId,\n createdByUid: user?.uid ?? '',\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n },\n )\n // Setup → Activity shows CRM work (AGL-2622): the company is org\n // data, but the act happened in this site's console and belongs in\n // its feed. Creation only — an edit is a save the card reports.\n logActivity('Added company', { type: 'company', id, name: draft.name.trim() })\n enqueueSnackbar(`Company \"${draft.name.trim()}\" created`, {\n variant: 'success',\n persist: false,\n })\n onSaved(id)\n }\n onClose()\n } catch (caught) {\n console.error(caught)\n setError('The company could not be saved.')\n } finally {\n setBusy(false)\n }\n }, [\n busy,\n scope,\n draft,\n company,\n bandFull,\n firestore,\n seed,\n createTokens,\n createHostId,\n user,\n logActivity,\n enqueueSnackbar,\n onSaved,\n onClose,\n fields.active,\n storedCustom,\n custom,\n ])\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={onClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>{'close drawer'}</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {company ? 'Edit company' : 'New company'}\n </Typography>\n <HelpTip\n {...pluginDocsHelp('companies', { anchor: '#create-a-company' })}\n />\n </>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {/*\n First, because the scope of everything below follows from it: at\n the organization level a new company names the site that\n captures it. Nothing under a site, and nothing on an edit — a\n company keeps the site it was made on (AGL-2630).\n */}\n {company ? null : (\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText=\"The site this company belongs to — it decides which of your sites may see it.\"\n />\n )}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The organization behind one or more of your contacts. The ' +\n 'domain is what suggests this company for a person from their ' +\n 'email address, so enter the bare hostname.'}\n </Typography>\n {bandFull ? (\n <Alert severity=\"warning\">{CRM_RECORDS_BAND_FULL_MESSAGE}</Alert>\n ) : null}\n <TextField\n size=\"small\"\n label=\"Name\"\n required\n value={draft.name}\n onChange={(event) => patch('name', event.target.value)}\n autoFocus\n />\n <TextField\n size=\"small\"\n label=\"Domain\"\n placeholder=\"example.com\"\n value={draft.domain}\n onChange={(event) => patch('domain', event.target.value)}\n />\n <TextField\n size=\"small\"\n label=\"Website\"\n placeholder=\"https://www.example.com\"\n value={draft.website}\n onChange={(event) => patch('website', event.target.value)}\n />\n <TextField\n size=\"small\"\n label=\"Phone\"\n placeholder=\"+1 512 555 0123\"\n value={draft.phone}\n onChange={(event) => patch('phone', event.target.value)}\n />\n <TextField\n size=\"small\"\n label=\"Industry\"\n value={draft.industry}\n onChange={(event) => patch('industry', event.target.value)}\n />\n {/*\n The owner is chosen from the team, never typed: the value is a\n uid, and a picker that is still loading says so rather than\n offering an empty list that reads as \"nobody can own this\".\n */}\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={draft.ownerUid}\n disabled={!members.ready}\n helperText={\n !members.ready\n ? 'Loading your team…'\n : (members.error ?? 'The team member responsible for this account.')\n }\n onChange={(event) => patch('ownerUid', event.target.value)}\n >\n <MenuItem value=\"\">{'Nobody yet'}</MenuItem>\n {draft.ownerUid &&\n !members.options.some((option) => option.uid === draft.ownerUid) ? (\n <MenuItem value={draft.ownerUid}>\n {members.labelFor(draft.ownerUid)}\n </MenuItem>\n ) : null}\n {members.options.map((option) => (\n <MenuItem key={option.uid} value={option.uid}>\n {crmMemberPickerLabel(option)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"enterprise, west\"\n helperText=\"Comma-separated, lowercased — the same tags the bulk bar adds\"\n value={draft.tags}\n onChange={(event) => patch('tags', event.target.value)}\n />\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n {ADDRESS_FIELDS.map((field) => (\n <TextField\n key={field.key}\n size=\"small\"\n label={field.label}\n value={draft.address[field.key] ?? ''}\n onChange={(event) => patchAddress(field.key, event.target.value)}\n />\n ))}\n <TextField\n size=\"small\"\n label=\"Notes\"\n value={draft.notes}\n onChange={(event) => patch('notes', event.target.value)}\n multiline\n minRows={3}\n />\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'Custom fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={busy}\n />\n ))}\n </>\n ) : null}\n {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n // A new company waits for its capturing site.\n disabled={busy || !scope || !draft.name.trim() || (!company && !createHostId)}\n onClick={() => void handleSave()}\n >\n {busy\n ? 'Saving…'\n : company\n ? 'Save company'\n : 'Create company'}\n </Button>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nCompanyEditDrawer.displayName = 'CompanyEditDrawer'\n\nexport default CompanyEditDrawer\n"],"names":["createResourceUid","CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","crmNewRecordListFields","pluginDocsHelp","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","HelpTip","MdiIcon","SrOnly","NavigationDrawerComponent","useSnackbar","useFirestore","useUser","writeGuardedBySeed","Alert","Button","IconButton","MenuItem","Stack","TextField","Typography","deleteField","doc","serverTimestamp","setDoc","updateDoc","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","useCrmRecordsQuota","useCrmScope","companyDraftFields","companyDraftFrom","EMPTY_COMPANY_DRAFT","crmCustomDraftDocument","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","CrmSitePicker","CRM_CLIENT_SEARCH_FIELDS","crmClientListFields","ADDRESS_FIELDS","key","label","CompanyEditDrawer","props","members","open","onClose","hostId","org","company","seed","onSaved","firestore","data","user","enqueueSnackbar","scope","createTokens","createHostId","logActivity","draft","setDraft","busy","setBusy","error","setError","fields","storedCustom","custom","setCustom","records","bandFull","ready","quota","allowed","companyId","$id","ownerUid","uid","patch","field","value","current","patchAddress","address","handleSave","result","ok","missing","active","length","join","customDocument","ref","companies","verdict","subject","unreadable","fromCache","cleared","Object","fromEntries","map","_rowId","stored","searchFields","set","updatedAt","message","variant","persist","id","visibleTo","createdByUid","createdAt","type","name","trim","caught","console","anchor","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","gutterY","spacing","disabled","helperText","severity","size","required","onChange","event","target","autoFocus","placeholder","domain","website","phone","industry","select","options","some","option","labelFor","tags","notes","multiline","minRows","definition","direction","displayName"],"mappings":"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"}
@@ -16,7 +16,7 @@
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
- import { createResourceUid, CRM_COLLECTIONS, CRM_RECORDS_BAND_FULL_MESSAGE } from "@aglyn/aglyn";
19
+ import { createResourceUid, CRM_COLLECTIONS, CRM_RECORDS_BAND_FULL_MESSAGE, crmNewRecordListFields } from "@aglyn/aglyn";
20
20
  import { useFirestore, useFirestoreCollection, useUser } from "@aglyn/tenant-feature-instance";
21
21
  import { Autocomplete, Button, createFilterOptions, Stack, TextField, Typography } from "@mui/material";
22
22
  import { collection, doc, limit, orderBy, query, serverTimestamp, setDoc } from "firebase/firestore";
@@ -120,7 +120,11 @@ export { suggestCompanyForEmail };
120
120
  const quota = await readCrmRecordsQuota(firestore, scope, org);
121
121
  if (!quota.allowed) throw new Error(CRM_RECORDS_BAND_FULL_MESSAGE);
122
122
  const id = createResourceUid();
123
- await setDoc(doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id), _extends({}, result.set, {
123
+ await setDoc(doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id), _extends({}, result.set, crmNewRecordListFields('companies', _extends({}, result.set, {
124
+ visibleTo: [
125
+ ...createTokens
126
+ ]
127
+ })), {
124
128
  visibleTo: [
125
129
  ...createTokens
126
130
  ],
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-picker.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n createResourceUid,\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n} from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Autocomplete,\n Button,\n createFilterOptions,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { readCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport { crmVisibleToClause, useCrmScope } from '../hooks/use-crm-scope'\nimport {\n type CompanyOption,\n companyDraftFields,\n EMPTY_COMPANY_DRAFT,\n suggestCompanyForEmail,\n} from '../model/companies'\n\nexport { suggestCompanyForEmail, type CompanyOption }\n\n/**\n * How many companies the picker lists.\n *\n * A ceiling and not a page: somebody choosing a company knows which one, and\n * a workspace past two hundred accounts finds it from the company list's own\n * search and links the person from there. Bounded because this read happens\n * on a contact's page, where the company list is a control and not the\n * subject.\n */\nexport const COMPANY_OPTIONS_LIMIT = 200\n\nexport interface CompanyOptions {\n options: CompanyOption[]\n /** The listen has answered at least once. */\n ready: boolean\n /** The ceiling cut the list, so a company can exist and not be offered. */\n truncated: boolean\n}\n\n/**\n * The companies this viewer may link a contact to, alphabetically\n * (AGL-2597).\n *\n * Read under the same `visibleTo` predicate the company list runs, because\n * the rules refuse a listen without it for a scoped member — and because a\n * picker offering a company the reader could not open would let them file a\n * person under a record they cannot see. Ordered by `nameLower`, the key\n * every company write stores, so the picker reads as a list of names rather\n * than as document-id order dressed up.\n *\n * `enabled` gates the listen: a contact's properties card has one picker and\n * a merchant who opened the record to read a note must not pay for the\n * company list.\n */\nexport function useCompanyOptions(props: {\n /** The site whose console is reading, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n enabled?: boolean\n}): CompanyOptions {\n const { hostId, org, enabled = true } = props\n const firestore = useFirestore()\n const { scope, visibleTo } = useCrmScope({ hostId, org })\n const { data, status } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n enabled && scope\n ? query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n limit(COMPANY_OPTIONS_LIMIT + 1),\n )\n : null,\n [firestore, scope, visibleTo, enabled],\n { idField: '$id' },\n )\n const options = useMemo<CompanyOption[]>(\n () =>\n (data ?? []).slice(0, COMPANY_OPTIONS_LIMIT).map((row) => ({\n id: String(row['$id']),\n name: String(row['name'] ?? ''),\n domain: typeof row['domain'] === 'string' ? row['domain'] : null,\n })),\n [data],\n )\n return {\n options,\n ready: Boolean(scope) && status !== 'loading',\n truncated: (data?.length ?? 0) > COMPANY_OPTIONS_LIMIT,\n }\n}\n\n/**\n * Create a company by name, answering the option the picker then selects.\n * Rejects with the sentence to show when the name cannot be stored.\n */\nexport type CreateCompany = (name: string) => Promise<CompanyOption>\n\n/**\n * The write behind \"Create “Acme”\" in the picker (AGL-2613).\n *\n * The same document the company drawer writes for a create — the search\n * keys, the scope every CRM creator stamps, the provenance — with only the\n * name filled in, because that is all the picker asked for: the domain, the\n * address and the rest are the company page's to add. The person filing it\n * owns it, as the drawer seeds too. `null` until the org has resolved, so a\n * picker offered before the scope is known offers no create.\n *\n * A company is a record of the band the drawers refuse on (AGL-2644): an\n * org at a full hard band is refused here with the drawers' sentence, and\n * the picker shows it beneath the field the way it shows any create that\n * failed. Measured on the click and not on the mount — see\n * `readCrmRecordsQuota` — and never for an edit, because linking an\n * existing company adds no record.\n */\nexport function useCreateCompany(props: {\n /** The site whose console is creating, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}): CreateCompany | null {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n // The provenance and the scope both come from the site the record is\n // captured by — the mounted one, or at the organization level the one the\n // reader picked (AGL-2630). No site picked, no create offered.\n const { scope, createTokens, createHostId } = useCrmScope({ hostId, org })\n const uid = user?.uid ?? ''\n return useMemo<CreateCompany | null>(() => {\n if (!scope || !createHostId) return null\n return async (name) => {\n const result = companyDraftFields({\n ...EMPTY_COMPANY_DRAFT,\n name,\n ownerUid: uid,\n })\n if (result.ok === false) throw new Error(result.error)\n const quota = await readCrmRecordsQuota(firestore, scope, org)\n if (!quota.allowed) throw new Error(CRM_RECORDS_BAND_FULL_MESSAGE)\n const id = createResourceUid()\n await setDoc(\n doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id),\n {\n ...result.set,\n visibleTo: [...createTokens],\n hostId: createHostId,\n createdByUid: uid,\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n },\n )\n return { id, name: String(result.set['name']), domain: null }\n }\n }, [scope, createTokens, firestore, createHostId, uid, org])\n}\n\n/** The sentinel id of the \"Create …\" row the list grows when nothing matches. */\nconst CREATE_OPTION_ID = '__create__'\n\ntype PickerOption = CompanyOption | { id: typeof CREATE_OPTION_ID; name: string; create: string }\n\nconst isCreateOption = (\n option: PickerOption,\n): option is Extract<PickerOption, { create: string }> => 'create' in option\n\nconst filterCompanies = createFilterOptions<PickerOption>({\n stringify: (option) =>\n isCreateOption(option)\n ? option.create\n : option.domain\n ? `${option.name} ${option.domain}`\n : option.name,\n})\n\nconst optionLabel = (option: PickerOption): string =>\n isCreateOption(option)\n ? `Create “${option.create}”`\n : option.domain\n ? `${option.name} · ${option.domain}`\n : option.name\n\nexport interface CompanyPickerProps {\n options: readonly CompanyOption[]\n /** The linked company's id, or `null` for none. */\n value: string | null\n /**\n * The choice, with the option behind it when the list — or a create — can\n * name it, so a caller can echo the company's name onto the contact\n * without a second lookup. `null` for a company the list does not carry.\n */\n onChange: (companyId: string | null, company: CompanyOption | null) => void\n /**\n * Create a company from the typed name and select it. Absent — or `null`\n * while the scope is unresolved — the picker offers no create row.\n */\n onCreate?: CreateCompany | null\n label?: string\n helperText?: string\n disabled?: boolean\n /**\n * The contact's email address, when the caller has one. A company whose\n * domain matches it is offered as a suggestion beneath the field — a\n * suggestion and never an automatic link, because the person who typed\n * `@acme.com` may be a contractor, a customer of Acme's, or Acme itself.\n */\n email?: string | null\n /**\n * The company name the record carries as free text while no company is\n * linked — an import, or a save from before the picker existed. Offered\n * beneath the field as the company to link or create, so the label a\n * merchant already typed is one click from becoming the record it names.\n */\n fallbackName?: string | null\n /** The option list has answered; until then the field says it is loading. */\n ready?: boolean\n /** The list was cut at the ceiling, so the field says to narrow by typing. */\n truncated?: boolean\n}\n\n/**\n * One company, chosen from the list a contact may be filed under\n * (AGL-2597, AGL-2613).\n *\n * A search over the loaded list rather than free text, because the value is\n * a document id and a typed name is how two records called \"Acme\" come to\n * exist. Typing narrows by name and domain; a name nothing matches grows the\n * list by one row, \"Create “…”\", which writes the company and selects it —\n * so the person filing a contact never has to leave the form to make the\n * account it belongs to. The clear control is the unlink: a saved contact\n * with no company reads as a decision rather than a field somebody skipped.\n *\n * A stored id the list does not carry — a company past the picker's ceiling,\n * or one this viewer cannot see — still renders as SOMETHING selected, or\n * the field would read empty for a contact that has a company and the next\n * save would silently unlink them.\n */\nexport function CompanyPicker(props: CompanyPickerProps) {\n const {\n options,\n value,\n onChange,\n onCreate,\n label = 'Company',\n helperText,\n disabled,\n email,\n fallbackName,\n ready = true,\n truncated,\n } = props\n const [creating, setCreating] = useState(false)\n const [createError, setCreateError] = useState('')\n\n const suggestion = useMemo(\n () => (email ? suggestCompanyForEmail(email, options) : null),\n [email, options],\n )\n const fallback = useMemo(() => {\n const name = String(fallbackName ?? '').trim()\n if (value || !name) return null\n const lower = name.toLowerCase()\n return {\n name,\n match: options.find((option) => option.name.trim().toLowerCase() === lower) ?? null,\n }\n }, [fallbackName, options, value])\n\n const selected = useMemo<PickerOption | null>(() => {\n if (!value) return null\n return (\n options.find((option) => option.id === value) ?? {\n id: value,\n name: `Company ${value.slice(-6)}`,\n domain: null,\n }\n )\n }, [options, value])\n /*\n * The list the control searches: every option, plus the placeholder for a\n * stored id the list does not carry, so the selected value is always one\n * of the options and the control never reports it as invalid.\n */\n const listed = useMemo<PickerOption[]>(\n () =>\n selected && !options.some((option) => option.id === selected.id)\n ? [selected, ...options]\n : [...options],\n [options, selected],\n )\n\n const create = useCallback(\n async (name: string) => {\n if (!onCreate) return\n setCreating(true)\n setCreateError('')\n try {\n const company = await onCreate(name)\n onChange(company.id, company)\n } catch (error) {\n setCreateError(\n error instanceof Error && error.message\n ? error.message\n : 'The company could not be created.',\n )\n } finally {\n setCreating(false)\n }\n },\n [onCreate, onChange],\n )\n\n const busy = disabled || !ready || creating\n return (\n <Stack spacing={0.5}>\n <Autocomplete<PickerOption, false, false, false>\n size=\"small\"\n options={listed}\n value={selected}\n disabled={busy}\n loading={!ready}\n getOptionLabel={optionLabel}\n isOptionEqualToValue={(option, current) => option.id === current.id}\n filterOptions={(candidates, state) => {\n const filtered = filterCompanies(candidates, state)\n const typed = state.inputValue.trim()\n const exact = typed.toLowerCase()\n const named = candidates.some(\n (option) =>\n !isCreateOption(option) && option.name.trim().toLowerCase() === exact,\n )\n if (onCreate && typed && !named) {\n filtered.push({ id: CREATE_OPTION_ID, name: typed, create: typed })\n }\n return filtered\n }}\n onChange={(_event, next) => {\n if (!next) return onChange(null, null)\n if (isCreateOption(next)) return void create(next.create)\n onChange(next.id, next)\n }}\n renderInput={(params) => (\n <TextField\n {...params}\n label={label}\n error={Boolean(createError)}\n helperText={\n !ready\n ? 'Loading companies…'\n : creating\n ? 'Creating the company…'\n : createError ||\n helperText ||\n (truncated\n ? `The first ${COMPANY_OPTIONS_LIMIT} by name — type to narrow, or create one.`\n : undefined)\n }\n />\n )}\n />\n {suggestion && suggestion.id !== value ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Suggested from the email address: ${suggestion.name}`}\n </Typography>\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => onChange(suggestion.id, suggestion)}\n >\n {'Use'}\n </Button>\n </Stack>\n ) : fallback ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {fallback.match\n ? `Recorded as “${fallback.name}” — a company by that name exists.`\n : `Recorded as “${fallback.name}” without a link.`}\n </Typography>\n {fallback.match ? (\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => onChange(fallback.match?.id ?? null, fallback.match)}\n >\n {'Use'}\n </Button>\n ) : onCreate ? (\n <Button size=\"small\" disabled={busy} onClick={() => void create(fallback.name)}>\n {`Create “${fallback.name}”`}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n )\n}\nCompanyPicker.displayName = 'CompanyPicker'\n\nexport default CompanyPicker\n"],"names":["createResourceUid","CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","useFirestore","useFirestoreCollection","useUser","Autocomplete","Button","createFilterOptions","Stack","TextField","Typography","collection","doc","limit","orderBy","query","serverTimestamp","setDoc","useCallback","useMemo","useState","readCrmRecordsQuota","crmVisibleToClause","useCrmScope","companyDraftFields","EMPTY_COMPANY_DRAFT","suggestCompanyForEmail","COMPANY_OPTIONS_LIMIT","useCompanyOptions","props","hostId","org","enabled","firestore","scope","visibleTo","data","status","companies","idField","options","slice","map","row","id","String","name","domain","ready","Boolean","truncated","length","useCreateCompany","user","createTokens","createHostId","uid","result","ownerUid","ok","Error","error","quota","allowed","set","createdByUid","createdAt","updatedAt","CREATE_OPTION_ID","isCreateOption","option","filterCompanies","stringify","create","optionLabel","CompanyPicker","value","onChange","onCreate","label","helperText","disabled","email","fallbackName","creating","setCreating","createError","setCreateError","suggestion","fallback","trim","lower","toLowerCase","match","find","selected","listed","some","company","message","busy","spacing","size","loading","getOptionLabel","isOptionEqualToValue","current","filterOptions","candidates","state","filtered","typed","inputValue","exact","named","push","_event","next","renderInput","params","undefined","direction","sx","alignItems","variant","color","onClick","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-picker.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n createResourceUid,\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n crmNewRecordListFields,\n} from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Autocomplete,\n Button,\n createFilterOptions,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { readCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport { crmVisibleToClause, useCrmScope } from '../hooks/use-crm-scope'\nimport {\n type CompanyOption,\n companyDraftFields,\n EMPTY_COMPANY_DRAFT,\n suggestCompanyForEmail,\n} from '../model/companies'\n\nexport { suggestCompanyForEmail, type CompanyOption }\n\n/**\n * How many companies the picker lists.\n *\n * A ceiling and not a page: somebody choosing a company knows which one, and\n * a workspace past two hundred accounts finds it from the company list's own\n * search and links the person from there. Bounded because this read happens\n * on a contact's page, where the company list is a control and not the\n * subject.\n */\nexport const COMPANY_OPTIONS_LIMIT = 200\n\nexport interface CompanyOptions {\n options: CompanyOption[]\n /** The listen has answered at least once. */\n ready: boolean\n /** The ceiling cut the list, so a company can exist and not be offered. */\n truncated: boolean\n}\n\n/**\n * The companies this viewer may link a contact to, alphabetically\n * (AGL-2597).\n *\n * Read under the same `visibleTo` predicate the company list runs, because\n * the rules refuse a listen without it for a scoped member — and because a\n * picker offering a company the reader could not open would let them file a\n * person under a record they cannot see. Ordered by `nameLower`, the key\n * every company write stores, so the picker reads as a list of names rather\n * than as document-id order dressed up.\n *\n * `enabled` gates the listen: a contact's properties card has one picker and\n * a merchant who opened the record to read a note must not pay for the\n * company list.\n */\nexport function useCompanyOptions(props: {\n /** The site whose console is reading, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n enabled?: boolean\n}): CompanyOptions {\n const { hostId, org, enabled = true } = props\n const firestore = useFirestore()\n const { scope, visibleTo } = useCrmScope({ hostId, org })\n const { data, status } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n enabled && scope\n ? query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n limit(COMPANY_OPTIONS_LIMIT + 1),\n )\n : null,\n [firestore, scope, visibleTo, enabled],\n { idField: '$id' },\n )\n const options = useMemo<CompanyOption[]>(\n () =>\n (data ?? []).slice(0, COMPANY_OPTIONS_LIMIT).map((row) => ({\n id: String(row['$id']),\n name: String(row['name'] ?? ''),\n domain: typeof row['domain'] === 'string' ? row['domain'] : null,\n })),\n [data],\n )\n return {\n options,\n ready: Boolean(scope) && status !== 'loading',\n truncated: (data?.length ?? 0) > COMPANY_OPTIONS_LIMIT,\n }\n}\n\n/**\n * Create a company by name, answering the option the picker then selects.\n * Rejects with the sentence to show when the name cannot be stored.\n */\nexport type CreateCompany = (name: string) => Promise<CompanyOption>\n\n/**\n * The write behind \"Create “Acme”\" in the picker (AGL-2613).\n *\n * The same document the company drawer writes for a create — the search\n * keys, the scope every CRM creator stamps, the provenance — with only the\n * name filled in, because that is all the picker asked for: the domain, the\n * address and the rest are the company page's to add. The person filing it\n * owns it, as the drawer seeds too. `null` until the org has resolved, so a\n * picker offered before the scope is known offers no create.\n *\n * A company is a record of the band the drawers refuse on (AGL-2644): an\n * org at a full hard band is refused here with the drawers' sentence, and\n * the picker shows it beneath the field the way it shows any create that\n * failed. Measured on the click and not on the mount — see\n * `readCrmRecordsQuota` — and never for an edit, because linking an\n * existing company adds no record.\n */\nexport function useCreateCompany(props: {\n /** The site whose console is creating, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}): CreateCompany | null {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n // The provenance and the scope both come from the site the record is\n // captured by — the mounted one, or at the organization level the one the\n // reader picked (AGL-2630). No site picked, no create offered.\n const { scope, createTokens, createHostId } = useCrmScope({ hostId, org })\n const uid = user?.uid ?? ''\n return useMemo<CreateCompany | null>(() => {\n if (!scope || !createHostId) return null\n return async (name) => {\n const result = companyDraftFields({\n ...EMPTY_COMPANY_DRAFT,\n name,\n ownerUid: uid,\n })\n if (result.ok === false) throw new Error(result.error)\n const quota = await readCrmRecordsQuota(firestore, scope, org)\n if (!quota.allowed) throw new Error(CRM_RECORDS_BAND_FULL_MESSAGE)\n const id = createResourceUid()\n await setDoc(\n doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id),\n {\n ...result.set,\n // What the Companies list searches and filters by (AGL-3321).\n ...crmNewRecordListFields('companies', { ...result.set, visibleTo: [...createTokens] }),\n visibleTo: [...createTokens],\n hostId: createHostId,\n createdByUid: uid,\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n },\n )\n return { id, name: String(result.set['name']), domain: null }\n }\n }, [scope, createTokens, firestore, createHostId, uid, org])\n}\n\n/** The sentinel id of the \"Create …\" row the list grows when nothing matches. */\nconst CREATE_OPTION_ID = '__create__'\n\ntype PickerOption = CompanyOption | { id: typeof CREATE_OPTION_ID; name: string; create: string }\n\nconst isCreateOption = (\n option: PickerOption,\n): option is Extract<PickerOption, { create: string }> => 'create' in option\n\nconst filterCompanies = createFilterOptions<PickerOption>({\n stringify: (option) =>\n isCreateOption(option)\n ? option.create\n : option.domain\n ? `${option.name} ${option.domain}`\n : option.name,\n})\n\nconst optionLabel = (option: PickerOption): string =>\n isCreateOption(option)\n ? `Create “${option.create}”`\n : option.domain\n ? `${option.name} · ${option.domain}`\n : option.name\n\nexport interface CompanyPickerProps {\n options: readonly CompanyOption[]\n /** The linked company's id, or `null` for none. */\n value: string | null\n /**\n * The choice, with the option behind it when the list — or a create — can\n * name it, so a caller can echo the company's name onto the contact\n * without a second lookup. `null` for a company the list does not carry.\n */\n onChange: (companyId: string | null, company: CompanyOption | null) => void\n /**\n * Create a company from the typed name and select it. Absent — or `null`\n * while the scope is unresolved — the picker offers no create row.\n */\n onCreate?: CreateCompany | null\n label?: string\n helperText?: string\n disabled?: boolean\n /**\n * The contact's email address, when the caller has one. A company whose\n * domain matches it is offered as a suggestion beneath the field — a\n * suggestion and never an automatic link, because the person who typed\n * `@acme.com` may be a contractor, a customer of Acme's, or Acme itself.\n */\n email?: string | null\n /**\n * The company name the record carries as free text while no company is\n * linked — an import, or a save from before the picker existed. Offered\n * beneath the field as the company to link or create, so the label a\n * merchant already typed is one click from becoming the record it names.\n */\n fallbackName?: string | null\n /** The option list has answered; until then the field says it is loading. */\n ready?: boolean\n /** The list was cut at the ceiling, so the field says to narrow by typing. */\n truncated?: boolean\n}\n\n/**\n * One company, chosen from the list a contact may be filed under\n * (AGL-2597, AGL-2613).\n *\n * A search over the loaded list rather than free text, because the value is\n * a document id and a typed name is how two records called \"Acme\" come to\n * exist. Typing narrows by name and domain; a name nothing matches grows the\n * list by one row, \"Create “…”\", which writes the company and selects it —\n * so the person filing a contact never has to leave the form to make the\n * account it belongs to. The clear control is the unlink: a saved contact\n * with no company reads as a decision rather than a field somebody skipped.\n *\n * A stored id the list does not carry — a company past the picker's ceiling,\n * or one this viewer cannot see — still renders as SOMETHING selected, or\n * the field would read empty for a contact that has a company and the next\n * save would silently unlink them.\n */\nexport function CompanyPicker(props: CompanyPickerProps) {\n const {\n options,\n value,\n onChange,\n onCreate,\n label = 'Company',\n helperText,\n disabled,\n email,\n fallbackName,\n ready = true,\n truncated,\n } = props\n const [creating, setCreating] = useState(false)\n const [createError, setCreateError] = useState('')\n\n const suggestion = useMemo(\n () => (email ? suggestCompanyForEmail(email, options) : null),\n [email, options],\n )\n const fallback = useMemo(() => {\n const name = String(fallbackName ?? '').trim()\n if (value || !name) return null\n const lower = name.toLowerCase()\n return {\n name,\n match: options.find((option) => option.name.trim().toLowerCase() === lower) ?? null,\n }\n }, [fallbackName, options, value])\n\n const selected = useMemo<PickerOption | null>(() => {\n if (!value) return null\n return (\n options.find((option) => option.id === value) ?? {\n id: value,\n name: `Company ${value.slice(-6)}`,\n domain: null,\n }\n )\n }, [options, value])\n /*\n * The list the control searches: every option, plus the placeholder for a\n * stored id the list does not carry, so the selected value is always one\n * of the options and the control never reports it as invalid.\n */\n const listed = useMemo<PickerOption[]>(\n () =>\n selected && !options.some((option) => option.id === selected.id)\n ? [selected, ...options]\n : [...options],\n [options, selected],\n )\n\n const create = useCallback(\n async (name: string) => {\n if (!onCreate) return\n setCreating(true)\n setCreateError('')\n try {\n const company = await onCreate(name)\n onChange(company.id, company)\n } catch (error) {\n setCreateError(\n error instanceof Error && error.message\n ? error.message\n : 'The company could not be created.',\n )\n } finally {\n setCreating(false)\n }\n },\n [onCreate, onChange],\n )\n\n const busy = disabled || !ready || creating\n return (\n <Stack spacing={0.5}>\n <Autocomplete<PickerOption, false, false, false>\n size=\"small\"\n options={listed}\n value={selected}\n disabled={busy}\n loading={!ready}\n getOptionLabel={optionLabel}\n isOptionEqualToValue={(option, current) => option.id === current.id}\n filterOptions={(candidates, state) => {\n const filtered = filterCompanies(candidates, state)\n const typed = state.inputValue.trim()\n const exact = typed.toLowerCase()\n const named = candidates.some(\n (option) =>\n !isCreateOption(option) && option.name.trim().toLowerCase() === exact,\n )\n if (onCreate && typed && !named) {\n filtered.push({ id: CREATE_OPTION_ID, name: typed, create: typed })\n }\n return filtered\n }}\n onChange={(_event, next) => {\n if (!next) return onChange(null, null)\n if (isCreateOption(next)) return void create(next.create)\n onChange(next.id, next)\n }}\n renderInput={(params) => (\n <TextField\n {...params}\n label={label}\n error={Boolean(createError)}\n helperText={\n !ready\n ? 'Loading companies…'\n : creating\n ? 'Creating the company…'\n : createError ||\n helperText ||\n (truncated\n ? `The first ${COMPANY_OPTIONS_LIMIT} by name — type to narrow, or create one.`\n : undefined)\n }\n />\n )}\n />\n {suggestion && suggestion.id !== value ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Suggested from the email address: ${suggestion.name}`}\n </Typography>\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => onChange(suggestion.id, suggestion)}\n >\n {'Use'}\n </Button>\n </Stack>\n ) : fallback ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {fallback.match\n ? `Recorded as “${fallback.name}” — a company by that name exists.`\n : `Recorded as “${fallback.name}” without a link.`}\n </Typography>\n {fallback.match ? (\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => onChange(fallback.match?.id ?? null, fallback.match)}\n >\n {'Use'}\n </Button>\n ) : onCreate ? (\n <Button size=\"small\" disabled={busy} onClick={() => void create(fallback.name)}>\n {`Create “${fallback.name}”`}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n )\n}\nCompanyPicker.displayName = 'CompanyPicker'\n\nexport default CompanyPicker\n"],"names":["createResourceUid","CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","crmNewRecordListFields","useFirestore","useFirestoreCollection","useUser","Autocomplete","Button","createFilterOptions","Stack","TextField","Typography","collection","doc","limit","orderBy","query","serverTimestamp","setDoc","useCallback","useMemo","useState","readCrmRecordsQuota","crmVisibleToClause","useCrmScope","companyDraftFields","EMPTY_COMPANY_DRAFT","suggestCompanyForEmail","COMPANY_OPTIONS_LIMIT","useCompanyOptions","props","hostId","org","enabled","firestore","scope","visibleTo","data","status","companies","idField","options","slice","map","row","id","String","name","domain","ready","Boolean","truncated","length","useCreateCompany","user","createTokens","createHostId","uid","result","ownerUid","ok","Error","error","quota","allowed","set","createdByUid","createdAt","updatedAt","CREATE_OPTION_ID","isCreateOption","option","filterCompanies","stringify","create","optionLabel","CompanyPicker","value","onChange","onCreate","label","helperText","disabled","email","fallbackName","creating","setCreating","createError","setCreateError","suggestion","fallback","trim","lower","toLowerCase","match","find","selected","listed","some","company","message","busy","spacing","size","loading","getOptionLabel","isOptionEqualToValue","current","filterOptions","candidates","state","filtered","typed","inputValue","exact","named","push","_event","next","renderInput","params","undefined","direction","sx","alignItems","variant","color","onClick","displayName"],"mappings":"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"}
@@ -21,7 +21,9 @@ import { CONTACT_LIFECYCLE_STAGE_LABELS, pluginDocsHelp } from "@aglyn/aglyn";
21
21
  import { mdiDeleteOutline, mdiMerge } from "@aglyn/shared-data-mdi";
22
22
  import { AppLink, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
23
23
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
24
- import { useFirestore, useFirestoreDoc } from "@aglyn/tenant-feature-instance";
24
+ import { useFirestore, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
25
+ import { restampCrmListFields } from "../model/list-fields-api.js";
26
+ import { crmTaskCallScope } from "../model/task-routes.js";
25
27
  import { Stack, Tooltip, Typography } from "@mui/material";
26
28
  import { arrayRemove, deleteDoc, deleteField, doc, updateDoc } from "firebase/firestore";
27
29
  import { useParams, useRouter } from "next/navigation";
@@ -199,6 +201,7 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
199
201
  emailsHub,
200
202
  hostId
201
203
  ]);
204
+ const { data: user } = useUser();
202
205
  const handleRemove = useCallback(async ()=>{
203
206
  var _ref;
204
207
  if (!row || !scope) return;
@@ -225,6 +228,12 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
225
228
  capturedByHostIds: arrayRemove(...plan.removeHostIds),
226
229
  updatedAt: new Date()
227
230
  }));
231
+ // The facet and the scope just let go of are what the Contacts list
232
+ // filters and searches by; the rules keep a client from restamping
233
+ // them, so the route does (AGL-3321).
234
+ await restampCrmListFields(user, crmTaskCallScope(hostId, orgId), 'contacts', [
235
+ id
236
+ ]);
228
237
  }
229
238
  // `siteLabel` and not "this site": at the organization level the line
230
239
  // lands in a feed that spans every site, so it has to name the one.
@@ -257,7 +266,10 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
257
266
  routes,
258
267
  row,
259
268
  scope,
260
- siteLabel
269
+ siteLabel,
270
+ user,
271
+ hostId,
272
+ orgId
261
273
  ]);
262
274
  const overflowItems = [
263
275
  /*
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-detail-page.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 { CONTACT_LIFECYCLE_STAGE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiMerge } from '@aglyn/shared-data-mdi'\nimport { AppLink, MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport { Stack, Tooltip, Typography } from '@mui/material'\nimport {\n arrayRemove,\n deleteDoc,\n deleteField,\n doc,\n updateDoc,\n} from 'firebase/firestore'\nimport { useParams, useRouter } from 'next/navigation'\nimport { useCallback, useMemo } from 'react'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { CrmCreateSiteDefault, useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { contactPrimaryGroup, contactRecordFromDoc } from '../model/contact-record'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport ContactAssociationsCard from './contact-associations-card'\nimport ContactCustomFieldsCard from './contact-custom-fields-card'\nimport ContactDuplicatesCard from './contact-duplicates-card'\nimport ContactKnownByCard from './contact-known-by-card'\nimport ContactMergeDialog, { useContactMergeDialog } from './contact-merge-dialog'\nimport ContactPropertiesCard from './contact-properties-card'\nimport ContactTimelineCard from './contact-timeline-card'\nimport { AddToListButton } from './add-to-list-button'\nimport { useBookingDoor } from './crm-booking-zone'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport { ContactDealsCard } from './contact-deals-card'\nimport { CrmCallButton } from './crm-call-actions'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { CrmSuiteNotice, crmSuiteIncluded, crmSuiteLockedReason } from './crm-suite-lock'\nimport { useErasePersonAction } from './erase-person-action'\nimport RecordFilesCard from './record-files-card'\nimport { RecordTasksCard } from './record-tasks-card'\nimport { useEmailsHubPath } from './use-emails-hub-path'\nimport { useOrgMembers } from './use-org-members'\n\nconst contactDocsHelp = pluginDocsHelp('contacts', {\n anchor: '#the-contacts-page',\n excerpt:\n \"One person's record: the profile your team keeps on them, where they \" +\n 'came from, what they are filed under, and what the site recorded.',\n})\n\n/**\n * `/crm/contacts/{contactId}` — one person (AGL-2596).\n *\n * A ROUTE rather than the v1 drawer, because a person is the thing every\n * other CRM record points at: a deal, a task and an activity all name a\n * contact, and a link that opens a drawer on top of a list is not a link\n * anybody can paste.\n *\n * ## What this page reads\n *\n * ONE document, by id, through the scoped rules — a single-document get is\n * judged on that document's own `visibleTo`, so no `array-contains-any`\n * filter is needed here the way it is on the list. The row is then flattened\n * through the viewing group's facet ONCE (`contactRecordFromDoc`) and the\n * cards below read the flat record: none of them reaches into\n * `facets.{groupId}` for itself, which is what keeps a card from ever\n * showing another holder's notes.\n *\n * Beyond the document: the team roster (one request, for the owner picker)\n * and the site's campaigns (one listen, for the filing picker) — both paid\n * here because opening a record IS the ask for them, and neither is paid by\n * the list.\n *\n * ## The cards\n *\n * Each in its own file, so a card can grow without this page growing:\n * properties (the editable profile), the relationship (sources, attribution,\n * consent, campaign filing) and the timeline. Other CRM sections add a card\n * here by inserting one import and one line.\n *\n * ## Delete is a detach\n *\n * The row is shared by every site that has captured this person, so the\n * action in the overflow menu drops THIS group's facet, consent, attribution\n * and scope tokens, and deletes the document only when nobody else is left\n * holding it — `planContactDetach` does the counting off `visibleTo`, which\n * is what both enforcement layers evaluate, and the rules refuse a delete by\n * a caller who is not the sole holder. Not the erasure path: a privacy\n * erasure removes the person regardless of who else holds them.\n */\nexport function ContactDetailPage(props: CrmDetailPageProps) {\n const { hostId, org, id, basePath } = props\n const routes = crmRoutes(basePath)\n const router = useRouter()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The org root, the viewing group and what this viewer may list — the\n // merge search's and the duplicates query's one clause — from the one\n // scope hook (AGL-2614); at the organization level the group is `null`\n // and resolved per person below, and the listeners carry no clause\n // (AGL-2630).\n const {\n scope,\n orgId,\n consentGroup: viewingGroup,\n visibleTo,\n } = useCrmScope({ hostId, org })\n const mount = useCrmOrgMount()\n const merge = useContactMergeDialog()\n /*\n * THE CRM (AGL-2788). This page is the CRM's, and the shell mounts no CRM\n * page for a plan without it (AGL-2851). What locks below — owner, stage\n * and company; tasks, deals, files and custom fields; a logged call or\n * activity; one-to-one email; a merge — draws only for an org whose plan\n * lacks the CRM. The shell mounted this page once the org had settled, so\n * this is the plan, not a paint of the org still loading.\n */\n const suiteIncluded = crmSuiteIncluded(org)\n\n const {\n data: row,\n status,\n fromCache,\n } = useFirestoreDoc<Record<string, any>>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'contacts', id) : null),\n [firestore, scope, id],\n { idField: '$id' },\n )\n /*\n * The controller this page is showing. Under a site: the sites declared\n * to be one sender, or this site alone — resolved from the org document\n * the shell already passed, so it costs no read. At the ORGANIZATION\n * level: the person's own PRIMARY holder, the first capturing site's\n * group, so an org-wide member reads the profile that site keeps and an\n * edit goes back to the same facet; the \"Known by\" card says which other\n * sites hold one.\n */\n const consentGroup = useMemo(\n () => viewingGroup ?? contactPrimaryGroup(row, org as Record<string, unknown>),\n [viewingGroup, row, org],\n )\n /**\n * The SITE the page acts as, for what needs one — the lead lookup, the\n * campaign filing, the site's activity feed: the mounted site, or the\n * primary holder's, or nothing for a row no site has captured.\n */\n const siteHostId = hostId ?? (consentGroup.hostId || null)\n /*\n * The feed the act is logged in, decided by the level it was performed\n * at (AGL-2738) and so given the MOUNTED site rather than `siteHostId`:\n * at the org hub a client-direct append to the holder's own site would\n * face a gate the record write never had to pass.\n */\n const logActivity = useCrmActivityLogger(hostId)\n const record = useMemo(\n () => (row ? contactRecordFromDoc(row, consentGroup) : null),\n [row, consentGroup],\n )\n const notFound = Boolean(scope) && status !== 'loading' && !row\n /** How the site the delete detaches from is named — see `handleRemove`. */\n const siteLabel = hostId\n ? 'this site'\n : siteHostId\n ? (mount?.siteName(siteHostId) ?? siteHostId)\n : 'its site'\n\n // The roster, for the owner picker and the owner's name — read because a\n // record page is the one place both are shown.\n const members = useOrgMembers(orgId, { enabled: Boolean(record) })\n /*\n * Where this person's ORDERS are read (AGL-2622): the site's orders list,\n * narrowed to their address. The count on the header is the number; the\n * list is the rows. Built from the route params already in the URL, so\n * no document is read to draw a link, and absent off a site — the\n * org-level mount has no orders list to point at.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const ordersHref =\n params?.orgSlug && params?.host && record?.email\n ? Aglyn.siteRecordLinks({\n orgSlug: String(params.orgSlug),\n host: String(params.host),\n }).ordersByCustomer(record.email)\n : null\n /*\n * Where this person's BOOKINGS are read (AGL-2660): the site's Bookings\n * page narrowed to their address, under the same match rule the booking\n * row's own \"View in CRM\" uses. Offered only where the site has a booking\n * door — a site that never enabled Bookings should not carry the row. At\n * the organization level the capturing site's page, addressed through\n * the subdomain the mount knows it by.\n */\n const bookingDoor = useBookingDoor(siteHostId, org)\n const siteSlug = params?.host\n ? String(params.host)\n : siteHostId && mount\n ? mount.siteSubdomain(siteHostId)\n : null\n const bookingsHref =\n bookingDoor.open && params?.orgSlug && siteSlug && record?.email\n ? Aglyn.siteRecordLinks({\n orgSlug: String(params.orgSlug),\n host: siteSlug,\n }).bookingsByBooker(record.email)\n : null\n\n /*\n * Where a campaign entry on the timeline links to: the email's own report\n * on this site's Emails hub (AGL-2616). Only a campaign THIS site sent can\n * be addressed — a sibling site in the same consent group has an Emails\n * hub of its own under a subdomain this page does not know — so the\n * builder answers `null` for those and the entry draws unlinked.\n */\n const emailsHub = useEmailsHubPath()\n const campaignHref = useCallback(\n (email: Aglyn.ContactCampaignEmail) =>\n emailsHub && email.hostId === hostId\n ? `${emailsHub}/messages/${encodeURIComponent(email.campaignId)}`\n : null,\n [emailsHub, hostId],\n )\n\n const handleRemove = useCallback(async () => {\n if (!row || !scope) return\n const confirmed = await confirm({\n title: 'Delete this contact?',\n description:\n `\"${record?.email ?? id}\" is removed from ${siteLabel}'s ` +\n 'Contacts, along with its notes, tags and timeline. Other sites ' +\n 'that captured the same person keep their own records. Their form ' +\n 'submissions, orders, bookings, and membership records are ' +\n 'separate — delete those from their own pages if the request ' +\n 'covers them.',\n confirmationText: 'Delete contact',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n const ref = doc(firestore, scope[0], scope[1], 'contacts', id)\n const plan = Aglyn.planContactDetach(row, consentGroup)\n if (plan.action === 'delete') {\n await deleteDoc(ref)\n } else {\n await updateDoc(ref, {\n ...Object.fromEntries(plan.remove.map((path) => [path, deleteField()])),\n visibleTo: arrayRemove(...plan.removeTokens),\n capturedByHostIds: arrayRemove(...plan.removeHostIds),\n updatedAt: new Date(),\n })\n }\n // `siteLabel` and not \"this site\": at the organization level the line\n // lands in a feed that spans every site, so it has to name the one.\n logActivity(`Removed contact from ${siteLabel}`, {\n type: 'contact',\n id,\n name: record?.name || record?.email,\n })\n enqueueSnackbar(\n plan.action === 'delete'\n ? 'Contact deleted'\n : `Contact removed from ${siteLabel}`,\n { variant: 'success', persist: false },\n )\n router.push(routes.section('contacts'))\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [\n confirm,\n consentGroup,\n enqueueSnackbar,\n firestore,\n id,\n logActivity,\n record,\n router,\n routes,\n row,\n scope,\n siteLabel,\n ])\n\n const overflowItems: RowActionsMenuItem[] = [\n /*\n * Two records for one person become one (AGL-2625). Opened empty, to be\n * searched, with the OTHER record surviving: a reader on the record they\n * want gone says \"merge this into…\". The dialog lets them switch.\n */\n {\n key: 'merge',\n label: 'Merge into…',\n icon: <MdiIcon path={mdiMerge.path} size={0.8} />,\n disabled: !row || !suiteIncluded,\n disabledReason: suiteIncluded ? 'The contact has not loaded' : crmSuiteLockedReason(),\n onClick: () => merge.open(),\n },\n {\n key: 'remove',\n label: 'Delete contact',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !row,\n disabledReason: 'The contact has not loaded',\n onClick: () => void handleRemove(),\n },\n ]\n // The privacy erasure (AGL-2623): its item joins the overflow, its banner\n // sits under the header while a request waits, its dialog renders once.\n const erase = useErasePersonAction({\n // Filed as the site the page acts as — at the organization level the\n // person's primary holder (AGL-2630).\n hostId: siteHostId,\n orgId,\n subject: record ? { kind: 'contact', id, email: record.email } : null,\n requestedAtMs: Aglyn.readErasureRequestedAtMs(row),\n })\n\n if (notFound) {\n return (\n <CrmRecordHeader\n kind=\"Contact\"\n title={undefined}\n help={contactDocsHelp}\n backHref={routes.section('contacts')}\n backLabel=\"Back to contacts\"\n >\n {/*\n * Not \"no data\". A contact that cannot be read is a different\n * situation from one with an empty profile — it may have been\n * removed from this site, or be held only by a site this reader\n * cannot see.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This contact could not be loaded. It may have been removed from this site.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n\n return (\n // A create opened from this page — a task, an activity, a deal — files\n // under the site that captured this person unless the reader says\n // otherwise (AGL-2630).\n <CrmCreateSiteDefault hostId={siteHostId}>\n <Stack spacing={3}>\n <CrmRecordHeader\n kind=\"Contact\"\n title={record ? record.name || record.email : undefined}\n // The identity, and the other addresses a merge folded into it.\n subtitle={\n record\n ? [record.email, ...record.alternateEmails.map((email) => `also ${email}`)].join(' · ')\n : undefined\n }\n help={contactDocsHelp}\n backHref={routes.section('contacts')}\n backLabel=\"Back to contacts\"\n actions={\n record ? (\n <>\n <AddToListButton hostId={hostId} org={org} contactId={id} email={record.email} />\n {/* Dial the number, and log the call it was (AGL-2661). */}\n <CrmCallButton\n hostId={siteHostId}\n org={org}\n link={{ contactId: id }}\n phone={record.phone}\n suiteLocked={!suiteIncluded}\n />\n <CrmSendEmailButton\n hostId={siteHostId}\n org={org}\n contactId={id}\n email={record.email}\n name={record.name}\n emailState={record.emailState}\n suiteLocked={!suiteIncluded}\n />\n </>\n ) : null\n }\n menuItems={[...overflowItems, ...erase.menuItems]}\n // The booking door (AGL-2660): this site's services, or at the\n // organization level the capturing site's.\n booking={record ? { hostId: siteHostId, org, kind: 'contact', recordId: id } : undefined}\n loading={!record}\n chips={\n record ? (\n <>\n {/* The verdict on the address (AGL-3245), first: a bounce or a\n do-not-contact mark is what a person deciding to write must see. */}\n <CrmEmailStateChip state={record.emailState} />\n <CrmRecordChip\n label=\"Stage\"\n value={\n record.lifecycleStage\n ? CONTACT_LIFECYCLE_STAGE_LABELS[record.lifecycleStage]\n : undefined\n }\n />\n <CrmRecordChip\n label=\"Owner\"\n value={record.ownerUid ? members.memberName(record.ownerUid) : undefined}\n />\n {/*\n The last time they opened or clicked one of this site's\n campaigns (AGL-2616) — the relationship's pulse, beside the\n owner who keeps it.\n */}\n <CrmRecordChip\n label=\"Last engaged\"\n value={\n record.lastEmailEngagementAtMs ? (\n <Tooltip title={new Date(record.lastEmailEngagementAtMs).toLocaleString()}>\n <span>\n {Aglyn.activityTimeLabel(record.lastEmailEngagementAtMs, Date.now())}\n </span>\n </Tooltip>\n ) : undefined\n }\n />\n <CrmRecordChip\n label=\"Orders\"\n value={\n record.ordersCount > 0 ? (\n ordersHref ? (\n <AppLink href={ordersHref} color=\"inherit\" underline=\"hover\">\n {`${record.ordersCount.toLocaleString()} · $${(record.ltvCents / 100).toFixed(2)} lifetime`}\n </AppLink>\n ) : (\n `${record.ordersCount.toLocaleString()} · $${(record.ltvCents / 100).toFixed(2)} lifetime`\n )\n ) : undefined\n }\n />\n </>\n ) : null\n }\n >\n {erase.banner}\n </CrmRecordHeader>\n {record && row && scope ? (\n <>\n {suiteIncluded ? null : (\n <CrmSuiteNotice>\n {'Working this contact — the owner, stage and company, tasks, ' +\n 'deals, files and custom fields, logging a call or an activity, ' +\n 'one-to-one email and merging duplicates — is part of the CRM.'}\n </CrmSuiteNotice>\n )}\n {/*\n The cross-site fact, at the organization level only (AGL-2630):\n which sites know this person, and their consent for each. Under\n a site the page IS one site's view and the card would say so on\n every record.\n */}\n {/* What an assistant proposes about the person (AGL-2917), where the plan works a contact. */}\n {suiteIncluded ? (\n <CrmRecordInsightsZone\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"contact\"\n recordId={id}\n name={record.name || record.email}\n taskLink={{ contactId: id }}\n />\n ) : null}\n {mount ? <ContactKnownByCard row={row} contactId={id} org={org} /> : null}\n <ContactPropertiesCard\n hostId={hostId}\n org={org}\n record={record}\n orgId={orgId}\n consentGroup={consentGroup}\n seed={{ status, fromCache }}\n members={members}\n suiteLocked={!suiteIncluded}\n />\n <ContactAssociationsCard\n hostId={siteHostId}\n record={record}\n row={row}\n consentGroup={consentGroup}\n seed={{ status, fromCache }}\n basePath={basePath}\n bookingsHref={bookingsHref}\n />\n {/*\n On a plan without the CRM, the cards that only work a record —\n custom fields, deals, tasks, files — are named in the notice above\n rather than drawn as four upsells in a row.\n */}\n {suiteIncluded ? (\n <ContactCustomFieldsCard hostId={hostId} org={org} contactId={id} basePath={basePath} />\n ) : null}\n <ContactTimelineCard\n hostId={hostId}\n org={org}\n contactId={id}\n contact={row}\n campaignHref={campaignHref}\n suiteLocked={!suiteIncluded}\n />\n {suiteIncluded ? (\n <>\n <ContactDealsCard\n hostId={hostId}\n org={org}\n basePath={basePath}\n contactId={id}\n contactName={record.name || record.email}\n />\n <RecordTasksCard hostId={hostId} org={org} basePath={basePath} contactId={id} />\n {/*\n The files are the VIEWING holder's, like everything else on this\n page: the write is a facet path, so a contract one brand filed\n never appears on a sibling brand's copy of the person.\n */}\n <RecordFilesCard\n scope={scope}\n collection=\"contacts\"\n recordId={id}\n hostId={hostId}\n facetGroupId={consentGroup.groupId}\n mediaIds={record.mediaIds}\n topic=\"contactRecord\"\n />\n </>\n ) : null}\n <ContactDuplicatesCard\n current={{ id, doc: row }}\n scope={scope}\n consentGroup={consentGroup}\n visibleTo={visibleTo}\n basePath={basePath}\n onMerge={(candidate) => merge.open({ other: candidate, keep: 'current' })}\n suiteLocked={!suiteIncluded}\n />\n <ContactMergeDialog\n {...merge.state}\n onClose={merge.close}\n hostId={siteHostId}\n current={{ id, doc: row }}\n scope={scope}\n consentGroup={consentGroup}\n visibleTo={visibleTo}\n memberName={members.memberName}\n // The page's own record was folded away: its address now opens\n // the survivor, which is where the reader is taken.\n onMerged={(survivorId) => {\n if (survivorId !== id) router.replace(routes.contact(survivorId))\n }}\n />\n </>\n ) : null}\n {erase.dialog}\n </Stack>\n </CrmCreateSiteDefault>\n )\n}\nContactDetailPage.displayName = 'ContactDetailPage'\n\nexport default ContactDetailPage\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","pluginDocsHelp","mdiDeleteOutline","mdiMerge","AppLink","MdiIcon","useConfirmationContext","useSnackbar","useFirestore","useFirestoreDoc","Stack","Tooltip","Typography","arrayRemove","deleteDoc","deleteField","doc","updateDoc","useParams","useRouter","useCallback","useMemo","useCrmActivityLogger","CrmCreateSiteDefault","useCrmOrgMount","useCrmScope","contactPrimaryGroup","contactRecordFromDoc","crmRoutes","ContactAssociationsCard","ContactCustomFieldsCard","ContactDuplicatesCard","ContactKnownByCard","ContactMergeDialog","useContactMergeDialog","ContactPropertiesCard","ContactTimelineCard","AddToListButton","useBookingDoor","CrmSendEmailButton","ContactDealsCard","CrmCallButton","CrmEmailStateChip","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","CrmSuiteNotice","crmSuiteIncluded","crmSuiteLockedReason","useErasePersonAction","RecordFilesCard","RecordTasksCard","useEmailsHubPath","useOrgMembers","contactDocsHelp","anchor","excerpt","ContactDetailPage","props","hostId","org","id","basePath","routes","router","firestore","enqueueSnackbar","confirm","scope","orgId","consentGroup","viewingGroup","visibleTo","mount","merge","suiteIncluded","data","row","status","fromCache","idField","siteHostId","logActivity","record","notFound","Boolean","siteLabel","siteName","members","enabled","params","ordersHref","orgSlug","host","email","siteRecordLinks","String","ordersByCustomer","bookingDoor","siteSlug","siteSubdomain","bookingsHref","open","bookingsByBooker","emailsHub","campaignHref","encodeURIComponent","campaignId","handleRemove","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","ref","plan","planContactDetach","action","Object","fromEntries","remove","map","path","removeTokens","capturedByHostIds","removeHostIds","updatedAt","Date","type","name","variant","persist","push","section","error","console","allowDuplicate","overflowItems","key","label","icon","size","disabled","disabledReason","onClick","destructive","erase","subject","kind","requestedAtMs","readErasureRequestedAtMs","undefined","help","backHref","backLabel","spacing","subtitle","alternateEmails","join","actions","contactId","link","phone","suiteLocked","emailState","menuItems","booking","recordId","loading","chips","state","value","lifecycleStage","ownerUid","memberName","lastEmailEngagementAtMs","toLocaleString","span","activityTimeLabel","now","ordersCount","href","underline","ltvCents","toFixed","banner","taskLink","seed","contact","contactName","collection","facetGroupId","groupId","mediaIds","topic","current","onMerge","candidate","other","keep","onClose","close","onMerged","survivorId","replace","dialog","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-detail-page.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 { CONTACT_LIFECYCLE_STAGE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiMerge } from '@aglyn/shared-data-mdi'\nimport { AppLink, MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, useFirestoreDoc, useUser } from '@aglyn/tenant-feature-instance'\nimport { restampCrmListFields } from '../model/list-fields-api'\nimport { crmTaskCallScope } from '../model/task-routes'\nimport { Stack, Tooltip, Typography } from '@mui/material'\nimport {\n arrayRemove,\n deleteDoc,\n deleteField,\n doc,\n updateDoc,\n} from 'firebase/firestore'\nimport { useParams, useRouter } from 'next/navigation'\nimport { useCallback, useMemo } from 'react'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { CrmCreateSiteDefault, useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { contactPrimaryGroup, contactRecordFromDoc } from '../model/contact-record'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport ContactAssociationsCard from './contact-associations-card'\nimport ContactCustomFieldsCard from './contact-custom-fields-card'\nimport ContactDuplicatesCard from './contact-duplicates-card'\nimport ContactKnownByCard from './contact-known-by-card'\nimport ContactMergeDialog, { useContactMergeDialog } from './contact-merge-dialog'\nimport ContactPropertiesCard from './contact-properties-card'\nimport ContactTimelineCard from './contact-timeline-card'\nimport { AddToListButton } from './add-to-list-button'\nimport { useBookingDoor } from './crm-booking-zone'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport { ContactDealsCard } from './contact-deals-card'\nimport { CrmCallButton } from './crm-call-actions'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { CrmSuiteNotice, crmSuiteIncluded, crmSuiteLockedReason } from './crm-suite-lock'\nimport { useErasePersonAction } from './erase-person-action'\nimport RecordFilesCard from './record-files-card'\nimport { RecordTasksCard } from './record-tasks-card'\nimport { useEmailsHubPath } from './use-emails-hub-path'\nimport { useOrgMembers } from './use-org-members'\n\nconst contactDocsHelp = pluginDocsHelp('contacts', {\n anchor: '#the-contacts-page',\n excerpt:\n \"One person's record: the profile your team keeps on them, where they \" +\n 'came from, what they are filed under, and what the site recorded.',\n})\n\n/**\n * `/crm/contacts/{contactId}` — one person (AGL-2596).\n *\n * A ROUTE rather than the v1 drawer, because a person is the thing every\n * other CRM record points at: a deal, a task and an activity all name a\n * contact, and a link that opens a drawer on top of a list is not a link\n * anybody can paste.\n *\n * ## What this page reads\n *\n * ONE document, by id, through the scoped rules — a single-document get is\n * judged on that document's own `visibleTo`, so no `array-contains-any`\n * filter is needed here the way it is on the list. The row is then flattened\n * through the viewing group's facet ONCE (`contactRecordFromDoc`) and the\n * cards below read the flat record: none of them reaches into\n * `facets.{groupId}` for itself, which is what keeps a card from ever\n * showing another holder's notes.\n *\n * Beyond the document: the team roster (one request, for the owner picker)\n * and the site's campaigns (one listen, for the filing picker) — both paid\n * here because opening a record IS the ask for them, and neither is paid by\n * the list.\n *\n * ## The cards\n *\n * Each in its own file, so a card can grow without this page growing:\n * properties (the editable profile), the relationship (sources, attribution,\n * consent, campaign filing) and the timeline. Other CRM sections add a card\n * here by inserting one import and one line.\n *\n * ## Delete is a detach\n *\n * The row is shared by every site that has captured this person, so the\n * action in the overflow menu drops THIS group's facet, consent, attribution\n * and scope tokens, and deletes the document only when nobody else is left\n * holding it — `planContactDetach` does the counting off `visibleTo`, which\n * is what both enforcement layers evaluate, and the rules refuse a delete by\n * a caller who is not the sole holder. Not the erasure path: a privacy\n * erasure removes the person regardless of who else holds them.\n */\nexport function ContactDetailPage(props: CrmDetailPageProps) {\n const { hostId, org, id, basePath } = props\n const routes = crmRoutes(basePath)\n const router = useRouter()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The org root, the viewing group and what this viewer may list — the\n // merge search's and the duplicates query's one clause — from the one\n // scope hook (AGL-2614); at the organization level the group is `null`\n // and resolved per person below, and the listeners carry no clause\n // (AGL-2630).\n const {\n scope,\n orgId,\n consentGroup: viewingGroup,\n visibleTo,\n } = useCrmScope({ hostId, org })\n const mount = useCrmOrgMount()\n const merge = useContactMergeDialog()\n /*\n * THE CRM (AGL-2788). This page is the CRM's, and the shell mounts no CRM\n * page for a plan without it (AGL-2851). What locks below — owner, stage\n * and company; tasks, deals, files and custom fields; a logged call or\n * activity; one-to-one email; a merge — draws only for an org whose plan\n * lacks the CRM. The shell mounted this page once the org had settled, so\n * this is the plan, not a paint of the org still loading.\n */\n const suiteIncluded = crmSuiteIncluded(org)\n\n const {\n data: row,\n status,\n fromCache,\n } = useFirestoreDoc<Record<string, any>>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'contacts', id) : null),\n [firestore, scope, id],\n { idField: '$id' },\n )\n /*\n * The controller this page is showing. Under a site: the sites declared\n * to be one sender, or this site alone — resolved from the org document\n * the shell already passed, so it costs no read. At the ORGANIZATION\n * level: the person's own PRIMARY holder, the first capturing site's\n * group, so an org-wide member reads the profile that site keeps and an\n * edit goes back to the same facet; the \"Known by\" card says which other\n * sites hold one.\n */\n const consentGroup = useMemo(\n () => viewingGroup ?? contactPrimaryGroup(row, org as Record<string, unknown>),\n [viewingGroup, row, org],\n )\n /**\n * The SITE the page acts as, for what needs one — the lead lookup, the\n * campaign filing, the site's activity feed: the mounted site, or the\n * primary holder's, or nothing for a row no site has captured.\n */\n const siteHostId = hostId ?? (consentGroup.hostId || null)\n /*\n * The feed the act is logged in, decided by the level it was performed\n * at (AGL-2738) and so given the MOUNTED site rather than `siteHostId`:\n * at the org hub a client-direct append to the holder's own site would\n * face a gate the record write never had to pass.\n */\n const logActivity = useCrmActivityLogger(hostId)\n const record = useMemo(\n () => (row ? contactRecordFromDoc(row, consentGroup) : null),\n [row, consentGroup],\n )\n const notFound = Boolean(scope) && status !== 'loading' && !row\n /** How the site the delete detaches from is named — see `handleRemove`. */\n const siteLabel = hostId\n ? 'this site'\n : siteHostId\n ? (mount?.siteName(siteHostId) ?? siteHostId)\n : 'its site'\n\n // The roster, for the owner picker and the owner's name — read because a\n // record page is the one place both are shown.\n const members = useOrgMembers(orgId, { enabled: Boolean(record) })\n /*\n * Where this person's ORDERS are read (AGL-2622): the site's orders list,\n * narrowed to their address. The count on the header is the number; the\n * list is the rows. Built from the route params already in the URL, so\n * no document is read to draw a link, and absent off a site — the\n * org-level mount has no orders list to point at.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const ordersHref =\n params?.orgSlug && params?.host && record?.email\n ? Aglyn.siteRecordLinks({\n orgSlug: String(params.orgSlug),\n host: String(params.host),\n }).ordersByCustomer(record.email)\n : null\n /*\n * Where this person's BOOKINGS are read (AGL-2660): the site's Bookings\n * page narrowed to their address, under the same match rule the booking\n * row's own \"View in CRM\" uses. Offered only where the site has a booking\n * door — a site that never enabled Bookings should not carry the row. At\n * the organization level the capturing site's page, addressed through\n * the subdomain the mount knows it by.\n */\n const bookingDoor = useBookingDoor(siteHostId, org)\n const siteSlug = params?.host\n ? String(params.host)\n : siteHostId && mount\n ? mount.siteSubdomain(siteHostId)\n : null\n const bookingsHref =\n bookingDoor.open && params?.orgSlug && siteSlug && record?.email\n ? Aglyn.siteRecordLinks({\n orgSlug: String(params.orgSlug),\n host: siteSlug,\n }).bookingsByBooker(record.email)\n : null\n\n /*\n * Where a campaign entry on the timeline links to: the email's own report\n * on this site's Emails hub (AGL-2616). Only a campaign THIS site sent can\n * be addressed — a sibling site in the same consent group has an Emails\n * hub of its own under a subdomain this page does not know — so the\n * builder answers `null` for those and the entry draws unlinked.\n */\n const emailsHub = useEmailsHubPath()\n const campaignHref = useCallback(\n (email: Aglyn.ContactCampaignEmail) =>\n emailsHub && email.hostId === hostId\n ? `${emailsHub}/messages/${encodeURIComponent(email.campaignId)}`\n : null,\n [emailsHub, hostId],\n )\n\n const { data: user } = useUser()\n const handleRemove = useCallback(async () => {\n if (!row || !scope) return\n const confirmed = await confirm({\n title: 'Delete this contact?',\n description:\n `\"${record?.email ?? id}\" is removed from ${siteLabel}'s ` +\n 'Contacts, along with its notes, tags and timeline. Other sites ' +\n 'that captured the same person keep their own records. Their form ' +\n 'submissions, orders, bookings, and membership records are ' +\n 'separate — delete those from their own pages if the request ' +\n 'covers them.',\n confirmationText: 'Delete contact',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n const ref = doc(firestore, scope[0], scope[1], 'contacts', id)\n const plan = Aglyn.planContactDetach(row, consentGroup)\n if (plan.action === 'delete') {\n await deleteDoc(ref)\n } else {\n await updateDoc(ref, {\n ...Object.fromEntries(plan.remove.map((path) => [path, deleteField()])),\n visibleTo: arrayRemove(...plan.removeTokens),\n capturedByHostIds: arrayRemove(...plan.removeHostIds),\n updatedAt: new Date(),\n })\n // The facet and the scope just let go of are what the Contacts list\n // filters and searches by; the rules keep a client from restamping\n // them, so the route does (AGL-3321).\n await restampCrmListFields(user, crmTaskCallScope(hostId, orgId), 'contacts', [id])\n }\n // `siteLabel` and not \"this site\": at the organization level the line\n // lands in a feed that spans every site, so it has to name the one.\n logActivity(`Removed contact from ${siteLabel}`, {\n type: 'contact',\n id,\n name: record?.name || record?.email,\n })\n enqueueSnackbar(\n plan.action === 'delete'\n ? 'Contact deleted'\n : `Contact removed from ${siteLabel}`,\n { variant: 'success', persist: false },\n )\n router.push(routes.section('contacts'))\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [\n confirm,\n consentGroup,\n enqueueSnackbar,\n firestore,\n id,\n logActivity,\n record,\n router,\n routes,\n row,\n scope,\n siteLabel,\n user,\n hostId,\n orgId,\n ])\n\n const overflowItems: RowActionsMenuItem[] = [\n /*\n * Two records for one person become one (AGL-2625). Opened empty, to be\n * searched, with the OTHER record surviving: a reader on the record they\n * want gone says \"merge this into…\". The dialog lets them switch.\n */\n {\n key: 'merge',\n label: 'Merge into…',\n icon: <MdiIcon path={mdiMerge.path} size={0.8} />,\n disabled: !row || !suiteIncluded,\n disabledReason: suiteIncluded ? 'The contact has not loaded' : crmSuiteLockedReason(),\n onClick: () => merge.open(),\n },\n {\n key: 'remove',\n label: 'Delete contact',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !row,\n disabledReason: 'The contact has not loaded',\n onClick: () => void handleRemove(),\n },\n ]\n // The privacy erasure (AGL-2623): its item joins the overflow, its banner\n // sits under the header while a request waits, its dialog renders once.\n const erase = useErasePersonAction({\n // Filed as the site the page acts as — at the organization level the\n // person's primary holder (AGL-2630).\n hostId: siteHostId,\n orgId,\n subject: record ? { kind: 'contact', id, email: record.email } : null,\n requestedAtMs: Aglyn.readErasureRequestedAtMs(row),\n })\n\n if (notFound) {\n return (\n <CrmRecordHeader\n kind=\"Contact\"\n title={undefined}\n help={contactDocsHelp}\n backHref={routes.section('contacts')}\n backLabel=\"Back to contacts\"\n >\n {/*\n * Not \"no data\". A contact that cannot be read is a different\n * situation from one with an empty profile — it may have been\n * removed from this site, or be held only by a site this reader\n * cannot see.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This contact could not be loaded. It may have been removed from this site.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n\n return (\n // A create opened from this page — a task, an activity, a deal — files\n // under the site that captured this person unless the reader says\n // otherwise (AGL-2630).\n <CrmCreateSiteDefault hostId={siteHostId}>\n <Stack spacing={3}>\n <CrmRecordHeader\n kind=\"Contact\"\n title={record ? record.name || record.email : undefined}\n // The identity, and the other addresses a merge folded into it.\n subtitle={\n record\n ? [record.email, ...record.alternateEmails.map((email) => `also ${email}`)].join(' · ')\n : undefined\n }\n help={contactDocsHelp}\n backHref={routes.section('contacts')}\n backLabel=\"Back to contacts\"\n actions={\n record ? (\n <>\n <AddToListButton hostId={hostId} org={org} contactId={id} email={record.email} />\n {/* Dial the number, and log the call it was (AGL-2661). */}\n <CrmCallButton\n hostId={siteHostId}\n org={org}\n link={{ contactId: id }}\n phone={record.phone}\n suiteLocked={!suiteIncluded}\n />\n <CrmSendEmailButton\n hostId={siteHostId}\n org={org}\n contactId={id}\n email={record.email}\n name={record.name}\n emailState={record.emailState}\n suiteLocked={!suiteIncluded}\n />\n </>\n ) : null\n }\n menuItems={[...overflowItems, ...erase.menuItems]}\n // The booking door (AGL-2660): this site's services, or at the\n // organization level the capturing site's.\n booking={record ? { hostId: siteHostId, org, kind: 'contact', recordId: id } : undefined}\n loading={!record}\n chips={\n record ? (\n <>\n {/* The verdict on the address (AGL-3245), first: a bounce or a\n do-not-contact mark is what a person deciding to write must see. */}\n <CrmEmailStateChip state={record.emailState} />\n <CrmRecordChip\n label=\"Stage\"\n value={\n record.lifecycleStage\n ? CONTACT_LIFECYCLE_STAGE_LABELS[record.lifecycleStage]\n : undefined\n }\n />\n <CrmRecordChip\n label=\"Owner\"\n value={record.ownerUid ? members.memberName(record.ownerUid) : undefined}\n />\n {/*\n The last time they opened or clicked one of this site's\n campaigns (AGL-2616) — the relationship's pulse, beside the\n owner who keeps it.\n */}\n <CrmRecordChip\n label=\"Last engaged\"\n value={\n record.lastEmailEngagementAtMs ? (\n <Tooltip title={new Date(record.lastEmailEngagementAtMs).toLocaleString()}>\n <span>\n {Aglyn.activityTimeLabel(record.lastEmailEngagementAtMs, Date.now())}\n </span>\n </Tooltip>\n ) : undefined\n }\n />\n <CrmRecordChip\n label=\"Orders\"\n value={\n record.ordersCount > 0 ? (\n ordersHref ? (\n <AppLink href={ordersHref} color=\"inherit\" underline=\"hover\">\n {`${record.ordersCount.toLocaleString()} · $${(record.ltvCents / 100).toFixed(2)} lifetime`}\n </AppLink>\n ) : (\n `${record.ordersCount.toLocaleString()} · $${(record.ltvCents / 100).toFixed(2)} lifetime`\n )\n ) : undefined\n }\n />\n </>\n ) : null\n }\n >\n {erase.banner}\n </CrmRecordHeader>\n {record && row && scope ? (\n <>\n {suiteIncluded ? null : (\n <CrmSuiteNotice>\n {'Working this contact — the owner, stage and company, tasks, ' +\n 'deals, files and custom fields, logging a call or an activity, ' +\n 'one-to-one email and merging duplicates — is part of the CRM.'}\n </CrmSuiteNotice>\n )}\n {/*\n The cross-site fact, at the organization level only (AGL-2630):\n which sites know this person, and their consent for each. Under\n a site the page IS one site's view and the card would say so on\n every record.\n */}\n {/* What an assistant proposes about the person (AGL-2917), where the plan works a contact. */}\n {suiteIncluded ? (\n <CrmRecordInsightsZone\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"contact\"\n recordId={id}\n name={record.name || record.email}\n taskLink={{ contactId: id }}\n />\n ) : null}\n {mount ? <ContactKnownByCard row={row} contactId={id} org={org} /> : null}\n <ContactPropertiesCard\n hostId={hostId}\n org={org}\n record={record}\n orgId={orgId}\n consentGroup={consentGroup}\n seed={{ status, fromCache }}\n members={members}\n suiteLocked={!suiteIncluded}\n />\n <ContactAssociationsCard\n hostId={siteHostId}\n record={record}\n row={row}\n consentGroup={consentGroup}\n seed={{ status, fromCache }}\n basePath={basePath}\n bookingsHref={bookingsHref}\n />\n {/*\n On a plan without the CRM, the cards that only work a record —\n custom fields, deals, tasks, files — are named in the notice above\n rather than drawn as four upsells in a row.\n */}\n {suiteIncluded ? (\n <ContactCustomFieldsCard hostId={hostId} org={org} contactId={id} basePath={basePath} />\n ) : null}\n <ContactTimelineCard\n hostId={hostId}\n org={org}\n contactId={id}\n contact={row}\n campaignHref={campaignHref}\n suiteLocked={!suiteIncluded}\n />\n {suiteIncluded ? (\n <>\n <ContactDealsCard\n hostId={hostId}\n org={org}\n basePath={basePath}\n contactId={id}\n contactName={record.name || record.email}\n />\n <RecordTasksCard hostId={hostId} org={org} basePath={basePath} contactId={id} />\n {/*\n The files are the VIEWING holder's, like everything else on this\n page: the write is a facet path, so a contract one brand filed\n never appears on a sibling brand's copy of the person.\n */}\n <RecordFilesCard\n scope={scope}\n collection=\"contacts\"\n recordId={id}\n hostId={hostId}\n facetGroupId={consentGroup.groupId}\n mediaIds={record.mediaIds}\n topic=\"contactRecord\"\n />\n </>\n ) : null}\n <ContactDuplicatesCard\n current={{ id, doc: row }}\n scope={scope}\n consentGroup={consentGroup}\n visibleTo={visibleTo}\n basePath={basePath}\n onMerge={(candidate) => merge.open({ other: candidate, keep: 'current' })}\n suiteLocked={!suiteIncluded}\n />\n <ContactMergeDialog\n {...merge.state}\n onClose={merge.close}\n hostId={siteHostId}\n current={{ id, doc: row }}\n scope={scope}\n consentGroup={consentGroup}\n visibleTo={visibleTo}\n memberName={members.memberName}\n // The page's own record was folded away: its address now opens\n // the survivor, which is where the reader is taken.\n onMerged={(survivorId) => {\n if (survivorId !== id) router.replace(routes.contact(survivorId))\n }}\n />\n </>\n ) : null}\n {erase.dialog}\n </Stack>\n </CrmCreateSiteDefault>\n )\n}\nContactDetailPage.displayName = 'ContactDetailPage'\n\nexport default ContactDetailPage\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","pluginDocsHelp","mdiDeleteOutline","mdiMerge","AppLink","MdiIcon","useConfirmationContext","useSnackbar","useFirestore","useFirestoreDoc","useUser","restampCrmListFields","crmTaskCallScope","Stack","Tooltip","Typography","arrayRemove","deleteDoc","deleteField","doc","updateDoc","useParams","useRouter","useCallback","useMemo","useCrmActivityLogger","CrmCreateSiteDefault","useCrmOrgMount","useCrmScope","contactPrimaryGroup","contactRecordFromDoc","crmRoutes","ContactAssociationsCard","ContactCustomFieldsCard","ContactDuplicatesCard","ContactKnownByCard","ContactMergeDialog","useContactMergeDialog","ContactPropertiesCard","ContactTimelineCard","AddToListButton","useBookingDoor","CrmSendEmailButton","ContactDealsCard","CrmCallButton","CrmEmailStateChip","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","CrmSuiteNotice","crmSuiteIncluded","crmSuiteLockedReason","useErasePersonAction","RecordFilesCard","RecordTasksCard","useEmailsHubPath","useOrgMembers","contactDocsHelp","anchor","excerpt","ContactDetailPage","props","hostId","org","id","basePath","routes","router","firestore","enqueueSnackbar","confirm","scope","orgId","consentGroup","viewingGroup","visibleTo","mount","merge","suiteIncluded","data","row","status","fromCache","idField","siteHostId","logActivity","record","notFound","Boolean","siteLabel","siteName","members","enabled","params","ordersHref","orgSlug","host","email","siteRecordLinks","String","ordersByCustomer","bookingDoor","siteSlug","siteSubdomain","bookingsHref","open","bookingsByBooker","emailsHub","campaignHref","encodeURIComponent","campaignId","user","handleRemove","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","ref","plan","planContactDetach","action","Object","fromEntries","remove","map","path","removeTokens","capturedByHostIds","removeHostIds","updatedAt","Date","type","name","variant","persist","push","section","error","console","allowDuplicate","overflowItems","key","label","icon","size","disabled","disabledReason","onClick","destructive","erase","subject","kind","requestedAtMs","readErasureRequestedAtMs","undefined","help","backHref","backLabel","spacing","subtitle","alternateEmails","join","actions","contactId","link","phone","suiteLocked","emailState","menuItems","booking","recordId","loading","chips","state","value","lifecycleStage","ownerUid","memberName","lastEmailEngagementAtMs","toLocaleString","span","activityTimeLabel","now","ordersCount","href","underline","ltvCents","toFixed","banner","taskLink","seed","contact","contactName","collection","facetGroupId","groupId","mediaIds","topic","current","onMerge","candidate","other","keep","onClose","close","onMerged","survivorId","replace","dialog","displayName"],"mappings":"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"}
@@ -44,10 +44,8 @@ export interface ContactListColumnOptions {
44
44
  *
45
45
  * Owner and Stage read off the flattened row, which is already this
46
46
  * holder's facet (`contactRecordFromDoc`), so neither column can show
47
- * another holder's assignment. Both are `filterable: false` on purpose: the
48
- * grid's filter panel translates to a Firestore query over the top-level
49
- * fields in `CONTACT_LIST_FILTER_FIELDS`, and a facet path is not one of
50
- * them. The toolbar's stage select and "Assigned to me" switch narrow the
51
- * loaded window instead, and say so.
47
+ * another holder's assignment. Filtering by either is the list's: the panel
48
+ * names the field and the list asks the holder's facet key of its query
49
+ * (`contactQueryClause`, AGL-3321).
52
50
  */
53
51
  export declare function contactListColumns(options: ContactListColumnOptions): GridColDef[];
@@ -17,7 +17,7 @@
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
19
  import { activityTimeLabel, CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_SOURCE_LABELS, EMAIL_STATE_LABELS } from "@aglyn/aglyn";
20
- import { hiddenFilterColumns, listFilterColumn } from "@aglyn/shared-ui-jsx/const/list-filter";
20
+ import { hiddenFilterColumns, listFilterColumn } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
21
21
  import { Chip, Stack, Tooltip, Typography } from "@mui/material";
22
22
  import { CONTACT_LIST_FILTER_FIELDS, CONTACT_LIST_FILTER_HEADERS } from "../constants/contact-filters.js";
23
23
  import { CrmEmailStateChip } from "./crm-email-state-chip.js";
@@ -58,11 +58,9 @@ import { nextActivityColumn } from "./crm-next-activity-column.js";
58
58
  *
59
59
  * Owner and Stage read off the flattened row, which is already this
60
60
  * holder's facet (`contactRecordFromDoc`), so neither column can show
61
- * another holder's assignment. Both are `filterable: false` on purpose: the
62
- * grid's filter panel translates to a Firestore query over the top-level
63
- * fields in `CONTACT_LIST_FILTER_FIELDS`, and a facet path is not one of
64
- * them. The toolbar's stage select and "Assigned to me" switch narrow the
65
- * loaded window instead, and say so.
61
+ * another holder's assignment. Filtering by either is the list's: the panel
62
+ * names the field and the list asks the holder's facet key of its query
63
+ * (`contactQueryClause`, AGL-3321).
66
64
  */ export function contactListColumns(options) {
67
65
  var _options_nowMs;
68
66
  const { memberName, siteName } = options;
@@ -142,8 +140,8 @@ import { nextActivityColumn } from "./crm-next-activity-column.js";
142
140
  * them. `capturedByHostIds` is a top-level array precisely so the
143
141
  * question can be answered without opening a facet. Not filterable from
144
142
  * the panel: an `array-contains` on it would displace the scope clause
145
- * under a site, and at the org level a saved view narrows the window
146
- * instead. A row that names no site predates attribution and says so,
143
+ * under a site, and a search or a facet filter at the org level (one
144
+ * array clause per query, AGL-3321). A row that names no site predates attribution and says so,
147
145
  * rather than reading as "every site".
148
146
  */ ...siteName ? [
149
147
  {