@aglyn/plugins-crm 1.0.0-beta.157 → 1.0.0-beta.158

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 (29) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/assignment-rule-drawer.js +2 -2
  3. package/src/lib/components/assignment-rule-drawer.js.map +1 -1
  4. package/src/lib/components/companies-bulk-bar.js +2 -2
  5. package/src/lib/components/companies-bulk-bar.js.map +1 -1
  6. package/src/lib/components/companies-section.js +2 -2
  7. package/src/lib/components/companies-section.js.map +1 -1
  8. package/src/lib/components/company-edit-drawer.js +2 -2
  9. package/src/lib/components/company-edit-drawer.js.map +1 -1
  10. package/src/lib/components/contact-properties-card.js +1 -1
  11. package/src/lib/components/contact-properties-card.js.map +1 -1
  12. package/src/lib/components/contacts-bulk-bar.js +2 -2
  13. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  14. package/src/lib/components/contacts-section.js +2 -1
  15. package/src/lib/components/contacts-section.js.map +1 -1
  16. package/src/lib/components/deal-edit-drawer.js +2 -2
  17. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  18. package/src/lib/components/deals-bulk-bar.js +2 -2
  19. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  20. package/src/lib/components/lead-owner-select.js +2 -2
  21. package/src/lib/components/lead-owner-select.js.map +1 -1
  22. package/src/lib/components/new-contact-drawer.js +2 -2
  23. package/src/lib/components/new-contact-drawer.js.map +1 -1
  24. package/src/lib/components/settings-section.js +10 -4
  25. package/src/lib/components/settings-section.js.map +1 -1
  26. package/src/lib/components/task-edit-drawer.js +2 -2
  27. package/src/lib/components/task-edit-drawer.js.map +1 -1
  28. package/src/lib/components/tasks-bulk-bar.js +2 -2
  29. package/src/lib/components/tasks-bulk-bar.js.map +1 -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} 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 {option.label}\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","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 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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"}
@@ -429,7 +429,7 @@ import { crmSuiteLockedReason } from "./crm-suite-lock.js";
429
429
  }) : null,
430
430
  members.options.map((owner)=>/*#__PURE__*/ _jsx(MenuItem, {
431
431
  value: owner.uid,
432
- children: owner.label
432
+ children: Aglyn.crmMemberPickerLabel(owner)
433
433
  }, owner.uid))
434
434
  ]
435
435
  })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-properties-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n type AglynOrgBilling,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ConsentGroup,\n type ContactLifecycleStage,\n crmTelHref,\n normalizePhone,\n} from '@aglyn/aglyn'\nimport { mdiPhoneOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Grid,\n IconButton,\n InputAdornment,\n MenuItem,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { type ContactRecord, parseContactTags } from '../model/contact-record'\nimport type { ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n CompanyPicker,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n addressDraftFrom,\n ContactAddressFields,\n type AddressDraft,\n} from './contact-address-fields'\nimport { crmSuiteLockedReason } from './crm-suite-lock'\nimport type { OrgMembers } from './use-org-members'\n\nexport interface ContactPropertiesCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /** The controller whose facet the edits are saved into. */\n consentGroup: ConsentGroup\n /**\n * The listener's verdict on the row the drafts were seeded from, for the\n * guard that refuses a save over an unconfirmed read.\n */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The team, for the owner picker and the owner's name. */\n members: OrgMembers\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The owner, the lifecycle stage and the company\n * show, locked, and are left out of a save; `crm/contact-update` refuses\n * such a plan the rest of the profile, the tags and the notes too.\n */\n suiteLocked?: boolean\n}\n\n/**\n * THE RECORD ITSELF: every field a team keeps on a person, in one card with\n * one Save (AGL-2596).\n *\n * ## One save, one request, one guard\n *\n * A field-at-a-time save would be nine requests and nine chances for the\n * stale-seed guard to refuse one of them — so the card holds a draft of the\n * whole profile and sends it once. The guard WRAPS the send: a row seeded\n * from the cache or from a failed read is refused with the message the guard\n * chooses, and what was typed stays in the fields, because a refusal that\n * also emptied the form would read as a save that worked.\n *\n * ## The server writes the facet (AGL-2804)\n *\n * Every field here lives in THIS holder's facet, and the Firestore rules\n * cannot tell one field of a facet from another, so the card writes nothing\n * itself. The draft goes to `crm/contact-update`, which writes each field by\n * dotted path into the holder's facet — never a nested object, which would\n * take every other holder's records with it — clears a blank field rather\n * than storing it empty, keeps the phone's and the company's search echoes\n * at the top of the document, and refuses every field to a plan without\n * the CRM. A refusal is shown in the route's own words.\n *\n * ## The stage goes to its own route\n *\n * The lifecycle stage is the one field an automation listens for:\n * `contactStageChanged` (AGL-2605) is emitted by `crm/contact-stage` after it\n * performs the write, and by nothing else. So when the stage the reader\n * picked differs from the one the record holds, it is sent to that route\n * once the rest of the profile has landed — the stage picked, or `null` for\n * \"Not placed yet\", which clears it and announces nothing. A refusal from\n * that route is reported on its own, with its sentence, after the fields that\n * did save.\n *\n * ## The name is an override\n *\n * The canonical name is shared by every site holding the person, so this\n * card never edits it. What it saves is this holder's own name for them,\n * and the helper says what a blank falls back to, so nobody clears the\n * field expecting the record to go nameless.\n *\n * ## Company is a record, with its name kept beside it\n *\n * The Company field is the picker (AGL-2613): the link is `companyId` in\n * this holder's facet, mirrored into the top-level `companyIds` for the\n * company page's query, and the company's contacts count moves with it —\n * all planned by the route from the stored document, so this card never\n * reasons about another holder's link. The name is sent too, from the picked\n * company, because the list column and the global search read the name and\n * not the id; a record that carries a name with no link — an import, a save\n * from before the picker — keeps it as the label, and the picker offers it\n * as the company to link or create.\n */\nexport function ContactPropertiesCard(props: ContactPropertiesCardProps) {\n const {\n hostId,\n org,\n record,\n consentGroup,\n seed,\n members,\n suiteLocked = false,\n } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n // The site a stage move is routed through: the mounted one, or at the\n // organization level the holder's own (AGL-2630).\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 companies = useCompanyOptions({ hostId, org })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [nameOverride, setNameOverride] = useState(record.nameOverride)\n const [phone, setPhone] = useState(record.phone)\n const [jobTitle, setJobTitle] = useState(record.jobTitle)\n const [companyName, setCompanyName] = useState(record.companyName)\n const [companyId, setCompanyId] = useState<string | null>(\n record.companyId || null,\n )\n const [ownerUid, setOwnerUid] = useState(record.ownerUid)\n const [lifecycleStage, setLifecycleStage] = useState<ContactLifecycleStage | ''>(\n record.lifecycleStage,\n )\n const [tags, setTags] = useState(record.tags.join(', '))\n const [notes, setNotes] = useState(record.notes)\n const [address, setAddress] = useState<AddressDraft>(\n addressDraftFrom(record.address),\n )\n const [phoneError, setPhoneError] = useState('')\n const [saving, setSaving] = useState(false)\n\n /*\n * Re-seeded when the RECORD changes, not when it re-renders. The listener\n * delivers a fresh row on every snapshot, including the one this card's\n * own save produces; re-seeding on each would overwrite what somebody is\n * in the middle of typing with what the server last confirmed.\n */\n const recordId = record.$id\n useEffect(() => {\n setNameOverride(record.nameOverride)\n setPhone(record.phone)\n setJobTitle(record.jobTitle)\n setCompanyName(record.companyName)\n setCompanyId(record.companyId || null)\n setOwnerUid(record.ownerUid)\n setLifecycleStage(record.lifecycleStage)\n setTags(record.tags.join(', '))\n setNotes(record.notes)\n setAddress(addressDraftFrom(record.address))\n setPhoneError('')\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n const handleSave = useCallback(async () => {\n const trimmedPhone = phone.trim()\n const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : ''\n if (trimmedPhone && !normalizedPhone) {\n setPhoneError('Enter it with its country code, like +1 512 555 0107.')\n return\n }\n setPhoneError('')\n // A stage that differs from the record's is the stage route's to write —\n // see the note above.\n const stageChanged = !suiteLocked && lifecycleStage !== record.lifecycleStage\n const set: ContactUpdateFields = {\n name: nameOverride.trim().slice(0, 120),\n phone: normalizedPhone ?? '',\n jobTitle: jobTitle.trim().slice(0, 120),\n address,\n tags: parseContactTags(tags),\n notes: notes.slice(0, 2000),\n // The owner and the company, sent only on a plan that carries the CRM.\n ...(suiteLocked\n ? {}\n : { ownerUid, companyId, companyName: companyName.trim().slice(0, 120) }),\n }\n setSaving(true)\n try {\n const verdict = await writeGuardedBySeed(\n {\n subject: 'contact',\n unreadable: seed.status === 'error',\n fromCache: seed.fromCache,\n },\n () => contactUpdate.updateOne(record.$id, set),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n logActivity('Updated contact', {\n type: 'contact',\n id: record.$id,\n name: record.name || record.email,\n })\n if (stageChanged && siteHostId) {\n try {\n await setContactStage(user, siteHostId, record.$id, lifecycleStage || null)\n } catch (error) {\n // The rest of the profile is saved; only the stage was refused, and\n // the route's own sentence says why.\n return void enqueueSnackbar(\n `Saved, but the stage could not be changed: ${\n error instanceof Error ? error.message : 'the request failed'\n }`,\n { variant: 'warning', persist: false },\n )\n }\n }\n enqueueSnackbar('Contact saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n } finally {\n setSaving(false)\n }\n }, [\n address,\n companyId,\n companyName,\n contactUpdate,\n enqueueSnackbar,\n siteHostId,\n jobTitle,\n lifecycleStage,\n logActivity,\n nameOverride,\n notes,\n ownerUid,\n phone,\n record.$id,\n record.email,\n record.lifecycleStage,\n record.name,\n seed.fromCache,\n seed.status,\n suiteLocked,\n tags,\n user,\n ])\n\n /*\n * An owner the roster no longer lists — somebody who left the team — is\n * still offered as the current value, named by their uid, so the select is\n * never handed a value it has no option for and the reader can see who it\n * was before reassigning.\n */\n const ownerKnown = members.options.some((option) => option.uid === ownerUid)\n\n /** The number in the box as something a dialer takes, or nothing (AGL-2661). */\n const telHref = crmTelHref(phone)\n\n return (\n <CardDisplay\n header={'Properties'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#the-record-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={saving}\n onClick={() => void handleSave()}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n ),\n }}\n >\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Email\"\n value={record.email}\n slotProps={{ input: { readOnly: true } }}\n helperText=\"The identity every site shares — it cannot be edited here.\"\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Name\"\n value={nameOverride}\n onChange={(event) => setNameOverride(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n helperText={\n record.canonicalName\n ? `Your own name for this person. Blank shows the name they gave: ${record.canonicalName}.`\n : 'Your own name for this person.'\n }\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={Boolean(phoneError)}\n helperText={phoneError || 'With the country code, like +1 512 555 0107'}\n fullWidth\n slotProps={{\n input: {\n /*\n * Click-to-call (AGL-2661), off what is IN the box rather\n * than off the saved value: the number on screen is the one\n * a reader means to ring, and `crmTelHref` withholds the\n * link from anything half-typed.\n */\n endAdornment: telHref ? (\n <InputAdornment position=\"end\">\n <Tooltip title={`Call ${phone.trim()}`}>\n <IconButton\n component=\"a\"\n href={telHref}\n size=\"small\"\n edge=\"end\"\n aria-label={`Call ${phone.trim()}`}\n >\n <MdiIcon path={mdiPhoneOutline.path} size={0.8} />\n </IconButton>\n </Tooltip>\n </InputAdornment>\n ) : null,\n },\n }}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => setJobTitle(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={companyId}\n onChange={(id, company) => {\n setCompanyId(id)\n // The label follows the link: the picked name, or nothing\n // once the link is cleared. A company the list cannot name\n // keeps whatever label the record already had.\n setCompanyName(company ? company.name : id ? companyName : '')\n }}\n onCreate={createCompany}\n email={record.email}\n fallbackName={companyName}\n disabled={saving || suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n setLifecycleStage(event.target.value as ContactLifecycleStage | '')\n }\n disabled={suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n fullWidth\n >\n <MenuItem value=\"\">{'Not placed yet'}</MenuItem>\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => setOwnerUid(event.target.value)}\n disabled={suiteLocked}\n helperText={\n suiteLocked\n ? crmSuiteLockedReason()\n : members.ready && !members.options.length\n ? 'The team roster could not be read, so nobody can be picked yet.'\n : 'The team member responsible for this relationship'\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Unassigned'}</MenuItem>\n {ownerUid && !ownerKnown ? (\n <MenuItem value={ownerUid}>{members.memberName(ownerUid)}</MenuItem>\n ) : null}\n {members.options.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {owner.label}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => setTags(event.target.value)}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n <ContactAddressFields value={address} onChange={setAddress} />\n </Stack>\n </Grid>\n <Grid size={{ xs: 12 }}>\n <TextField\n size=\"small\"\n label=\"About\"\n value={notes}\n onChange={(event) => setNotes(event.target.value)}\n helperText=\"Notes for your team. Nobody outside this site's group can read them.\"\n multiline\n minRows={3}\n fullWidth\n />\n </Grid>\n </Grid>\n </CardDisplay>\n )\n}\nContactPropertiesCard.displayName = 'ContactPropertiesCard'\n\nexport default ContactPropertiesCard\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","crmTelHref","normalizePhone","mdiPhoneOutline","CardDisplay","MdiIcon","useSnackbar","useUser","writeGuardedBySeed","Button","Grid","IconButton","InputAdornment","MenuItem","Stack","TextField","Tooltip","Typography","useCallback","useEffect","useState","useContactUpdate","useCrmActivityLogger","parseContactTags","setContactStage","CompanyPicker","useCompanyOptions","useCreateCompany","addressDraftFrom","ContactAddressFields","crmSuiteLockedReason","ContactPropertiesCard","props","hostId","org","record","consentGroup","seed","members","suiteLocked","data","user","enqueueSnackbar","contactUpdate","siteHostId","logActivity","companies","createCompany","nameOverride","setNameOverride","phone","setPhone","jobTitle","setJobTitle","companyName","setCompanyName","companyId","setCompanyId","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","join","notes","setNotes","address","setAddress","phoneError","setPhoneError","saving","setSaving","recordId","$id","handleSave","trimmedPhone","trim","normalizedPhone","stageChanged","set","name","slice","verdict","subject","unreadable","status","fromCache","updateOne","ok","message","variant","persist","type","id","email","error","Error","console","allowDuplicate","ownerKnown","options","some","option","uid","telHref","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","color","size","disabled","onClick","container","spacing","xs","sm","label","value","slotProps","input","readOnly","helperText","fullWidth","onChange","event","target","htmlInput","maxLength","canonicalName","Boolean","endAdornment","position","title","component","href","edge","aria-label","path","ready","truncated","company","onCreate","fallbackName","undefined","select","map","stage","length","memberName","owner","placeholder","multiline","minRows","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-properties-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n type AglynOrgBilling,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ConsentGroup,\n type ContactLifecycleStage,\n crmTelHref,\n normalizePhone,\n} from '@aglyn/aglyn'\nimport { mdiPhoneOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Grid,\n IconButton,\n InputAdornment,\n MenuItem,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { type ContactRecord, parseContactTags } from '../model/contact-record'\nimport type { ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n CompanyPicker,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n addressDraftFrom,\n ContactAddressFields,\n type AddressDraft,\n} from './contact-address-fields'\nimport { crmSuiteLockedReason } from './crm-suite-lock'\nimport type { OrgMembers } from './use-org-members'\n\nexport interface ContactPropertiesCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /** The controller whose facet the edits are saved into. */\n consentGroup: ConsentGroup\n /**\n * The listener's verdict on the row the drafts were seeded from, for the\n * guard that refuses a save over an unconfirmed read.\n */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The team, for the owner picker and the owner's name. */\n members: OrgMembers\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The owner, the lifecycle stage and the company\n * show, locked, and are left out of a save; `crm/contact-update` refuses\n * such a plan the rest of the profile, the tags and the notes too.\n */\n suiteLocked?: boolean\n}\n\n/**\n * THE RECORD ITSELF: every field a team keeps on a person, in one card with\n * one Save (AGL-2596).\n *\n * ## One save, one request, one guard\n *\n * A field-at-a-time save would be nine requests and nine chances for the\n * stale-seed guard to refuse one of them — so the card holds a draft of the\n * whole profile and sends it once. The guard WRAPS the send: a row seeded\n * from the cache or from a failed read is refused with the message the guard\n * chooses, and what was typed stays in the fields, because a refusal that\n * also emptied the form would read as a save that worked.\n *\n * ## The server writes the facet (AGL-2804)\n *\n * Every field here lives in THIS holder's facet, and the Firestore rules\n * cannot tell one field of a facet from another, so the card writes nothing\n * itself. The draft goes to `crm/contact-update`, which writes each field by\n * dotted path into the holder's facet — never a nested object, which would\n * take every other holder's records with it — clears a blank field rather\n * than storing it empty, keeps the phone's and the company's search echoes\n * at the top of the document, and refuses every field to a plan without\n * the CRM. A refusal is shown in the route's own words.\n *\n * ## The stage goes to its own route\n *\n * The lifecycle stage is the one field an automation listens for:\n * `contactStageChanged` (AGL-2605) is emitted by `crm/contact-stage` after it\n * performs the write, and by nothing else. So when the stage the reader\n * picked differs from the one the record holds, it is sent to that route\n * once the rest of the profile has landed — the stage picked, or `null` for\n * \"Not placed yet\", which clears it and announces nothing. A refusal from\n * that route is reported on its own, with its sentence, after the fields that\n * did save.\n *\n * ## The name is an override\n *\n * The canonical name is shared by every site holding the person, so this\n * card never edits it. What it saves is this holder's own name for them,\n * and the helper says what a blank falls back to, so nobody clears the\n * field expecting the record to go nameless.\n *\n * ## Company is a record, with its name kept beside it\n *\n * The Company field is the picker (AGL-2613): the link is `companyId` in\n * this holder's facet, mirrored into the top-level `companyIds` for the\n * company page's query, and the company's contacts count moves with it —\n * all planned by the route from the stored document, so this card never\n * reasons about another holder's link. The name is sent too, from the picked\n * company, because the list column and the global search read the name and\n * not the id; a record that carries a name with no link — an import, a save\n * from before the picker — keeps it as the label, and the picker offers it\n * as the company to link or create.\n */\nexport function ContactPropertiesCard(props: ContactPropertiesCardProps) {\n const {\n hostId,\n org,\n record,\n consentGroup,\n seed,\n members,\n suiteLocked = false,\n } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n // The site a stage move is routed through: the mounted one, or at the\n // organization level the holder's own (AGL-2630).\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 companies = useCompanyOptions({ hostId, org })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [nameOverride, setNameOverride] = useState(record.nameOverride)\n const [phone, setPhone] = useState(record.phone)\n const [jobTitle, setJobTitle] = useState(record.jobTitle)\n const [companyName, setCompanyName] = useState(record.companyName)\n const [companyId, setCompanyId] = useState<string | null>(\n record.companyId || null,\n )\n const [ownerUid, setOwnerUid] = useState(record.ownerUid)\n const [lifecycleStage, setLifecycleStage] = useState<ContactLifecycleStage | ''>(\n record.lifecycleStage,\n )\n const [tags, setTags] = useState(record.tags.join(', '))\n const [notes, setNotes] = useState(record.notes)\n const [address, setAddress] = useState<AddressDraft>(\n addressDraftFrom(record.address),\n )\n const [phoneError, setPhoneError] = useState('')\n const [saving, setSaving] = useState(false)\n\n /*\n * Re-seeded when the RECORD changes, not when it re-renders. The listener\n * delivers a fresh row on every snapshot, including the one this card's\n * own save produces; re-seeding on each would overwrite what somebody is\n * in the middle of typing with what the server last confirmed.\n */\n const recordId = record.$id\n useEffect(() => {\n setNameOverride(record.nameOverride)\n setPhone(record.phone)\n setJobTitle(record.jobTitle)\n setCompanyName(record.companyName)\n setCompanyId(record.companyId || null)\n setOwnerUid(record.ownerUid)\n setLifecycleStage(record.lifecycleStage)\n setTags(record.tags.join(', '))\n setNotes(record.notes)\n setAddress(addressDraftFrom(record.address))\n setPhoneError('')\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n const handleSave = useCallback(async () => {\n const trimmedPhone = phone.trim()\n const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : ''\n if (trimmedPhone && !normalizedPhone) {\n setPhoneError('Enter it with its country code, like +1 512 555 0107.')\n return\n }\n setPhoneError('')\n // A stage that differs from the record's is the stage route's to write —\n // see the note above.\n const stageChanged = !suiteLocked && lifecycleStage !== record.lifecycleStage\n const set: ContactUpdateFields = {\n name: nameOverride.trim().slice(0, 120),\n phone: normalizedPhone ?? '',\n jobTitle: jobTitle.trim().slice(0, 120),\n address,\n tags: parseContactTags(tags),\n notes: notes.slice(0, 2000),\n // The owner and the company, sent only on a plan that carries the CRM.\n ...(suiteLocked\n ? {}\n : { ownerUid, companyId, companyName: companyName.trim().slice(0, 120) }),\n }\n setSaving(true)\n try {\n const verdict = await writeGuardedBySeed(\n {\n subject: 'contact',\n unreadable: seed.status === 'error',\n fromCache: seed.fromCache,\n },\n () => contactUpdate.updateOne(record.$id, set),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n logActivity('Updated contact', {\n type: 'contact',\n id: record.$id,\n name: record.name || record.email,\n })\n if (stageChanged && siteHostId) {\n try {\n await setContactStage(user, siteHostId, record.$id, lifecycleStage || null)\n } catch (error) {\n // The rest of the profile is saved; only the stage was refused, and\n // the route's own sentence says why.\n return void enqueueSnackbar(\n `Saved, but the stage could not be changed: ${\n error instanceof Error ? error.message : 'the request failed'\n }`,\n { variant: 'warning', persist: false },\n )\n }\n }\n enqueueSnackbar('Contact saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n } finally {\n setSaving(false)\n }\n }, [\n address,\n companyId,\n companyName,\n contactUpdate,\n enqueueSnackbar,\n siteHostId,\n jobTitle,\n lifecycleStage,\n logActivity,\n nameOverride,\n notes,\n ownerUid,\n phone,\n record.$id,\n record.email,\n record.lifecycleStage,\n record.name,\n seed.fromCache,\n seed.status,\n suiteLocked,\n tags,\n user,\n ])\n\n /*\n * An owner the roster no longer lists — somebody who left the team — is\n * still offered as the current value, named by their uid, so the select is\n * never handed a value it has no option for and the reader can see who it\n * was before reassigning.\n */\n const ownerKnown = members.options.some((option) => option.uid === ownerUid)\n\n /** The number in the box as something a dialer takes, or nothing (AGL-2661). */\n const telHref = crmTelHref(phone)\n\n return (\n <CardDisplay\n header={'Properties'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#the-record-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={saving}\n onClick={() => void handleSave()}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n ),\n }}\n >\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Email\"\n value={record.email}\n slotProps={{ input: { readOnly: true } }}\n helperText=\"The identity every site shares — it cannot be edited here.\"\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Name\"\n value={nameOverride}\n onChange={(event) => setNameOverride(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n helperText={\n record.canonicalName\n ? `Your own name for this person. Blank shows the name they gave: ${record.canonicalName}.`\n : 'Your own name for this person.'\n }\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={Boolean(phoneError)}\n helperText={phoneError || 'With the country code, like +1 512 555 0107'}\n fullWidth\n slotProps={{\n input: {\n /*\n * Click-to-call (AGL-2661), off what is IN the box rather\n * than off the saved value: the number on screen is the one\n * a reader means to ring, and `crmTelHref` withholds the\n * link from anything half-typed.\n */\n endAdornment: telHref ? (\n <InputAdornment position=\"end\">\n <Tooltip title={`Call ${phone.trim()}`}>\n <IconButton\n component=\"a\"\n href={telHref}\n size=\"small\"\n edge=\"end\"\n aria-label={`Call ${phone.trim()}`}\n >\n <MdiIcon path={mdiPhoneOutline.path} size={0.8} />\n </IconButton>\n </Tooltip>\n </InputAdornment>\n ) : null,\n },\n }}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => setJobTitle(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={companyId}\n onChange={(id, company) => {\n setCompanyId(id)\n // The label follows the link: the picked name, or nothing\n // once the link is cleared. A company the list cannot name\n // keeps whatever label the record already had.\n setCompanyName(company ? company.name : id ? companyName : '')\n }}\n onCreate={createCompany}\n email={record.email}\n fallbackName={companyName}\n disabled={saving || suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n setLifecycleStage(event.target.value as ContactLifecycleStage | '')\n }\n disabled={suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n fullWidth\n >\n <MenuItem value=\"\">{'Not placed yet'}</MenuItem>\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => setOwnerUid(event.target.value)}\n disabled={suiteLocked}\n helperText={\n suiteLocked\n ? crmSuiteLockedReason()\n : members.ready && !members.options.length\n ? 'The team roster could not be read, so nobody can be picked yet.'\n : 'The team member responsible for this relationship'\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Unassigned'}</MenuItem>\n {ownerUid && !ownerKnown ? (\n <MenuItem value={ownerUid}>{members.memberName(ownerUid)}</MenuItem>\n ) : null}\n {members.options.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {Aglyn.crmMemberPickerLabel(owner)}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => setTags(event.target.value)}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n <ContactAddressFields value={address} onChange={setAddress} />\n </Stack>\n </Grid>\n <Grid size={{ xs: 12 }}>\n <TextField\n size=\"small\"\n label=\"About\"\n value={notes}\n onChange={(event) => setNotes(event.target.value)}\n helperText=\"Notes for your team. Nobody outside this site's group can read them.\"\n multiline\n minRows={3}\n fullWidth\n />\n </Grid>\n </Grid>\n </CardDisplay>\n )\n}\nContactPropertiesCard.displayName = 'ContactPropertiesCard'\n\nexport default ContactPropertiesCard\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","crmTelHref","normalizePhone","mdiPhoneOutline","CardDisplay","MdiIcon","useSnackbar","useUser","writeGuardedBySeed","Button","Grid","IconButton","InputAdornment","MenuItem","Stack","TextField","Tooltip","Typography","useCallback","useEffect","useState","useContactUpdate","useCrmActivityLogger","parseContactTags","setContactStage","CompanyPicker","useCompanyOptions","useCreateCompany","addressDraftFrom","ContactAddressFields","crmSuiteLockedReason","ContactPropertiesCard","props","hostId","org","record","consentGroup","seed","members","suiteLocked","data","user","enqueueSnackbar","contactUpdate","siteHostId","logActivity","companies","createCompany","nameOverride","setNameOverride","phone","setPhone","jobTitle","setJobTitle","companyName","setCompanyName","companyId","setCompanyId","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","join","notes","setNotes","address","setAddress","phoneError","setPhoneError","saving","setSaving","recordId","$id","handleSave","trimmedPhone","trim","normalizedPhone","stageChanged","set","name","slice","verdict","subject","unreadable","status","fromCache","updateOne","ok","message","variant","persist","type","id","email","error","Error","console","allowDuplicate","ownerKnown","options","some","option","uid","telHref","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","color","size","disabled","onClick","container","spacing","xs","sm","label","value","slotProps","input","readOnly","helperText","fullWidth","onChange","event","target","htmlInput","maxLength","canonicalName","Boolean","endAdornment","position","title","component","href","edge","aria-label","path","ready","truncated","company","onCreate","fallbackName","undefined","select","map","stage","length","memberName","owner","crmMemberPickerLabel","placeholder","multiline","minRows","displayName"],"mappings":"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"}
@@ -58,7 +58,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
58
58
  * The membership routes, with the check-then-attest sequence the Emails
59
59
  * console runs — see `add-to-list-dialog.tsx`. Nothing here writes a
60
60
  * membership document.
61
- */ import { CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_LIFECYCLE_STAGES } from "@aglyn/aglyn";
61
+ */ import { CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_LIFECYCLE_STAGES, crmMemberPickerLabel } from "@aglyn/aglyn";
62
62
  import { useConfirmationContext } from "@aglyn/shared-ui-jsx";
63
63
  import { Button, MenuItem, TextField } from "@mui/material";
64
64
  import { deleteDoc, doc, updateDoc, writeBatch } from "firebase/firestore";
@@ -380,7 +380,7 @@ function ContactsBulkBarBody(props) {
380
380
  }),
381
381
  team.options.map((member)=>/*#__PURE__*/ _jsx(MenuItem, {
382
382
  value: member.uid,
383
- children: member.label
383
+ children: crmMemberPickerLabel(member)
384
384
  }, member.uid))
385
385
  ]
386
386
  }) : pending === 'stage' ? /*#__PURE__*/ _jsx(TextField, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contacts-bulk-bar.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\n/**\n * The bar over the contacts table, for whatever rows are ticked (AGL-2603).\n *\n * The table's selection is opt-in and this is what it is for: a chosen set\n * of people, and one act over all of them. Tag or untag them, hand them to\n * an owner, move them along the funnel, put them on an email audience, take\n * them into a spreadsheet, or let them go from this site's CRM.\n *\n * ## The chrome is the shared frame\n *\n * This bar settled how a bar over a table reads — it appears only for a\n * selection and says how many, its actions sit in one wrapping row with\n * Clear at the end, and whatever the last action could not do is listed by\n * name in an alert under it — and the companies, deals and tasks bars were\n * built on that shape as `CrmBulkBarFrame` and `useCrmBulkApply` (AGL-2621).\n * The contacts bar stands on the same two (AGL-2635): what is its own is the\n * actions, the dialog's field, the audience door, and how each act is sent.\n *\n * ## Each act goes where its field is written (AGL-2804)\n *\n * A contact's tags, owner and company live in the holder's facet, which is\n * the server's to write — the rules cannot tell one field of a facet from\n * another — so each of those acts is ONE request to `crm/contact-update` for\n * the rows it reaches (`contacts-bulk-writes.ts` decides which), sent in\n * pieces the route accepts. A stage move is `crm/contact-stage`, row by row:\n * a move is what an automation listens for, and only the route that\n * performed it can announce it. Letting the rows go is the one act written\n * client-direct, because a holder dropping its own facet is the one facet\n * change the rules leave the browser.\n *\n * ## A refused row is named\n *\n * Whatever the server or the store refused comes back by address into the\n * alert under the bar — not into a count, and not into the console. A\n * merchant who tagged four hundred people and got three hundred and\n * ninety-eight needs the two addresses.\n *\n * ## Add to list goes through the audience's own door\n *\n * The membership routes, with the check-then-attest sequence the Emails\n * console runs — see `add-to-list-dialog.tsx`. Nothing here writes a\n * membership document.\n */\n\nimport {\n type AglynOrgBilling,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ConsentGroup,\n type ContactLifecycleStage,\n} from '@aglyn/aglyn'\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { Button, MenuItem, TextField } from '@mui/material'\nimport { deleteDoc, doc, updateDoc, writeBatch } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n useFirestore,\n useOrgMemberOptions,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { CRM_CONTACT_UPDATE_MAX, type ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n contactBulkAddressOf,\n normalizeBulkTag,\n planAddTag,\n planDetach,\n planRemoveTag,\n planSetCompany,\n type ContactBulkRow,\n type ContactBulkSelection,\n type ContactBulkSkip,\n type ContactBulkWrite,\n} from '../model/contacts-bulk-writes'\nimport {\n type ContactCsvOptions,\n type ContactCsvRow,\n contactsCsv,\n downloadTextFile,\n} from '../model/contacts-csv'\nimport {\n type CrmBulkOutcome,\n runCrmBulkBatch,\n runCrmBulkCalls,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport AddToListDialog from './add-to-list-dialog'\nimport {\n CompanyPicker,\n type CompanyOption,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { CrmSuiteLockedButton } from './crm-suite-lock'\n\nexport interface ContactsBulkBarProps {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** `['orgs', orgId]`, or `null` while the org is unresolved. */\n scope: readonly [string, string] | null\n /**\n * The holder these rows are being read AS — whose facet the acts land in.\n * `null` at the organization level (AGL-2630), where each row's own holder\n * (`groupId`) takes the act instead, and \"Remove from this site\" is not\n * offered because there is no site to remove from.\n */\n consentGroup: ConsentGroup | null\n /** The table's rows, already projected through the holder's facet. */\n rows: readonly (ContactBulkRow & ContactCsvRow)[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /**\n * How the export names an owner and which custom fields it carries — the\n * table's own options, so the selection's file and the table's are one\n * format (AGL-2621).\n */\n csv?: ContactCsvOptions\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The owner, the stage and the company stand locked;\n * tags, the exports, the audience door and removing people from a site\n * draw no lock of their own.\n */\n suiteLocked?: boolean\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'contact', plural: 'contacts' }\n\n/** The one small dialog the value-taking actions share. */\ntype PendingAction = 'add-tag' | 'remove-tag' | 'owner' | 'stage' | 'company'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n 'add-tag': 'Add a tag',\n 'remove-tag': 'Remove a tag',\n owner: 'Set the owner',\n stage: 'Set the lifecycle stage',\n company: 'Set the company',\n}\n\n/** How a finished action reads, given how many rows it reached. */\nconst doneSentence = (action: PendingAction | 'detach', done: number): string => {\n const people = countNoun(done, NOUN)\n switch (action) {\n case 'add-tag':\n return `Tagged ${people}`\n case 'remove-tag':\n return `Removed the tag from ${people}`\n case 'owner':\n return `Owner set on ${people}`\n case 'stage':\n return `Stage set on ${people}`\n case 'company':\n return `Company set on ${people}`\n case 'detach':\n return `${people} removed from this site`\n }\n}\n\n/** The report's line for a row an act left out on purpose. */\nconst skippedLine = (row: ContactBulkSkip) => ({ label: row.email, reason: row.reason })\n\n/**\n * The bar, or nothing.\n *\n * The empty state is a separate component boundary on purpose: with no\n * selection the body below is never mounted, so none of its hooks run — no\n * roster hook armed, no dialog state held — on the visit to the list that\n * every reader makes and most never tick a row on.\n */\nexport function ContactsBulkBar(props: ContactsBulkBarProps) {\n if (!props.selected.length) return null\n return <ContactsBulkBarBody {...props} />\n}\nContactsBulkBar.displayName = 'ContactsBulkBar'\n\nfunction ContactsBulkBarBody(props: ContactsBulkBarProps) {\n const {\n hostId,\n org,\n scope,\n consentGroup,\n rows,\n selected,\n onSelectedChange,\n csv,\n suiteLocked = false,\n } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { confirm } = useConfirmationContext()\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'contact' })\n const contactUpdate = useContactUpdate(hostId)\n\n const selectedRows = useMemo(() => {\n const chosen = new Set(selected)\n return rows.filter((row) => chosen.has(row.$id))\n }, [rows, selected])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n const [company, setCompany] = useState<CompanyOption | null>(null)\n const [listOpen, setListOpen] = useState(false)\n\n /*\n * The roster, read only while the owner dialog is open. A bulk bar sits on\n * every visit to the list; the team is a request for a control nobody has\n * opened until they open it. The company list follows the same rule for\n * its own dialog (AGL-2613).\n */\n const team = useOrgMemberOptions(scope?.[1], { enabled: pending === 'owner' })\n const companies = useCompanyOptions({\n hostId,\n org,\n enabled: pending === 'company',\n })\n const createCompany = useCreateCompany({ hostId, org })\n // The site the audiences API is asked from: the mounted one, or at the\n // organization level the one the reader picked (AGL-2630).\n const { createHostId } = useCrmScope({ hostId, org })\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setCompany(null)\n setPending(action)\n }\n\n /**\n * The Firestore writers for the one act written client-direct: letting the\n * rows go, a batch at a time and a row at a time for a batch that failed.\n */\n const writers = useMemo(() => {\n const refFor = (id: string) =>\n doc(firestore, scope?.[0] ?? 'orgs', scope?.[1] ?? '', 'contacts', id)\n return {\n commitBatch: async (writes: readonly ContactBulkWrite[]) => {\n const batch = writeBatch(firestore)\n for (const write of writes) {\n if (write.kind === 'delete') batch.delete(refFor(write.id))\n else batch.update(refFor(write.id), write.data)\n }\n await batch.commit()\n },\n commitOne: async (write: ContactBulkWrite) => {\n if (write.kind === 'delete') await deleteDoc(refFor(write.id))\n else await updateDoc(refFor(write.id), write.data)\n },\n }\n }, [firestore, scope])\n\n /** One act's request for the rows it reaches, in the pieces the route accepts. */\n const saveThroughRoute = useCallback(\n (targets: readonly ContactBulkRow[], set: ContactUpdateFields) =>\n runCrmBulkBatch(\n targets,\n (row) => row.$id,\n contactBulkAddressOf,\n (piece) => contactUpdate.updateMany(piece.map((row) => row.$id), set),\n CRM_CONTACT_UPDATE_MAX,\n ),\n [contactUpdate],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending || !scope) return\n /*\n * A row no site holds has no facet to write. Under a site every selected\n * row is the viewing group's; at the organization level (AGL-2630) each\n * row was flattened through its own primary holder and is written back\n * through it, so a row nobody holds yet is left out and says so.\n */\n const held: ContactBulkRow[] = []\n const unheld: ContactBulkSkip[] = []\n for (const row of selectedRows) {\n if (consentGroup || row.groupId) held.push(row)\n else {\n unheld.push({\n email: contactBulkAddressOf(row),\n reason: 'no site holds this contact yet',\n })\n }\n }\n\n const planned = ((): { selection: ContactBulkSelection; job: () => Promise<CrmBulkOutcome> } | null => {\n if (pending === 'add-tag' || pending === 'remove-tag') {\n const tag = normalizeBulkTag(value)\n if (!tag) return null\n const selection =\n pending === 'add-tag' ? planAddTag(held, tag) : planRemoveTag(held, tag)\n const set: ContactUpdateFields =\n pending === 'add-tag' ? { addTag: tag } : { removeTag: tag }\n return { selection, job: () => saveThroughRoute(selection.rows, set) }\n }\n if (pending === 'owner') {\n return {\n selection: { rows: held, skipped: [] },\n job: () => saveThroughRoute(held, { ownerUid: value }),\n }\n }\n if (pending === 'company') {\n const companyId = company?.id ?? null\n const selection = planSetCompany(held, companyId)\n return { selection, job: () => saveThroughRoute(selection.rows, { companyId }) }\n }\n if (!value) return null\n const stage = value as ContactLifecycleStage\n return {\n selection: { rows: held, skipped: [] },\n /*\n * One row at a time through the one writer of a contact's stage, on\n * the site that holds the row: the mounted site, or at the\n * organization level the row's own holder's.\n */\n job: () =>\n runCrmBulkCalls(held, contactBulkAddressOf, async (row) => {\n const site = hostId ?? (row.holderHostId || null)\n if (!site) throw new Error('no site holds this contact yet')\n await setContactStage(user, site, row.$id, stage)\n }),\n }\n })()\n if (!planned) return\n const action = pending\n setPending(null)\n await apply({\n attempted: planned.selection.rows.length,\n skipped: [...unheld, ...planned.selection.skipped].map(skippedLine),\n job: planned.job,\n done: (count) => doneSentence(action, count),\n })\n }, [\n pending,\n scope,\n selectedRows,\n consentGroup,\n value,\n company,\n saveThroughRoute,\n hostId,\n user,\n apply,\n ])\n\n const handleExport = useCallback(() => {\n downloadTextFile(\n 'contacts-selected.csv',\n 'text/csv',\n contactsCsv(selectedRows, csv),\n )\n }, [selectedRows, csv])\n\n const handleDetach = useCallback(async () => {\n if (!scope || !selectedRows.length || !consentGroup) return\n const count = selectedRows.length\n const confirmed = await confirm({\n title: count === 1 ? 'Remove this contact?' : `Remove ${count} contacts?`,\n description:\n `${count === 1 ? 'This person is' : `These ${count} people are`} ` +\n \"removed from this site's Contacts, along with their notes, tags and \" +\n 'timeline. Other sites that captured the same people keep their own ' +\n 'records. Their form submissions, orders, bookings, and membership ' +\n 'records are separate — delete those from their own pages if the ' +\n 'request covers them.',\n confirmationText: count === 1 ? 'Remove contact' : 'Remove contacts',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel.\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n const plan = planDetach(selectedRows, consentGroup, Date.now())\n const outcome = await apply({\n attempted: plan.writes.length,\n skipped: plan.skipped.map(skippedLine),\n job: () => runCrmBulkWrites(writers, plan.writes, (write) => write.email),\n done: (done) => doneSentence('detach', done),\n })\n // The rows that went are gone from the table; the refused ones stay\n // selected, so the reader can see which they are and try again.\n const refused = new Set(outcome.refused.map((row) => row.label))\n onSelectedChange(\n selectedRows\n .filter((row) => refused.has(contactBulkAddressOf(row)))\n .map((row) => row.$id),\n )\n }, [scope, selectedRows, confirm, apply, writers, consentGroup, onSelectedChange])\n\n const emails = selectedRows\n .map((row) => String(row.email ?? '').trim())\n .filter(Boolean)\n\n return (\n <CrmBulkBarFrame\n count={selected.length}\n noun={NOUN}\n busy={busy}\n onClear={() => onSelectedChange([])}\n report={report}\n onDismissReport={dismissReport}\n extras={\n <>\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={\n !(pending === 'stage' && !value) &&\n !(\n (pending === 'add-tag' || pending === 'remove-tag') &&\n !normalizeBulkTag(value)\n )\n }\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'add-tag' || pending === 'remove-tag' ? (\n <TextField\n autoFocus\n size=\"small\"\n label=\"Tag\"\n placeholder=\"vip\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText={\n pending === 'add-tag'\n ? 'Lowercased, like the tags on a profile'\n : 'Removed wherever it is present'\n }\n />\n ) : pending === 'owner' ? (\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n error={Boolean(team.error)}\n helperText={team.error ?? (team.ready ? undefined : 'Loading the team…')}\n >\n <MenuItem value=\"\">{'Nobody — clear the owner'}</MenuItem>\n {team.options.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {member.label}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'stage' ? (\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n >\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'company' ? (\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={company?.id ?? null}\n onChange={(_id, picked) => setCompany(picked)}\n onCreate={createCompany}\n helperText=\"Leave it empty to unlink the selected contacts from their companies.\"\n />\n ) : null}\n </CrmBulkValueDialog>\n {listOpen ? (\n <AddToListDialog\n open\n onClose={() => setListOpen(false)}\n hostId={createHostId}\n scope={scope}\n emails={emails}\n />\n ) : null}\n </>\n }\n >\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('add-tag')}>\n {'Add tag'}\n </Button>\n <Button\n size=\"small\"\n disabled={busy || !scope}\n onClick={() => openAction('remove-tag')}\n >\n {'Remove tag'}\n </Button>\n {suiteLocked ? (\n <>\n <CrmSuiteLockedButton>{'Set owner'}</CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Set stage'}</CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Set company'}</CrmSuiteLockedButton>\n </>\n ) : (\n <>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('stage')}>\n {'Set stage'}\n </Button>\n <Button\n size=\"small\"\n disabled={busy || !scope}\n onClick={() => openAction('company')}\n >\n {'Set company'}\n </Button>\n </>\n )}\n <Button\n size=\"small\"\n disabled={busy || !scope || !emails.length || !createHostId}\n onClick={() => setListOpen(true)}\n >\n {'Add to list'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={handleExport}>\n {'Export CSV'}\n </Button>\n {/*\n The selection's file is the rows on screen; this one is the whole\n collection, streamed by the server (AGL-2662).\n */}\n <CrmExportAllButton\n resource=\"contacts\"\n orgId={scope?.[1] ?? null}\n hostId={hostId}\n disabled={busy}\n />\n {/*\n Only under a site. The act is a DETACH from the viewing site's\n CRM, and at the organization level there is no viewing site — an\n org-wide member removes a person from a site by opening the record\n under that site.\n */}\n {consentGroup ? (\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy || !scope}\n onClick={() => void handleDetach()}\n >\n {'Remove from this site'}\n </Button>\n ) : null}\n </CrmBulkBarFrame>\n )\n}\nContactsBulkBarBody.displayName = 'ContactsBulkBarBody'\n\nexport default ContactsBulkBar\n"],"names":["CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","useConfirmationContext","Button","MenuItem","TextField","deleteDoc","doc","updateDoc","writeBatch","useCallback","useMemo","useState","useFirestore","useOrgMemberOptions","useUser","useContactUpdate","useCrmBulkApply","useCrmScope","CRM_CONTACT_UPDATE_MAX","setContactStage","contactBulkAddressOf","normalizeBulkTag","planAddTag","planDetach","planRemoveTag","planSetCompany","contactsCsv","downloadTextFile","runCrmBulkBatch","runCrmBulkCalls","runCrmBulkWrites","AddToListDialog","CompanyPicker","useCompanyOptions","useCreateCompany","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","CrmSuiteLockedButton","NOUN","singular","plural","ACTION_TITLES","owner","stage","company","doneSentence","action","done","people","skippedLine","row","label","email","reason","ContactsBulkBar","props","selected","length","ContactsBulkBarBody","displayName","team","hostId","org","scope","consentGroup","rows","onSelectedChange","csv","suiteLocked","firestore","data","user","confirm","busy","report","apply","dismissReport","recordKind","contactUpdate","selectedRows","chosen","Set","filter","has","$id","pending","setPending","value","setValue","setCompany","listOpen","setListOpen","enabled","companies","createCompany","createHostId","openAction","writers","refFor","id","commitBatch","writes","batch","write","kind","delete","update","commit","commitOne","saveThroughRoute","targets","set","piece","updateMany","map","handleApply","held","unheld","groupId","push","planned","tag","selection","addTag","removeTag","job","skipped","ownerUid","companyId","site","holderHostId","Error","attempted","count","handleExport","handleDetach","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","plan","Date","now","outcome","refused","emails","String","trim","Boolean","noun","onClear","onDismissReport","extras","open","canApply","onClose","onApply","autoFocus","size","placeholder","onChange","event","target","helperText","select","error","ready","undefined","options","member","uid","truncated","_id","picked","onCreate","disabled","onClick","resource","orgId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contacts-bulk-bar.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\n/**\n * The bar over the contacts table, for whatever rows are ticked (AGL-2603).\n *\n * The table's selection is opt-in and this is what it is for: a chosen set\n * of people, and one act over all of them. Tag or untag them, hand them to\n * an owner, move them along the funnel, put them on an email audience, take\n * them into a spreadsheet, or let them go from this site's CRM.\n *\n * ## The chrome is the shared frame\n *\n * This bar settled how a bar over a table reads — it appears only for a\n * selection and says how many, its actions sit in one wrapping row with\n * Clear at the end, and whatever the last action could not do is listed by\n * name in an alert under it — and the companies, deals and tasks bars were\n * built on that shape as `CrmBulkBarFrame` and `useCrmBulkApply` (AGL-2621).\n * The contacts bar stands on the same two (AGL-2635): what is its own is the\n * actions, the dialog's field, the audience door, and how each act is sent.\n *\n * ## Each act goes where its field is written (AGL-2804)\n *\n * A contact's tags, owner and company live in the holder's facet, which is\n * the server's to write — the rules cannot tell one field of a facet from\n * another — so each of those acts is ONE request to `crm/contact-update` for\n * the rows it reaches (`contacts-bulk-writes.ts` decides which), sent in\n * pieces the route accepts. A stage move is `crm/contact-stage`, row by row:\n * a move is what an automation listens for, and only the route that\n * performed it can announce it. Letting the rows go is the one act written\n * client-direct, because a holder dropping its own facet is the one facet\n * change the rules leave the browser.\n *\n * ## A refused row is named\n *\n * Whatever the server or the store refused comes back by address into the\n * alert under the bar — not into a count, and not into the console. A\n * merchant who tagged four hundred people and got three hundred and\n * ninety-eight needs the two addresses.\n *\n * ## Add to list goes through the audience's own door\n *\n * The membership routes, with the check-then-attest sequence the Emails\n * console runs — see `add-to-list-dialog.tsx`. Nothing here writes a\n * membership document.\n */\n\nimport {\n type AglynOrgBilling,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ConsentGroup,\n type ContactLifecycleStage,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { Button, MenuItem, TextField } from '@mui/material'\nimport { deleteDoc, doc, updateDoc, writeBatch } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n useFirestore,\n useOrgMemberOptions,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { CRM_CONTACT_UPDATE_MAX, type ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n contactBulkAddressOf,\n normalizeBulkTag,\n planAddTag,\n planDetach,\n planRemoveTag,\n planSetCompany,\n type ContactBulkRow,\n type ContactBulkSelection,\n type ContactBulkSkip,\n type ContactBulkWrite,\n} from '../model/contacts-bulk-writes'\nimport {\n type ContactCsvOptions,\n type ContactCsvRow,\n contactsCsv,\n downloadTextFile,\n} from '../model/contacts-csv'\nimport {\n type CrmBulkOutcome,\n runCrmBulkBatch,\n runCrmBulkCalls,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport AddToListDialog from './add-to-list-dialog'\nimport {\n CompanyPicker,\n type CompanyOption,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { CrmSuiteLockedButton } from './crm-suite-lock'\n\nexport interface ContactsBulkBarProps {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** `['orgs', orgId]`, or `null` while the org is unresolved. */\n scope: readonly [string, string] | null\n /**\n * The holder these rows are being read AS — whose facet the acts land in.\n * `null` at the organization level (AGL-2630), where each row's own holder\n * (`groupId`) takes the act instead, and \"Remove from this site\" is not\n * offered because there is no site to remove from.\n */\n consentGroup: ConsentGroup | null\n /** The table's rows, already projected through the holder's facet. */\n rows: readonly (ContactBulkRow & ContactCsvRow)[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /**\n * How the export names an owner and which custom fields it carries — the\n * table's own options, so the selection's file and the table's are one\n * format (AGL-2621).\n */\n csv?: ContactCsvOptions\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The owner, the stage and the company stand locked;\n * tags, the exports, the audience door and removing people from a site\n * draw no lock of their own.\n */\n suiteLocked?: boolean\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'contact', plural: 'contacts' }\n\n/** The one small dialog the value-taking actions share. */\ntype PendingAction = 'add-tag' | 'remove-tag' | 'owner' | 'stage' | 'company'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n 'add-tag': 'Add a tag',\n 'remove-tag': 'Remove a tag',\n owner: 'Set the owner',\n stage: 'Set the lifecycle stage',\n company: 'Set the company',\n}\n\n/** How a finished action reads, given how many rows it reached. */\nconst doneSentence = (action: PendingAction | 'detach', done: number): string => {\n const people = countNoun(done, NOUN)\n switch (action) {\n case 'add-tag':\n return `Tagged ${people}`\n case 'remove-tag':\n return `Removed the tag from ${people}`\n case 'owner':\n return `Owner set on ${people}`\n case 'stage':\n return `Stage set on ${people}`\n case 'company':\n return `Company set on ${people}`\n case 'detach':\n return `${people} removed from this site`\n }\n}\n\n/** The report's line for a row an act left out on purpose. */\nconst skippedLine = (row: ContactBulkSkip) => ({ label: row.email, reason: row.reason })\n\n/**\n * The bar, or nothing.\n *\n * The empty state is a separate component boundary on purpose: with no\n * selection the body below is never mounted, so none of its hooks run — no\n * roster hook armed, no dialog state held — on the visit to the list that\n * every reader makes and most never tick a row on.\n */\nexport function ContactsBulkBar(props: ContactsBulkBarProps) {\n if (!props.selected.length) return null\n return <ContactsBulkBarBody {...props} />\n}\nContactsBulkBar.displayName = 'ContactsBulkBar'\n\nfunction ContactsBulkBarBody(props: ContactsBulkBarProps) {\n const {\n hostId,\n org,\n scope,\n consentGroup,\n rows,\n selected,\n onSelectedChange,\n csv,\n suiteLocked = false,\n } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { confirm } = useConfirmationContext()\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'contact' })\n const contactUpdate = useContactUpdate(hostId)\n\n const selectedRows = useMemo(() => {\n const chosen = new Set(selected)\n return rows.filter((row) => chosen.has(row.$id))\n }, [rows, selected])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n const [company, setCompany] = useState<CompanyOption | null>(null)\n const [listOpen, setListOpen] = useState(false)\n\n /*\n * The roster, read only while the owner dialog is open. A bulk bar sits on\n * every visit to the list; the team is a request for a control nobody has\n * opened until they open it. The company list follows the same rule for\n * its own dialog (AGL-2613).\n */\n const team = useOrgMemberOptions(scope?.[1], { enabled: pending === 'owner' })\n const companies = useCompanyOptions({\n hostId,\n org,\n enabled: pending === 'company',\n })\n const createCompany = useCreateCompany({ hostId, org })\n // The site the audiences API is asked from: the mounted one, or at the\n // organization level the one the reader picked (AGL-2630).\n const { createHostId } = useCrmScope({ hostId, org })\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setCompany(null)\n setPending(action)\n }\n\n /**\n * The Firestore writers for the one act written client-direct: letting the\n * rows go, a batch at a time and a row at a time for a batch that failed.\n */\n const writers = useMemo(() => {\n const refFor = (id: string) =>\n doc(firestore, scope?.[0] ?? 'orgs', scope?.[1] ?? '', 'contacts', id)\n return {\n commitBatch: async (writes: readonly ContactBulkWrite[]) => {\n const batch = writeBatch(firestore)\n for (const write of writes) {\n if (write.kind === 'delete') batch.delete(refFor(write.id))\n else batch.update(refFor(write.id), write.data)\n }\n await batch.commit()\n },\n commitOne: async (write: ContactBulkWrite) => {\n if (write.kind === 'delete') await deleteDoc(refFor(write.id))\n else await updateDoc(refFor(write.id), write.data)\n },\n }\n }, [firestore, scope])\n\n /** One act's request for the rows it reaches, in the pieces the route accepts. */\n const saveThroughRoute = useCallback(\n (targets: readonly ContactBulkRow[], set: ContactUpdateFields) =>\n runCrmBulkBatch(\n targets,\n (row) => row.$id,\n contactBulkAddressOf,\n (piece) => contactUpdate.updateMany(piece.map((row) => row.$id), set),\n CRM_CONTACT_UPDATE_MAX,\n ),\n [contactUpdate],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending || !scope) return\n /*\n * A row no site holds has no facet to write. Under a site every selected\n * row is the viewing group's; at the organization level (AGL-2630) each\n * row was flattened through its own primary holder and is written back\n * through it, so a row nobody holds yet is left out and says so.\n */\n const held: ContactBulkRow[] = []\n const unheld: ContactBulkSkip[] = []\n for (const row of selectedRows) {\n if (consentGroup || row.groupId) held.push(row)\n else {\n unheld.push({\n email: contactBulkAddressOf(row),\n reason: 'no site holds this contact yet',\n })\n }\n }\n\n const planned = ((): { selection: ContactBulkSelection; job: () => Promise<CrmBulkOutcome> } | null => {\n if (pending === 'add-tag' || pending === 'remove-tag') {\n const tag = normalizeBulkTag(value)\n if (!tag) return null\n const selection =\n pending === 'add-tag' ? planAddTag(held, tag) : planRemoveTag(held, tag)\n const set: ContactUpdateFields =\n pending === 'add-tag' ? { addTag: tag } : { removeTag: tag }\n return { selection, job: () => saveThroughRoute(selection.rows, set) }\n }\n if (pending === 'owner') {\n return {\n selection: { rows: held, skipped: [] },\n job: () => saveThroughRoute(held, { ownerUid: value }),\n }\n }\n if (pending === 'company') {\n const companyId = company?.id ?? null\n const selection = planSetCompany(held, companyId)\n return { selection, job: () => saveThroughRoute(selection.rows, { companyId }) }\n }\n if (!value) return null\n const stage = value as ContactLifecycleStage\n return {\n selection: { rows: held, skipped: [] },\n /*\n * One row at a time through the one writer of a contact's stage, on\n * the site that holds the row: the mounted site, or at the\n * organization level the row's own holder's.\n */\n job: () =>\n runCrmBulkCalls(held, contactBulkAddressOf, async (row) => {\n const site = hostId ?? (row.holderHostId || null)\n if (!site) throw new Error('no site holds this contact yet')\n await setContactStage(user, site, row.$id, stage)\n }),\n }\n })()\n if (!planned) return\n const action = pending\n setPending(null)\n await apply({\n attempted: planned.selection.rows.length,\n skipped: [...unheld, ...planned.selection.skipped].map(skippedLine),\n job: planned.job,\n done: (count) => doneSentence(action, count),\n })\n }, [\n pending,\n scope,\n selectedRows,\n consentGroup,\n value,\n company,\n saveThroughRoute,\n hostId,\n user,\n apply,\n ])\n\n const handleExport = useCallback(() => {\n downloadTextFile(\n 'contacts-selected.csv',\n 'text/csv',\n contactsCsv(selectedRows, csv),\n )\n }, [selectedRows, csv])\n\n const handleDetach = useCallback(async () => {\n if (!scope || !selectedRows.length || !consentGroup) return\n const count = selectedRows.length\n const confirmed = await confirm({\n title: count === 1 ? 'Remove this contact?' : `Remove ${count} contacts?`,\n description:\n `${count === 1 ? 'This person is' : `These ${count} people are`} ` +\n \"removed from this site's Contacts, along with their notes, tags and \" +\n 'timeline. Other sites that captured the same people keep their own ' +\n 'records. Their form submissions, orders, bookings, and membership ' +\n 'records are separate — delete those from their own pages if the ' +\n 'request covers them.',\n confirmationText: count === 1 ? 'Remove contact' : 'Remove contacts',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel.\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n const plan = planDetach(selectedRows, consentGroup, Date.now())\n const outcome = await apply({\n attempted: plan.writes.length,\n skipped: plan.skipped.map(skippedLine),\n job: () => runCrmBulkWrites(writers, plan.writes, (write) => write.email),\n done: (done) => doneSentence('detach', done),\n })\n // The rows that went are gone from the table; the refused ones stay\n // selected, so the reader can see which they are and try again.\n const refused = new Set(outcome.refused.map((row) => row.label))\n onSelectedChange(\n selectedRows\n .filter((row) => refused.has(contactBulkAddressOf(row)))\n .map((row) => row.$id),\n )\n }, [scope, selectedRows, confirm, apply, writers, consentGroup, onSelectedChange])\n\n const emails = selectedRows\n .map((row) => String(row.email ?? '').trim())\n .filter(Boolean)\n\n return (\n <CrmBulkBarFrame\n count={selected.length}\n noun={NOUN}\n busy={busy}\n onClear={() => onSelectedChange([])}\n report={report}\n onDismissReport={dismissReport}\n extras={\n <>\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={\n !(pending === 'stage' && !value) &&\n !(\n (pending === 'add-tag' || pending === 'remove-tag') &&\n !normalizeBulkTag(value)\n )\n }\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'add-tag' || pending === 'remove-tag' ? (\n <TextField\n autoFocus\n size=\"small\"\n label=\"Tag\"\n placeholder=\"vip\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText={\n pending === 'add-tag'\n ? 'Lowercased, like the tags on a profile'\n : 'Removed wherever it is present'\n }\n />\n ) : pending === 'owner' ? (\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n error={Boolean(team.error)}\n helperText={team.error ?? (team.ready ? undefined : 'Loading the team…')}\n >\n <MenuItem value=\"\">{'Nobody — clear the owner'}</MenuItem>\n {team.options.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'stage' ? (\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n >\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'company' ? (\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={company?.id ?? null}\n onChange={(_id, picked) => setCompany(picked)}\n onCreate={createCompany}\n helperText=\"Leave it empty to unlink the selected contacts from their companies.\"\n />\n ) : null}\n </CrmBulkValueDialog>\n {listOpen ? (\n <AddToListDialog\n open\n onClose={() => setListOpen(false)}\n hostId={createHostId}\n scope={scope}\n emails={emails}\n />\n ) : null}\n </>\n }\n >\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('add-tag')}>\n {'Add tag'}\n </Button>\n <Button\n size=\"small\"\n disabled={busy || !scope}\n onClick={() => openAction('remove-tag')}\n >\n {'Remove tag'}\n </Button>\n {suiteLocked ? (\n <>\n <CrmSuiteLockedButton>{'Set owner'}</CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Set stage'}</CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Set company'}</CrmSuiteLockedButton>\n </>\n ) : (\n <>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('stage')}>\n {'Set stage'}\n </Button>\n <Button\n size=\"small\"\n disabled={busy || !scope}\n onClick={() => openAction('company')}\n >\n {'Set company'}\n </Button>\n </>\n )}\n <Button\n size=\"small\"\n disabled={busy || !scope || !emails.length || !createHostId}\n onClick={() => setListOpen(true)}\n >\n {'Add to list'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={handleExport}>\n {'Export CSV'}\n </Button>\n {/*\n The selection's file is the rows on screen; this one is the whole\n collection, streamed by the server (AGL-2662).\n */}\n <CrmExportAllButton\n resource=\"contacts\"\n orgId={scope?.[1] ?? null}\n hostId={hostId}\n disabled={busy}\n />\n {/*\n Only under a site. The act is a DETACH from the viewing site's\n CRM, and at the organization level there is no viewing site — an\n org-wide member removes a person from a site by opening the record\n under that site.\n */}\n {consentGroup ? (\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy || !scope}\n onClick={() => void handleDetach()}\n >\n {'Remove from this site'}\n </Button>\n ) : null}\n </CrmBulkBarFrame>\n )\n}\nContactsBulkBarBody.displayName = 'ContactsBulkBarBody'\n\nexport default ContactsBulkBar\n"],"names":["CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","crmMemberPickerLabel","useConfirmationContext","Button","MenuItem","TextField","deleteDoc","doc","updateDoc","writeBatch","useCallback","useMemo","useState","useFirestore","useOrgMemberOptions","useUser","useContactUpdate","useCrmBulkApply","useCrmScope","CRM_CONTACT_UPDATE_MAX","setContactStage","contactBulkAddressOf","normalizeBulkTag","planAddTag","planDetach","planRemoveTag","planSetCompany","contactsCsv","downloadTextFile","runCrmBulkBatch","runCrmBulkCalls","runCrmBulkWrites","AddToListDialog","CompanyPicker","useCompanyOptions","useCreateCompany","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","CrmSuiteLockedButton","NOUN","singular","plural","ACTION_TITLES","owner","stage","company","doneSentence","action","done","people","skippedLine","row","label","email","reason","ContactsBulkBar","props","selected","length","ContactsBulkBarBody","displayName","team","hostId","org","scope","consentGroup","rows","onSelectedChange","csv","suiteLocked","firestore","data","user","confirm","busy","report","apply","dismissReport","recordKind","contactUpdate","selectedRows","chosen","Set","filter","has","$id","pending","setPending","value","setValue","setCompany","listOpen","setListOpen","enabled","companies","createCompany","createHostId","openAction","writers","refFor","id","commitBatch","writes","batch","write","kind","delete","update","commit","commitOne","saveThroughRoute","targets","set","piece","updateMany","map","handleApply","held","unheld","groupId","push","planned","tag","selection","addTag","removeTag","job","skipped","ownerUid","companyId","site","holderHostId","Error","attempted","count","handleExport","handleDetach","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","plan","Date","now","outcome","refused","emails","String","trim","Boolean","noun","onClear","onDismissReport","extras","open","canApply","onClose","onApply","autoFocus","size","placeholder","onChange","event","target","helperText","select","error","ready","undefined","options","member","uid","truncated","_id","picked","onCreate","disabled","onClick","resource","orgId"],"mappings":"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"}
@@ -16,6 +16,7 @@
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
+ import * as Aglyn from "@aglyn/aglyn";
19
20
  import { CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_LIFECYCLE_STAGES, CONTACT_SOURCE_LABELS, CRM_CONTACT_VIEW_FIELDS, crmContactCustomColumn, crmViewFiltersFromSegment, crmViewSegmentFilters, newResourceScopeFields, ORG_SCOPE_TOKEN, pluginDocsHelp } from "@aglyn/aglyn";
20
21
  import { hiddenFilterVisibility, matchListFilter } from "@aglyn/shared-ui-jsx/const/list-filter";
21
22
  import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
@@ -529,7 +530,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
529
530
  ] : [],
530
531
  ...members.options.filter((option)=>option.uid !== uid).map((option)=>({
531
532
  value: option.uid,
532
- label: option.label
533
+ label: Aglyn.crmMemberPickerLabel(option)
533
534
  }))
534
535
  ],
535
536
  [CRM_CONTACT_VIEW_FIELDS.stage]: CONTACT_LIFECYCLE_STAGES.map((stage)=>({