@aglyn/plugins-crm 1.0.0-beta.164 → 1.0.0-beta.166
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/lib/components/contact-associations-card.js +21 -5
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-field-drawer.js +19 -2
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/components/crm-console-page.js +12 -7
- package/src/lib/components/crm-console-page.js.map +1 -1
- package/src/lib/components/crm-glance-card.js +4 -2
- package/src/lib/components/crm-glance-card.js.map +1 -1
- package/src/lib/components/fields-section.d.ts +2 -1
- package/src/lib/components/fields-section.js +6 -3
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.d.ts +59 -0
- package/src/lib/components/lead-campaigns-card.js +166 -0
- package/src/lib/components/lead-campaigns-card.js.map +1 -0
- package/src/lib/components/lead-detail-page.js +54 -4
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-history-card.js +5 -0
- package/src/lib/components/lead-history-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.d.ts +12 -0
- package/src/lib/components/lead-properties-card.js +64 -6
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.js +7 -1
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.d.ts +7 -2
- package/src/lib/components/leads-bulk-bar.js +42 -6
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +43 -17
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-lead-drawer.d.ts +14 -1
- package/src/lib/components/new-lead-drawer.js +41 -3
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/components/reports/lead-counts.d.ts +16 -9
- package/src/lib/components/reports/lead-counts.js +29 -18
- package/src/lib/components/reports/lead-counts.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +44 -40
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/hooks/use-campaign-filing-log.d.ts +29 -0
- package/src/lib/hooks/use-campaign-filing-log.js +91 -0
- package/src/lib/hooks/use-campaign-filing-log.js.map +1 -0
- package/src/lib/hooks/use-org-leads.d.ts +29 -32
- package/src/lib/hooks/use-org-leads.js +58 -68
- package/src/lib/hooks/use-org-leads.js.map +1 -1
- package/src/lib/model/campaign-filing-activity.d.ts +82 -0
- package/src/lib/model/campaign-filing-activity.js +88 -0
- package/src/lib/model/campaign-filing-activity.js.map +1 -0
- package/src/lib/model/crm-routes.d.ts +9 -6
- package/src/lib/model/crm-routes.js +9 -6
- package/src/lib/model/crm-routes.js.map +1 -1
- package/src/lib/server/campaign-filing-activity.d.ts +79 -0
- package/src/lib/server/campaign-filing-activity.js +93 -0
- package/src/lib/server/campaign-filing-activity.js.map +1 -0
- package/src/lib/server/capture-contact.js +11 -5
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/convert-open-lead.js +6 -2
- package/src/lib/server/convert-open-lead.js.map +1 -1
- package/src/lib/server/email-send.js +3 -3
- package/src/lib/server/email-send.js.map +1 -1
- package/src/lib/server/erase-person.js +25 -7
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/lead-campaign-carry.js +28 -2
- package/src/lib/server/lead-campaign-carry.js.map +1 -1
- package/src/lib/server/lead-create.d.ts +15 -4
- package/src/lib/server/lead-create.js +75 -15
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/leads-import.js +2 -2
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server/record-email-state.js +10 -0
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-facts.js +3 -3
- package/src/lib/server/record-facts.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/fields-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n CONTACT_FIELD_TYPE_LABELS,\n type ConsolePluginPageProps,\n createResourceUid,\n CRM_COLLECTIONS,\n CRM_FIELD_OBJECT_LABELS,\n CRM_FIELD_OBJECTS,\n type CrmFieldObject,\n isCrmFieldObject,\n newResourceScopeFields,\n ORG_SCOPE_TOKEN,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport {\n mdiArchiveArrowUpOutline,\n mdiArchiveOutline,\n mdiArrowDown,\n mdiArrowUp,\n mdiDeleteOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n CardDisplay,\n MdiIcon,\n SrOnly,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Chip,\n IconButton,\n Stack,\n Tab,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Tabs,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n deleteDoc,\n doc,\n setDoc,\n updateDoc,\n writeBatch,\n} from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport {\n type ContactFieldDefinitionDoc,\n useContactFieldDefinitions,\n} from '../hooks/use-contact-field-definitions'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport ContactFieldDrawer, { type ContactFieldDraft } from './contact-field-drawer'\nimport { recomputeAllCrmNextActivity } from '../model/next-activity-api'\nimport { crmTaskCallScope } from '../model/task-routes'\n\nexport type ContactsFieldsSectionProps = Pick<ConsolePluginPageProps, 'hostId' | 'org'>\n\n/** What each tab says the fields are for, above its list. */\nconst OBJECT_INTRO: Record<CrmFieldObject, string> = {\n contact:\n 'The custom fields on a contact — text, number, date, choice, ' +\n 'checkbox or link. They show on every contact, as columns on the ' +\n 'list, and a form field can save into one.',\n company:\n 'The custom fields on a company. They show on every company’s page ' +\n 'and its edit form, as columns on the companies list, and a CSV ' +\n 'import can fill them.',\n deal:\n 'The custom fields on a deal. They show on every deal’s page and ' +\n 'its edit form, and as columns on the deals table.',\n}\n\n/** The noun each tab's empty state and captions use. */\nconst OBJECT_NOUN: Record<CrmFieldObject, string> = {\n contact: 'contact',\n company: 'company',\n deal: 'deal',\n}\n\n/**\n * `/crm/fields` — the custom fields a holder keeps on a person (AGL-2601),\n * and since AGL-2661 on a company and a deal too, one tab each.\n *\n * Definitions live in `orgs/{orgId}/contactFields`, one document per field,\n * and the VALUES live under each contact facet's `custom` keyed by the\n * definition's `key`. That split decides everything this section does:\n *\n * - A field is RETIRED, not deleted, while values may exist under it. A\n * retired field leaves every form and every column, but an export can\n * still read what was written, and a restore brings it back intact.\n * Delete is offered only on a retired field, because retiring is the step\n * where the author had to look at what they were losing.\n * - The KEY never changes. It is the map key every value sits under; a\n * rename is a new field and a retire.\n * - Order is a stored `order` on each document, moved with the arrows here,\n * and every reader sorts by it — the profile card, the columns, an export.\n *\n * ## What is NOT on this page\n *\n * How many contacts carry a value under each field. That count is a read of\n * every contact document in the org — the expensive-read shape this codebase\n * has a standing rule against — and it would be paid on every visit to a\n * settings page. The list says so rather than showing a number it cannot\n * afford to keep true.\n *\n * Definitions are ORG-WIDE (`visibleTo: ['org']`): a field is a fact about\n * how this business describes people, and every site in the org files\n * people into the same address book. `hostId` records which site defined it.\n */\nexport function ContactsFieldsSection(props: ContactsFieldsSectionProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The org root from the one scope hook (AGL-2614), which also answers at\n // the organization level (AGL-2630). A definition is org-wide either way;\n // the defining site it records is the mounted one, or the picked one, or\n // none for a field defined over the whole org before any pick.\n const { scope, ready: scopeReady, createHostId } = useCrmScope({ hostId, org })\n const orgId = scope?.[1] ?? null\n /*\n * ONE TAB PER OBJECT (AGL-2661). The list, the drawer and every write\n * below are about the tab's object: the hook narrows the org's one\n * definition list to it, a new field is stamped with it, and the drawer\n * refuses a key only against the keys THIS object already uses — a\n * company and a contact may both define `region`.\n */\n const [object, setObject] = useState<CrmFieldObject>('contact')\n const { definitions, ready, fromCache } = useContactFieldDefinitions(orgId, object)\n const noun = OBJECT_NOUN[object]\n\n /*\n * MAINTENANCE: recompute every record's next activity (AGL-2661). The\n * figure is kept by every task write, so this is for the records written\n * before it existed and for whatever a write that could not reach the\n * route left behind. A console action rather than a script, because the\n * person who notices a stale column is the person who should be able to\n * fix it.\n */\n const { data: user } = useUser()\n const [recomputing, setRecomputing] = useState(false)\n const recomputeNextActivity = useCallback(async () => {\n const callScope = crmTaskCallScope(hostId, orgId)\n if (!callScope || recomputing) return\n const confirmed = await confirm({\n title: 'Recompute next activity?',\n description:\n 'Every contact, company and deal in this organization gets its ' +\n '\"Next activity\" recomputed from its open tasks. Safe to run any time; ' +\n 'it only rewrites a figure the task list already implies.',\n confirmationText: 'Recompute',\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n setRecomputing(true)\n try {\n const result = await recomputeAllCrmNextActivity(user, callScope)\n enqueueSnackbar(\n `Next activity recomputed: ${result.records.toLocaleString()} record${result.records === 1 ? '' : 's'} ` +\n `from ${(result.tasks ?? 0).toLocaleString()} open task${result.tasks === 1 ? '' : 's'}` +\n (result.truncated\n ? ' — the first batch of open tasks only; nothing stale was cleared'\n : ''),\n { variant: 'success' },\n )\n } catch (cause) {\n enqueueSnackbar(\n cause instanceof Error ? cause.message : 'The next activity could not be recomputed.',\n { variant: 'warning' },\n )\n } finally {\n setRecomputing(false)\n }\n }, [hostId, orgId, recomputing, confirm, user, enqueueSnackbar])\n\n const [drawerOpen, setDrawerOpen] = useState(false)\n const [editing, setEditing] = useState<ContactFieldDefinitionDoc | null>(null)\n const [busyId, setBusyId] = useState('')\n\n const fieldRef = useCallback(\n (id: string) => {\n if (!scope) throw new Error('a contact field needs an organization')\n return doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.contactFields, id)\n },\n [firestore, scope],\n )\n\n const openCreate = useCallback(() => {\n setEditing(null)\n setDrawerOpen(true)\n }, [])\n const openEdit = useCallback((definition: ContactFieldDefinitionDoc) => {\n setEditing(definition)\n setDrawerOpen(true)\n }, [])\n\n /**\n * Create, or save the editable half of an existing field.\n *\n * On create the document carries everything a reader expects: the key,\n * an `order` past every existing one so it lands last, an explicit\n * `retiredAt: null` so a `where('retiredAt', '==', null)` can find it, the\n * defining site, and the org-wide scope stamp the rules require. On edit\n * only the label, the choices and the required flag move — the key and\n * the type are the drawer's disabled inputs, and this writer does not\n * take them either.\n */\n const handleSubmit = useCallback(\n async (draft: ContactFieldDraft) => {\n if (!scope) return\n const now = new Date()\n if (editing) {\n const verdict = await writeGuardedBySeed(\n { subject: 'field', fromCache },\n async () => {\n await updateDoc(fieldRef(editing.$id), {\n label: draft.label,\n ...(editing.type === 'select' ? { options: draft.options } : {}),\n required: draft.required,\n updatedAt: now,\n })\n },\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n enqueueSnackbar('Field saved', { variant: 'success', persist: false })\n } else {\n const order =\n definitions.reduce(\n (highest, definition) => Math.max(highest, Number(definition.order) || 0),\n -1,\n ) + 1\n await setDoc(\n doc(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.contactFields),\n createResourceUid(),\n ),\n {\n key: draft.key,\n label: draft.label,\n type: draft.type,\n ...(draft.type === 'select' ? { options: draft.options } : {}),\n required: draft.required,\n order,\n retiredAt: null,\n // Which record the field describes — the tab it was made on.\n object,\n hostId: createHostId,\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n createdAt: now,\n updatedAt: now,\n },\n )\n enqueueSnackbar(`Field \"${draft.label}\" added`, {\n variant: 'success',\n persist: false,\n })\n }\n setDrawerOpen(false)\n setEditing(null)\n },\n [scope, editing, fromCache, fieldRef, enqueueSnackbar, definitions, firestore, createHostId, object],\n )\n\n /**\n * Move a field one place up or down.\n *\n * Writes `order` as the POSITION for every row whose stored order is not\n * already its position, in one batch. Swapping two numbers would be enough\n * on a list whose orders are already distinct, and wrong on one where they\n * are not — every field created before this section existed carries the\n * same `order`, and a swap of two equal numbers moves nothing. Renumbering\n * what is out of place makes the first move on such a list also the one\n * that normalizes it, at the cost of a batch that is at most the list.\n */\n const move = useCallback(\n async (definition: ContactFieldDefinitionDoc, direction: -1 | 1) => {\n if (!scope || busyId) return\n const index = definitions.findIndex((entry) => entry.$id === definition.$id)\n const target = index + direction\n if (index < 0 || target < 0 || target >= definitions.length) return\n const next = [...definitions]\n next[index] = definitions[target]\n next[target] = definitions[index]\n setBusyId(definition.$id)\n try {\n const verdict = await writeGuardedBySeed(\n { subject: 'field order', fromCache },\n async () => {\n const batch = writeBatch(firestore)\n const now = new Date()\n next.forEach((entry, position) => {\n if (entry.order === position) return\n batch.update(fieldRef(entry.$id), { order: position, updatedAt: now })\n })\n await batch.commit()\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyId('')\n }\n },\n [scope, busyId, definitions, fromCache, firestore, fieldRef, enqueueSnackbar],\n )\n\n /**\n * Retire a field, or bring one back.\n *\n * Retiring is asked about because it takes a control off every contact\n * form and a column off the list; restoring is not, because it puts a\n * choice back and is undone by retiring again. Neither touches a value.\n */\n const toggleRetired = useCallback(\n async (definition: ContactFieldDefinitionDoc) => {\n if (!scope || busyId) return\n if (!definition.retiredAt) {\n const accepted = await confirm({\n title: `Retire “${definition.label}”?`,\n description:\n `It leaves every ${noun} form, column and form-field mapping. ` +\n 'Values already saved under it are kept and still export, and ' +\n 'you can restore the field at any time.',\n confirmationText: 'Retire',\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating\n // on the resolved value alone would always fall through.\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n }\n setBusyId(definition.$id)\n try {\n await updateDoc(fieldRef(definition.$id), {\n retiredAt: definition.retiredAt ? null : Date.now(),\n updatedAt: new Date(),\n })\n enqueueSnackbar(definition.retiredAt ? 'Field restored' : 'Field retired', {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyId('')\n }\n },\n [scope, busyId, confirm, fieldRef, enqueueSnackbar, noun],\n )\n\n /**\n * Delete a RETIRED field's definition.\n *\n * The values under its key are not touched — they sit on contact documents\n * this page does not read — so what is destroyed is the ability to show\n * them anywhere. The dialog says exactly that, and the key stays taken for\n * as long as the list can see it, which after this write it cannot; a new\n * field with the same key would read the orphaned values back as its own.\n */\n const handleDelete = useCallback(\n async (definition: ContactFieldDefinitionDoc) => {\n if (!scope || busyId || !definition.retiredAt) return\n const accepted = await confirm({\n title: `Delete “${definition.label}”?`,\n description:\n 'The definition is removed for good. Values saved under its key ' +\n `(${definition.key}) stay on the ${noun}s that carry them but ` +\n 'nothing will show them again — and a new field created with the ' +\n 'same key would read them as its own.',\n confirmationText: 'Delete field',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setBusyId(definition.$id)\n try {\n await deleteDoc(fieldRef(definition.$id))\n enqueueSnackbar('Field deleted', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyId('')\n }\n },\n [scope, busyId, confirm, fieldRef, enqueueSnackbar, noun],\n )\n\n const rowActions = (definition: ContactFieldDefinitionDoc): RowActionsMenuItem[] => [\n {\n key: 'edit',\n label: 'Edit field',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n onClick: () => openEdit(definition),\n },\n {\n key: 'retire',\n label: definition.retiredAt ? 'Restore' : 'Retire',\n icon: (\n <MdiIcon\n path={definition.retiredAt ? mdiArchiveArrowUpOutline.path : mdiArchiveOutline.path}\n size={0.8}\n />\n ),\n destructive: !definition.retiredAt,\n disabled: Boolean(busyId),\n disabledReason: busyId ? 'Another field is being saved' : undefined,\n onClick: () => void toggleRetired(definition),\n },\n ...(definition.retiredAt\n ? [\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: Boolean(busyId),\n disabledReason: busyId ? 'Another field is being saved' : undefined,\n onClick: () => void handleDelete(definition),\n } satisfies RowActionsMenuItem,\n ]\n : []),\n ]\n\n return (\n <CardDisplay\n header={'Fields'}\n help={pluginDocsHelp('contactFields', { anchor: '#define-a-field' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: scope ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button size=\"small\" onClick={() => void recomputeNextActivity()} disabled={recomputing}>\n {recomputing ? 'Recomputing…' : 'Recompute next activity'}\n </Button>\n <Button variant=\"contained\" onClick={openCreate}>\n {'New field'}\n </Button>\n </Stack>\n ) : null,\n }}\n >\n <Stack spacing={2}>\n <Tabs\n value={object}\n onChange={(_event, next) => {\n if (isCrmFieldObject(next)) setObject(next)\n }}\n aria-label=\"Fields by record\"\n >\n {CRM_FIELD_OBJECTS.map((entry) => (\n <Tab key={entry} value={entry} label={CRM_FIELD_OBJECT_LABELS[entry]} />\n ))}\n </Tabs>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${OBJECT_INTRO[object]} Fields are shared across every site in this ` +\n 'organization, like the records themselves.'}\n </Typography>\n {scopeReady && !scope ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`This site has no organization, so it has no ${noun} fields.`}\n </Typography>\n ) : !ready ? null : definitions.length === 0 ? (\n <EmptyStateComponent\n label={`No custom ${noun} fields yet`}\n description={`A field you define here is kept on every ${noun} and shows on its page.`}\n action={\n scope ? (\n <Button variant=\"contained\" onClick={openCreate}>\n {'New field'}\n </Button>\n ) : undefined\n }\n />\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell sx={{ width: 96 }}>{'Order'}</TableCell>\n <TableCell>{'Field'}</TableCell>\n <TableCell>{'Key'}</TableCell>\n <TableCell>{'Type'}</TableCell>\n <TableCell>{'Required'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {definitions.map((definition, index) => (\n <TableRow\n key={definition.$id}\n hover\n sx={definition.retiredAt ? { opacity: 0.6 } : undefined}\n >\n <TableCell>\n <Stack direction=\"row\" spacing={0}>\n <IconButton\n size=\"small\"\n disabled={index === 0 || Boolean(busyId)}\n onClick={() => void move(definition, -1)}\n >\n <MdiIcon path={mdiArrowUp.path} size={0.7} />\n <SrOnly>{`Move ${definition.label} up`}</SrOnly>\n </IconButton>\n <IconButton\n size=\"small\"\n disabled={index === definitions.length - 1 || Boolean(busyId)}\n onClick={() => void move(definition, 1)}\n >\n <MdiIcon path={mdiArrowDown.path} size={0.7} />\n <SrOnly>{`Move ${definition.label} down`}</SrOnly>\n </IconButton>\n </Stack>\n </TableCell>\n <TableCell>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', alignItems: 'center' }}\n >\n <Typography variant=\"body2\">{definition.label}</Typography>\n {definition.retiredAt ? (\n <Chip size=\"small\" variant=\"outlined\" color=\"warning\" label=\"Retired\" />\n ) : null}\n </Stack>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ fontFamily: 'monospace' }}>\n {definition.key}\n </Typography>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\">\n {CONTACT_FIELD_TYPE_LABELS[definition.type] ?? definition.type}\n {definition.type === 'select' && definition.options?.length\n ? ` · ${definition.options.length} choices`\n : ''}\n </Typography>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {definition.required ? 'Yes' : '—'}\n </Typography>\n </TableCell>\n <TableCell align=\"right\" sx={{ width: 56 }}>\n <RowActionsMenu label={definition.label} items={rowActions(definition)} />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n )}\n {definitions.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`How many ${noun}s carry a value under each field is not ` +\n `counted — that would read every ${noun} in the organization ` +\n 'each time this page opened.'}\n </Typography>\n ) : null}\n </Stack>\n <ContactFieldDrawer\n open={drawerOpen}\n onClose={() => setDrawerOpen(false)}\n definition={editing}\n object={object}\n takenKeys={definitions.map((definition) => definition.key)}\n onSubmit={handleSubmit}\n />\n </CardDisplay>\n )\n}\nContactsFieldsSection.displayName = 'ContactsFieldsSection'\n\nexport default ContactsFieldsSection\n"],"names":["CONTACT_FIELD_TYPE_LABELS","createResourceUid","CRM_COLLECTIONS","CRM_FIELD_OBJECT_LABELS","CRM_FIELD_OBJECTS","isCrmFieldObject","newResourceScopeFields","ORG_SCOPE_TOKEN","pluginDocsHelp","EmptyStateComponent","mdiArchiveArrowUpOutline","mdiArchiveOutline","mdiArrowDown","mdiArrowUp","mdiDeleteOutline","mdiPencilOutline","CardDisplay","MdiIcon","SrOnly","useConfirmationContext","RowActionsMenu","ScrollTable","useSnackbar","useFirestore","useUser","writeGuardedBySeed","Button","Chip","IconButton","Stack","Tab","TableBody","TableCell","TableHead","TableRow","Tabs","Typography","collection","deleteDoc","doc","setDoc","updateDoc","writeBatch","useCallback","useState","useContactFieldDefinitions","useCrmScope","ContactFieldDrawer","recomputeAllCrmNextActivity","crmTaskCallScope","OBJECT_INTRO","contact","company","deal","OBJECT_NOUN","ContactsFieldsSection","props","hostId","org","firestore","enqueueSnackbar","confirm","scope","ready","scopeReady","createHostId","orgId","object","setObject","definitions","fromCache","noun","data","user","recomputing","setRecomputing","recomputeNextActivity","callScope","confirmed","title","description","confirmationText","then","catch","result","records","toLocaleString","tasks","truncated","variant","cause","Error","message","drawerOpen","setDrawerOpen","editing","setEditing","busyId","setBusyId","fieldRef","id","contactFields","openCreate","openEdit","definition","handleSubmit","draft","now","Date","verdict","subject","$id","label","type","options","required","updatedAt","ok","persist","order","reduce","highest","Math","max","Number","key","retiredAt","createdAt","move","direction","index","findIndex","entry","target","length","next","batch","forEach","position","update","commit","error","console","allowDuplicate","toggleRetired","accepted","handleDelete","confirmationButtonProps","color","rowActions","icon","path","size","onClick","destructive","disabled","Boolean","disabledReason","undefined","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","spacing","sx","alignItems","value","onChange","_event","aria-label","map","width","align","hover","opacity","flexWrap","fontFamily","items","open","onClose","takenKeys","onSubmit","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/fields-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n CONTACT_FIELD_TYPE_LABELS,\n type ConsolePluginPageProps,\n createResourceUid,\n CRM_COLLECTIONS,\n CRM_FIELD_OBJECT_LABELS,\n CRM_FIELD_OBJECTS,\n type CrmFieldObject,\n isCrmFieldObject,\n newResourceScopeFields,\n ORG_SCOPE_TOKEN,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport {\n mdiArchiveArrowUpOutline,\n mdiArchiveOutline,\n mdiArrowDown,\n mdiArrowUp,\n mdiDeleteOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n CardDisplay,\n MdiIcon,\n SrOnly,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Chip,\n IconButton,\n Stack,\n Tab,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Tabs,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n deleteDoc,\n doc,\n setDoc,\n updateDoc,\n writeBatch,\n} from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport {\n type ContactFieldDefinitionDoc,\n useContactFieldDefinitions,\n} from '../hooks/use-contact-field-definitions'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport ContactFieldDrawer, { type ContactFieldDraft } from './contact-field-drawer'\nimport { recomputeAllCrmNextActivity } from '../model/next-activity-api'\nimport { crmTaskCallScope } from '../model/task-routes'\n\nexport type ContactsFieldsSectionProps = Pick<ConsolePluginPageProps, 'hostId' | 'org'>\n\n/** What each tab says the fields are for, above its list. */\nconst OBJECT_INTRO: Record<CrmFieldObject, string> = {\n contact:\n 'The custom fields on a contact — text, number, date, choice, ' +\n 'checkbox or link. They show on every contact, as columns on the ' +\n 'list, and a form field can save into one.',\n company:\n 'The custom fields on a company. They show on every company’s page ' +\n 'and its edit form, as columns on the companies list, and a CSV ' +\n 'import can fill them.',\n deal:\n 'The custom fields on a deal. They show on every deal’s page and ' +\n 'its edit form, and as columns on the deals table.',\n lead:\n 'The custom fields on a lead. They show on every lead’s page and the ' +\n 'New lead drawer, and as columns on the leads list. They stay on the ' +\n 'lead when it converts — a contact’s fields are its own.',\n}\n\n/** The noun each tab's empty state and captions use. */\nconst OBJECT_NOUN: Record<CrmFieldObject, string> = {\n contact: 'contact',\n company: 'company',\n deal: 'deal',\n lead: 'lead',\n}\n\n/**\n * `/crm/fields` — the custom fields a holder keeps on a person (AGL-2601),\n * since AGL-2661 on a company and a deal too, and since AGL-3272 on a lead:\n * one tab each, and every CRM record describable in the org's own words.\n *\n * Definitions live in `orgs/{orgId}/contactFields`, one document per field,\n * and the VALUES live under each contact facet's `custom` keyed by the\n * definition's `key`. That split decides everything this section does:\n *\n * - A field is RETIRED, not deleted, while values may exist under it. A\n * retired field leaves every form and every column, but an export can\n * still read what was written, and a restore brings it back intact.\n * Delete is offered only on a retired field, because retiring is the step\n * where the author had to look at what they were losing.\n * - The KEY never changes. It is the map key every value sits under; a\n * rename is a new field and a retire.\n * - Order is a stored `order` on each document, moved with the arrows here,\n * and every reader sorts by it — the profile card, the columns, an export.\n *\n * ## What is NOT on this page\n *\n * How many contacts carry a value under each field. That count is a read of\n * every contact document in the org — the expensive-read shape this codebase\n * has a standing rule against — and it would be paid on every visit to a\n * settings page. The list says so rather than showing a number it cannot\n * afford to keep true.\n *\n * Definitions are ORG-WIDE (`visibleTo: ['org']`): a field is a fact about\n * how this business describes people, and every site in the org files\n * people into the same address book. `hostId` records which site defined it.\n */\nexport function ContactsFieldsSection(props: ContactsFieldsSectionProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The org root from the one scope hook (AGL-2614), which also answers at\n // the organization level (AGL-2630). A definition is org-wide either way;\n // the defining site it records is the mounted one, or the picked one, or\n // none for a field defined over the whole org before any pick.\n const { scope, ready: scopeReady, createHostId } = useCrmScope({ hostId, org })\n const orgId = scope?.[1] ?? null\n /*\n * ONE TAB PER OBJECT (AGL-2661). The list, the drawer and every write\n * below are about the tab's object: the hook narrows the org's one\n * definition list to it, a new field is stamped with it, and the drawer\n * refuses a key only against the keys THIS object already uses — a\n * company and a contact may both define `region`.\n */\n const [object, setObject] = useState<CrmFieldObject>('contact')\n const { definitions, ready, fromCache } = useContactFieldDefinitions(orgId, object)\n const noun = OBJECT_NOUN[object]\n\n /*\n * MAINTENANCE: recompute every record's next activity (AGL-2661). The\n * figure is kept by every task write, so this is for the records written\n * before it existed and for whatever a write that could not reach the\n * route left behind. A console action rather than a script, because the\n * person who notices a stale column is the person who should be able to\n * fix it.\n */\n const { data: user } = useUser()\n const [recomputing, setRecomputing] = useState(false)\n const recomputeNextActivity = useCallback(async () => {\n const callScope = crmTaskCallScope(hostId, orgId)\n if (!callScope || recomputing) return\n const confirmed = await confirm({\n title: 'Recompute next activity?',\n description:\n 'Every contact, company and deal in this organization gets its ' +\n '\"Next activity\" recomputed from its open tasks. Safe to run any time; ' +\n 'it only rewrites a figure the task list already implies.',\n confirmationText: 'Recompute',\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n setRecomputing(true)\n try {\n const result = await recomputeAllCrmNextActivity(user, callScope)\n enqueueSnackbar(\n `Next activity recomputed: ${result.records.toLocaleString()} record${result.records === 1 ? '' : 's'} ` +\n `from ${(result.tasks ?? 0).toLocaleString()} open task${result.tasks === 1 ? '' : 's'}` +\n (result.truncated\n ? ' — the first batch of open tasks only; nothing stale was cleared'\n : ''),\n { variant: 'success' },\n )\n } catch (cause) {\n enqueueSnackbar(\n cause instanceof Error ? cause.message : 'The next activity could not be recomputed.',\n { variant: 'warning' },\n )\n } finally {\n setRecomputing(false)\n }\n }, [hostId, orgId, recomputing, confirm, user, enqueueSnackbar])\n\n const [drawerOpen, setDrawerOpen] = useState(false)\n const [editing, setEditing] = useState<ContactFieldDefinitionDoc | null>(null)\n const [busyId, setBusyId] = useState('')\n\n const fieldRef = useCallback(\n (id: string) => {\n if (!scope) throw new Error('a contact field needs an organization')\n return doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.contactFields, id)\n },\n [firestore, scope],\n )\n\n const openCreate = useCallback(() => {\n setEditing(null)\n setDrawerOpen(true)\n }, [])\n const openEdit = useCallback((definition: ContactFieldDefinitionDoc) => {\n setEditing(definition)\n setDrawerOpen(true)\n }, [])\n\n /**\n * Create, or save the editable half of an existing field.\n *\n * On create the document carries everything a reader expects: the key,\n * an `order` past every existing one so it lands last, an explicit\n * `retiredAt: null` so a `where('retiredAt', '==', null)` can find it, the\n * defining site, and the org-wide scope stamp the rules require. On edit\n * only the label, the choices and the required flag move — the key and\n * the type are the drawer's disabled inputs, and this writer does not\n * take them either.\n */\n const handleSubmit = useCallback(\n async (draft: ContactFieldDraft) => {\n if (!scope) return\n const now = new Date()\n if (editing) {\n const verdict = await writeGuardedBySeed(\n { subject: 'field', fromCache },\n async () => {\n await updateDoc(fieldRef(editing.$id), {\n label: draft.label,\n ...(editing.type === 'select' ? { options: draft.options } : {}),\n required: draft.required,\n updatedAt: now,\n })\n },\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n enqueueSnackbar('Field saved', { variant: 'success', persist: false })\n } else {\n const order =\n definitions.reduce(\n (highest, definition) => Math.max(highest, Number(definition.order) || 0),\n -1,\n ) + 1\n await setDoc(\n doc(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.contactFields),\n createResourceUid(),\n ),\n {\n key: draft.key,\n label: draft.label,\n type: draft.type,\n ...(draft.type === 'select' ? { options: draft.options } : {}),\n required: draft.required,\n order,\n retiredAt: null,\n // Which record the field describes — the tab it was made on.\n object,\n hostId: createHostId,\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n createdAt: now,\n updatedAt: now,\n },\n )\n enqueueSnackbar(`Field \"${draft.label}\" added`, {\n variant: 'success',\n persist: false,\n })\n }\n setDrawerOpen(false)\n setEditing(null)\n },\n [scope, editing, fromCache, fieldRef, enqueueSnackbar, definitions, firestore, createHostId, object],\n )\n\n /**\n * Move a field one place up or down.\n *\n * Writes `order` as the POSITION for every row whose stored order is not\n * already its position, in one batch. Swapping two numbers would be enough\n * on a list whose orders are already distinct, and wrong on one where they\n * are not — every field created before this section existed carries the\n * same `order`, and a swap of two equal numbers moves nothing. Renumbering\n * what is out of place makes the first move on such a list also the one\n * that normalizes it, at the cost of a batch that is at most the list.\n */\n const move = useCallback(\n async (definition: ContactFieldDefinitionDoc, direction: -1 | 1) => {\n if (!scope || busyId) return\n const index = definitions.findIndex((entry) => entry.$id === definition.$id)\n const target = index + direction\n if (index < 0 || target < 0 || target >= definitions.length) return\n const next = [...definitions]\n next[index] = definitions[target]\n next[target] = definitions[index]\n setBusyId(definition.$id)\n try {\n const verdict = await writeGuardedBySeed(\n { subject: 'field order', fromCache },\n async () => {\n const batch = writeBatch(firestore)\n const now = new Date()\n next.forEach((entry, position) => {\n if (entry.order === position) return\n batch.update(fieldRef(entry.$id), { order: position, updatedAt: now })\n })\n await batch.commit()\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyId('')\n }\n },\n [scope, busyId, definitions, fromCache, firestore, fieldRef, enqueueSnackbar],\n )\n\n /**\n * Retire a field, or bring one back.\n *\n * Retiring is asked about because it takes a control off every contact\n * form and a column off the list; restoring is not, because it puts a\n * choice back and is undone by retiring again. Neither touches a value.\n */\n const toggleRetired = useCallback(\n async (definition: ContactFieldDefinitionDoc) => {\n if (!scope || busyId) return\n if (!definition.retiredAt) {\n const accepted = await confirm({\n title: `Retire “${definition.label}”?`,\n description:\n `It leaves every ${noun} form, column and form-field mapping. ` +\n 'Values already saved under it are kept and still export, and ' +\n 'you can restore the field at any time.',\n confirmationText: 'Retire',\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating\n // on the resolved value alone would always fall through.\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n }\n setBusyId(definition.$id)\n try {\n await updateDoc(fieldRef(definition.$id), {\n retiredAt: definition.retiredAt ? null : Date.now(),\n updatedAt: new Date(),\n })\n enqueueSnackbar(definition.retiredAt ? 'Field restored' : 'Field retired', {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyId('')\n }\n },\n [scope, busyId, confirm, fieldRef, enqueueSnackbar, noun],\n )\n\n /**\n * Delete a RETIRED field's definition.\n *\n * The values under its key are not touched — they sit on contact documents\n * this page does not read — so what is destroyed is the ability to show\n * them anywhere. The dialog says exactly that, and the key stays taken for\n * as long as the list can see it, which after this write it cannot; a new\n * field with the same key would read the orphaned values back as its own.\n */\n const handleDelete = useCallback(\n async (definition: ContactFieldDefinitionDoc) => {\n if (!scope || busyId || !definition.retiredAt) return\n const accepted = await confirm({\n title: `Delete “${definition.label}”?`,\n description:\n 'The definition is removed for good. Values saved under its key ' +\n `(${definition.key}) stay on the ${noun}s that carry them but ` +\n 'nothing will show them again — and a new field created with the ' +\n 'same key would read them as its own.',\n confirmationText: 'Delete field',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setBusyId(definition.$id)\n try {\n await deleteDoc(fieldRef(definition.$id))\n enqueueSnackbar('Field deleted', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyId('')\n }\n },\n [scope, busyId, confirm, fieldRef, enqueueSnackbar, noun],\n )\n\n const rowActions = (definition: ContactFieldDefinitionDoc): RowActionsMenuItem[] => [\n {\n key: 'edit',\n label: 'Edit field',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n onClick: () => openEdit(definition),\n },\n {\n key: 'retire',\n label: definition.retiredAt ? 'Restore' : 'Retire',\n icon: (\n <MdiIcon\n path={definition.retiredAt ? mdiArchiveArrowUpOutline.path : mdiArchiveOutline.path}\n size={0.8}\n />\n ),\n destructive: !definition.retiredAt,\n disabled: Boolean(busyId),\n disabledReason: busyId ? 'Another field is being saved' : undefined,\n onClick: () => void toggleRetired(definition),\n },\n ...(definition.retiredAt\n ? [\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: Boolean(busyId),\n disabledReason: busyId ? 'Another field is being saved' : undefined,\n onClick: () => void handleDelete(definition),\n } satisfies RowActionsMenuItem,\n ]\n : []),\n ]\n\n return (\n <CardDisplay\n header={'Fields'}\n help={pluginDocsHelp('contactFields', { anchor: '#define-a-field' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: scope ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button size=\"small\" onClick={() => void recomputeNextActivity()} disabled={recomputing}>\n {recomputing ? 'Recomputing…' : 'Recompute next activity'}\n </Button>\n <Button variant=\"contained\" onClick={openCreate}>\n {'New field'}\n </Button>\n </Stack>\n ) : null,\n }}\n >\n <Stack spacing={2}>\n <Tabs\n value={object}\n onChange={(_event, next) => {\n if (isCrmFieldObject(next)) setObject(next)\n }}\n aria-label=\"Fields by record\"\n >\n {CRM_FIELD_OBJECTS.map((entry) => (\n <Tab key={entry} value={entry} label={CRM_FIELD_OBJECT_LABELS[entry]} />\n ))}\n </Tabs>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${OBJECT_INTRO[object]} Fields are shared across every site in this ` +\n 'organization, like the records themselves.'}\n </Typography>\n {scopeReady && !scope ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`This site has no organization, so it has no ${noun} fields.`}\n </Typography>\n ) : !ready ? null : definitions.length === 0 ? (\n <EmptyStateComponent\n label={`No custom ${noun} fields yet`}\n description={`A field you define here is kept on every ${noun} and shows on its page.`}\n action={\n scope ? (\n <Button variant=\"contained\" onClick={openCreate}>\n {'New field'}\n </Button>\n ) : undefined\n }\n />\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell sx={{ width: 96 }}>{'Order'}</TableCell>\n <TableCell>{'Field'}</TableCell>\n <TableCell>{'Key'}</TableCell>\n <TableCell>{'Type'}</TableCell>\n <TableCell>{'Required'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {definitions.map((definition, index) => (\n <TableRow\n key={definition.$id}\n hover\n sx={definition.retiredAt ? { opacity: 0.6 } : undefined}\n >\n <TableCell>\n <Stack direction=\"row\" spacing={0}>\n <IconButton\n size=\"small\"\n disabled={index === 0 || Boolean(busyId)}\n onClick={() => void move(definition, -1)}\n >\n <MdiIcon path={mdiArrowUp.path} size={0.7} />\n <SrOnly>{`Move ${definition.label} up`}</SrOnly>\n </IconButton>\n <IconButton\n size=\"small\"\n disabled={index === definitions.length - 1 || Boolean(busyId)}\n onClick={() => void move(definition, 1)}\n >\n <MdiIcon path={mdiArrowDown.path} size={0.7} />\n <SrOnly>{`Move ${definition.label} down`}</SrOnly>\n </IconButton>\n </Stack>\n </TableCell>\n <TableCell>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', alignItems: 'center' }}\n >\n <Typography variant=\"body2\">{definition.label}</Typography>\n {definition.retiredAt ? (\n <Chip size=\"small\" variant=\"outlined\" color=\"warning\" label=\"Retired\" />\n ) : null}\n </Stack>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ fontFamily: 'monospace' }}>\n {definition.key}\n </Typography>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\">\n {CONTACT_FIELD_TYPE_LABELS[definition.type] ?? definition.type}\n {definition.type === 'select' && definition.options?.length\n ? ` · ${definition.options.length} choices`\n : ''}\n </Typography>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {definition.required ? 'Yes' : '—'}\n </Typography>\n </TableCell>\n <TableCell align=\"right\" sx={{ width: 56 }}>\n <RowActionsMenu label={definition.label} items={rowActions(definition)} />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n )}\n {definitions.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`How many ${noun}s carry a value under each field is not ` +\n `counted — that would read every ${noun} in the organization ` +\n 'each time this page opened.'}\n </Typography>\n ) : null}\n </Stack>\n <ContactFieldDrawer\n open={drawerOpen}\n onClose={() => setDrawerOpen(false)}\n definition={editing}\n object={object}\n takenKeys={definitions.map((definition) => definition.key)}\n onSubmit={handleSubmit}\n />\n </CardDisplay>\n )\n}\nContactsFieldsSection.displayName = 'ContactsFieldsSection'\n\nexport default ContactsFieldsSection\n"],"names":["CONTACT_FIELD_TYPE_LABELS","createResourceUid","CRM_COLLECTIONS","CRM_FIELD_OBJECT_LABELS","CRM_FIELD_OBJECTS","isCrmFieldObject","newResourceScopeFields","ORG_SCOPE_TOKEN","pluginDocsHelp","EmptyStateComponent","mdiArchiveArrowUpOutline","mdiArchiveOutline","mdiArrowDown","mdiArrowUp","mdiDeleteOutline","mdiPencilOutline","CardDisplay","MdiIcon","SrOnly","useConfirmationContext","RowActionsMenu","ScrollTable","useSnackbar","useFirestore","useUser","writeGuardedBySeed","Button","Chip","IconButton","Stack","Tab","TableBody","TableCell","TableHead","TableRow","Tabs","Typography","collection","deleteDoc","doc","setDoc","updateDoc","writeBatch","useCallback","useState","useContactFieldDefinitions","useCrmScope","ContactFieldDrawer","recomputeAllCrmNextActivity","crmTaskCallScope","OBJECT_INTRO","contact","company","deal","lead","OBJECT_NOUN","ContactsFieldsSection","props","hostId","org","firestore","enqueueSnackbar","confirm","scope","ready","scopeReady","createHostId","orgId","object","setObject","definitions","fromCache","noun","data","user","recomputing","setRecomputing","recomputeNextActivity","callScope","confirmed","title","description","confirmationText","then","catch","result","records","toLocaleString","tasks","truncated","variant","cause","Error","message","drawerOpen","setDrawerOpen","editing","setEditing","busyId","setBusyId","fieldRef","id","contactFields","openCreate","openEdit","definition","handleSubmit","draft","now","Date","verdict","subject","$id","label","type","options","required","updatedAt","ok","persist","order","reduce","highest","Math","max","Number","key","retiredAt","createdAt","move","direction","index","findIndex","entry","target","length","next","batch","forEach","position","update","commit","error","console","allowDuplicate","toggleRetired","accepted","handleDelete","confirmationButtonProps","color","rowActions","icon","path","size","onClick","destructive","disabled","Boolean","disabledReason","undefined","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","spacing","sx","alignItems","value","onChange","_event","aria-label","map","width","align","hover","opacity","flexWrap","fontFamily","items","open","onClose","takenKeys","onSubmit","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type CampaignPickerOption } from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component';
|
|
2
|
+
import { type FirestoreDocStatus } from '@aglyn/tenant-feature-instance';
|
|
3
|
+
/** The helper line under the picker — the contact card's, word for word. */
|
|
4
|
+
export declare const LEAD_CAMPAIGNS_HELPER_TEXT = "Your own filing. It never adds anyone to a send \u2014 a campaign mails its lists.";
|
|
5
|
+
/**
|
|
6
|
+
* The campaigns a lead is filed under, as NAMES for the record header.
|
|
7
|
+
*
|
|
8
|
+
* Resolved from the site's containers; an id with no live container — a
|
|
9
|
+
* campaign the console deleted, or one still loading — is left out rather
|
|
10
|
+
* than shown, because a document id on a chip tells a reader nothing and
|
|
11
|
+
* looks like a fault. The picker below keeps such an id visible as itself,
|
|
12
|
+
* so the filing is never silently lost; the header only ever names.
|
|
13
|
+
*/
|
|
14
|
+
export declare function leadCampaignNames(lead: Record<string, unknown> | null | undefined, options: readonly CampaignPickerOption[]): string[];
|
|
15
|
+
export interface LeadCampaignsCardProps {
|
|
16
|
+
hostId: string;
|
|
17
|
+
leadId: string;
|
|
18
|
+
lead: Record<string, unknown>;
|
|
19
|
+
leadStatus: FirestoreDocStatus;
|
|
20
|
+
/** The listener has not confirmed this document with the server yet. */
|
|
21
|
+
fromCache: boolean;
|
|
22
|
+
/** The site's campaigns, read once by the page for this card and the header. */
|
|
23
|
+
options: readonly CampaignPickerOption[];
|
|
24
|
+
/** The campaign read has answered — false while it settles. */
|
|
25
|
+
optionsReady: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* What a saved filing changed, once it has landed: the ids added to and
|
|
28
|
+
* removed from the lead's membership. The page files the activity
|
|
29
|
+
* entries (AGL-3274); the card only knows the document.
|
|
30
|
+
*/
|
|
31
|
+
onFiled?: (change: {
|
|
32
|
+
added: string[];
|
|
33
|
+
removed: string[];
|
|
34
|
+
}) => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).
|
|
38
|
+
*
|
|
39
|
+
* The contact's Relationship card has carried this picker since AGL-2596;
|
|
40
|
+
* the lead's page had none, so a lead filed under a campaign by the New
|
|
41
|
+
* lead drawer, the bulk bar or a sequence's enroll showed nothing of it —
|
|
42
|
+
* the one field on the record the reader had no way to see or change.
|
|
43
|
+
*
|
|
44
|
+
* The same control, the same value helper, the same helper line. The save
|
|
45
|
+
* is the lead's own client door — a one-field `update` on
|
|
46
|
+
* `hosts/{hostId}/leads`, as the properties card writes status and notes —
|
|
47
|
+
* guarded by the seed like every draft edited over a cached read, and
|
|
48
|
+
* `campaignMembershipUnchanged` keeps Save quiet while the picker only
|
|
49
|
+
* reorders what is stored.
|
|
50
|
+
*
|
|
51
|
+
* It is FILING, not audience: the helper says so, in the sentence the
|
|
52
|
+
* contact card uses, because a reader who finds a campaign picker on a
|
|
53
|
+
* person's page assumes the opposite.
|
|
54
|
+
*/
|
|
55
|
+
export declare function LeadCampaignsCard(props: LeadCampaignsCardProps): import("react").JSX.Element;
|
|
56
|
+
export declare namespace LeadCampaignsCard {
|
|
57
|
+
var displayName: string;
|
|
58
|
+
}
|
|
59
|
+
export default LeadCampaignsCard;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { campaignMembershipUnchanged, campaignMembershipValue, pluginDocsHelp, readCampaignIds } from "@aglyn/aglyn";
|
|
19
|
+
import CampaignPicker from "@aglyn/shared-ui-email-campaigns/components/campaign-picker.component";
|
|
20
|
+
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
21
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
22
|
+
import { useFirestore, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
|
|
23
|
+
import { Button, Stack } from "@mui/material";
|
|
24
|
+
import { doc, serverTimestamp, updateDoc } from "firebase/firestore";
|
|
25
|
+
import { useEffect, useState } from "react";
|
|
26
|
+
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
27
|
+
/** The helper line under the picker — the contact card's, word for word. */ export const LEAD_CAMPAIGNS_HELPER_TEXT = 'Your own filing. It never adds anyone to a send — a campaign mails its lists.';
|
|
28
|
+
/**
|
|
29
|
+
* The campaigns a lead is filed under, as NAMES for the record header.
|
|
30
|
+
*
|
|
31
|
+
* Resolved from the site's containers; an id with no live container — a
|
|
32
|
+
* campaign the console deleted, or one still loading — is left out rather
|
|
33
|
+
* than shown, because a document id on a chip tells a reader nothing and
|
|
34
|
+
* looks like a fault. The picker below keeps such an id visible as itself,
|
|
35
|
+
* so the filing is never silently lost; the header only ever names.
|
|
36
|
+
*/ export function leadCampaignNames(lead, options) {
|
|
37
|
+
const labels = new Map(options.map((option)=>[
|
|
38
|
+
option.value,
|
|
39
|
+
option.label
|
|
40
|
+
]));
|
|
41
|
+
return readCampaignIds(lead).map((id)=>labels.get(id)).filter((name)=>Boolean(name));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).
|
|
45
|
+
*
|
|
46
|
+
* The contact's Relationship card has carried this picker since AGL-2596;
|
|
47
|
+
* the lead's page had none, so a lead filed under a campaign by the New
|
|
48
|
+
* lead drawer, the bulk bar or a sequence's enroll showed nothing of it —
|
|
49
|
+
* the one field on the record the reader had no way to see or change.
|
|
50
|
+
*
|
|
51
|
+
* The same control, the same value helper, the same helper line. The save
|
|
52
|
+
* is the lead's own client door — a one-field `update` on
|
|
53
|
+
* `hosts/{hostId}/leads`, as the properties card writes status and notes —
|
|
54
|
+
* guarded by the seed like every draft edited over a cached read, and
|
|
55
|
+
* `campaignMembershipUnchanged` keeps Save quiet while the picker only
|
|
56
|
+
* reorders what is stored.
|
|
57
|
+
*
|
|
58
|
+
* It is FILING, not audience: the helper says so, in the sentence the
|
|
59
|
+
* contact card uses, because a reader who finds a campaign picker on a
|
|
60
|
+
* person's page assumes the opposite.
|
|
61
|
+
*/ export function LeadCampaignsCard(props) {
|
|
62
|
+
const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props;
|
|
63
|
+
// The lead's collection is the org's now (AGL-3275); the site still names
|
|
64
|
+
// the surface, and the scope hook resolves the org from it.
|
|
65
|
+
const { orgId } = useCrmScope({
|
|
66
|
+
hostId
|
|
67
|
+
});
|
|
68
|
+
const firestore = useFirestore();
|
|
69
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
70
|
+
const stored = readCampaignIds(lead);
|
|
71
|
+
const [selected, setSelected] = useState(stored);
|
|
72
|
+
const [dirty, setDirty] = useState(false);
|
|
73
|
+
const [saving, setSaving] = useState(false);
|
|
74
|
+
// A newer membership from the server replaces an UNEDITED selection — a
|
|
75
|
+
// sequence's enroll or a bulk add lands while the page is open — and an
|
|
76
|
+
// edited one is the reader's, decided by the guard on save.
|
|
77
|
+
const storedKey = stored.join(',');
|
|
78
|
+
useEffect(()=>{
|
|
79
|
+
if (!dirty) setSelected(stored);
|
|
80
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
81
|
+
}, [
|
|
82
|
+
storedKey,
|
|
83
|
+
dirty
|
|
84
|
+
]);
|
|
85
|
+
const unchanged = campaignMembershipUnchanged(stored, selected);
|
|
86
|
+
const empty = optionsReady && !options.length;
|
|
87
|
+
const save = async ()=>{
|
|
88
|
+
if (unchanged) return;
|
|
89
|
+
setSaving(true);
|
|
90
|
+
const next = campaignMembershipValue(selected);
|
|
91
|
+
const verdict = await writeGuardedBySeed({
|
|
92
|
+
subject: 'lead',
|
|
93
|
+
fromCache,
|
|
94
|
+
unreadable: leadStatus === 'error'
|
|
95
|
+
}, async ()=>{
|
|
96
|
+
await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {
|
|
97
|
+
campaignIds: next,
|
|
98
|
+
updatedAt: serverTimestamp()
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
setSaving(false);
|
|
102
|
+
if (!verdict.ok) {
|
|
103
|
+
var _verdict_message;
|
|
104
|
+
enqueueSnackbar((_verdict_message = verdict.message) != null ? _verdict_message : 'The filing could not be saved.', {
|
|
105
|
+
variant: 'warning'
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
setDirty(false);
|
|
110
|
+
enqueueSnackbar('Filing saved', {
|
|
111
|
+
variant: 'success',
|
|
112
|
+
persist: false
|
|
113
|
+
});
|
|
114
|
+
onFiled == null ? void 0 : onFiled({
|
|
115
|
+
added: next.filter((id)=>!stored.includes(id)),
|
|
116
|
+
removed: stored.filter((id)=>!next.includes(id))
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
120
|
+
header: 'Campaigns',
|
|
121
|
+
help: pluginDocsHelp('crmLeads', {
|
|
122
|
+
anchor: '#a-leads-page'
|
|
123
|
+
}),
|
|
124
|
+
contentGutterX: true,
|
|
125
|
+
contentGutterY: true,
|
|
126
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
127
|
+
spacing: 2,
|
|
128
|
+
children: [
|
|
129
|
+
/*#__PURE__*/ _jsx(CampaignPicker, {
|
|
130
|
+
options: options,
|
|
131
|
+
value: selected,
|
|
132
|
+
onChange: (next)=>{
|
|
133
|
+
setSelected(next);
|
|
134
|
+
setDirty(true);
|
|
135
|
+
},
|
|
136
|
+
label: "Filed under campaigns",
|
|
137
|
+
helperText: LEAD_CAMPAIGNS_HELPER_TEXT,
|
|
138
|
+
disabled: saving,
|
|
139
|
+
empty: empty,
|
|
140
|
+
emptyText: "This site has no campaigns yet. Create one from Marketing to file leads under it."
|
|
141
|
+
}),
|
|
142
|
+
empty ? null : /*#__PURE__*/ _jsx(Stack, {
|
|
143
|
+
direction: "row",
|
|
144
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
145
|
+
size: "small",
|
|
146
|
+
variant: "outlined",
|
|
147
|
+
disabled: saving || unchanged,
|
|
148
|
+
onClick: ()=>void save().catch((error)=>{
|
|
149
|
+
console.error(error);
|
|
150
|
+
enqueueSnackbar(error instanceof Error && error.message ? error.message : 'An error has occurred', {
|
|
151
|
+
variant: 'error',
|
|
152
|
+
allowDuplicate: true
|
|
153
|
+
});
|
|
154
|
+
setSaving(false);
|
|
155
|
+
}),
|
|
156
|
+
children: saving ? 'Saving…' : 'Save filing'
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
]
|
|
160
|
+
})
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
LeadCampaignsCard.displayName = 'LeadCampaignsCard';
|
|
164
|
+
export default LeadCampaignsCard;
|
|
165
|
+
|
|
166
|
+
//# sourceMappingURL=lead-campaigns-card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-campaigns-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 {\n campaignMembershipUnchanged,\n campaignMembershipValue,\n pluginDocsHelp,\n readCampaignIds,\n} from '@aglyn/aglyn'\nimport CampaignPicker, {\n type CampaignPickerOption,\n} from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack } from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\n\n/** The helper line under the picker — the contact card's, word for word. */\nexport const LEAD_CAMPAIGNS_HELPER_TEXT =\n 'Your own filing. It never adds anyone to a send — a campaign mails its lists.'\n\n/**\n * The campaigns a lead is filed under, as NAMES for the record header.\n *\n * Resolved from the site's containers; an id with no live container — a\n * campaign the console deleted, or one still loading — is left out rather\n * than shown, because a document id on a chip tells a reader nothing and\n * looks like a fault. The picker below keeps such an id visible as itself,\n * so the filing is never silently lost; the header only ever names.\n */\nexport function leadCampaignNames(\n lead: Record<string, unknown> | null | undefined,\n options: readonly CampaignPickerOption[],\n): string[] {\n const labels = new Map(options.map((option) => [option.value, option.label]))\n return readCampaignIds(lead)\n .map((id) => labels.get(id))\n .filter((name): name is string => Boolean(name))\n}\n\nexport interface LeadCampaignsCardProps {\n hostId: string\n leadId: string\n lead: Record<string, unknown>\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n /** The site's campaigns, read once by the page for this card and the header. */\n options: readonly CampaignPickerOption[]\n /** The campaign read has answered — false while it settles. */\n optionsReady: boolean\n /**\n * What a saved filing changed, once it has landed: the ids added to and\n * removed from the lead's membership. The page files the activity\n * entries (AGL-3274); the card only knows the document.\n */\n onFiled?: (change: { added: string[]; removed: string[] }) => void\n}\n\n/**\n * WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).\n *\n * The contact's Relationship card has carried this picker since AGL-2596;\n * the lead's page had none, so a lead filed under a campaign by the New\n * lead drawer, the bulk bar or a sequence's enroll showed nothing of it —\n * the one field on the record the reader had no way to see or change.\n *\n * The same control, the same value helper, the same helper line. The save\n * is the lead's own client door — a one-field `update` on\n * `hosts/{hostId}/leads`, as the properties card writes status and notes —\n * guarded by the seed like every draft edited over a cached read, and\n * `campaignMembershipUnchanged` keeps Save quiet while the picker only\n * reorders what is stored.\n *\n * It is FILING, not audience: the helper says so, in the sentence the\n * contact card uses, because a reader who finds a campaign picker on a\n * person's page assumes the opposite.\n */\nexport function LeadCampaignsCard(props: LeadCampaignsCardProps) {\n const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const stored = readCampaignIds(lead)\n const [selected, setSelected] = useState<string[]>(stored)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n // A newer membership from the server replaces an UNEDITED selection — a\n // sequence's enroll or a bulk add lands while the page is open — and an\n // edited one is the reader's, decided by the guard on save.\n const storedKey = stored.join(',')\n useEffect(() => {\n if (!dirty) setSelected(stored)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storedKey, dirty])\n\n const unchanged = campaignMembershipUnchanged(stored, selected)\n const empty = optionsReady && !options.length\n\n const save = async () => {\n if (unchanged) return\n setSaving(true)\n const next = campaignMembershipValue(selected)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n campaignIds: next,\n updatedAt: serverTimestamp(),\n })\n },\n )\n setSaving(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The filing could not be saved.', { variant: 'warning' })\n return\n }\n setDirty(false)\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n onFiled?.({\n added: next.filter((id) => !stored.includes(id)),\n removed: stored.filter((id) => !next.includes(id)),\n })\n }\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <CampaignPicker\n options={options}\n value={selected}\n onChange={(next) => {\n setSelected(next)\n setDirty(true)\n }}\n label=\"Filed under campaigns\"\n helperText={LEAD_CAMPAIGNS_HELPER_TEXT}\n disabled={saving}\n empty={empty}\n emptyText=\"This site has no campaigns yet. Create one from Marketing to file leads under it.\"\n />\n {empty ? null : (\n <Stack direction=\"row\">\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || unchanged}\n onClick={() => void save().catch((error: unknown) => {\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 setSaving(false)\n })}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n </Stack>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nLeadCampaignsCard.displayName = 'LeadCampaignsCard'\n\nexport default LeadCampaignsCard\n"],"names":["campaignMembershipUnchanged","campaignMembershipValue","pluginDocsHelp","readCampaignIds","CampaignPicker","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Stack","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","LEAD_CAMPAIGNS_HELPER_TEXT","leadCampaignNames","lead","options","labels","Map","map","option","value","label","id","get","filter","name","Boolean","LeadCampaignsCard","props","hostId","leadId","leadStatus","fromCache","optionsReady","onFiled","orgId","firestore","enqueueSnackbar","stored","selected","setSelected","dirty","setDirty","saving","setSaving","storedKey","join","unchanged","empty","length","save","next","verdict","subject","unreadable","campaignIds","updatedAt","ok","message","variant","persist","added","includes","removed","header","help","anchor","contentGutterX","contentGutterY","spacing","onChange","helperText","disabled","emptyText","direction","size","onClick","catch","error","console","Error","allowDuplicate","displayName"],"mappings":"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"}
|
|
@@ -16,15 +16,17 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
18
|
import { pluginDocsHelp, normalizeContactEmail, readErasureRequestedAtMs } from "@aglyn/aglyn";
|
|
19
|
-
import { useFirestore, useFirestoreDoc, useOrgDataScope } from "@aglyn/tenant-feature-instance";
|
|
19
|
+
import { useFirestore, useFirestoreDoc, useHostCampaigns, useOrgDataScope } from "@aglyn/tenant-feature-instance";
|
|
20
20
|
import { Stack, Typography } from "@mui/material";
|
|
21
21
|
import { doc } from "firebase/firestore";
|
|
22
22
|
import { useState } from "react";
|
|
23
|
+
import { useCampaignFilingLog } from "../hooks/use-campaign-filing-log.js";
|
|
23
24
|
import { useOrgMemberOptions } from "../hooks/use-org-member-options.js";
|
|
24
25
|
import { crmRoutes } from "../model/crm-routes.js";
|
|
25
|
-
import { CrmRecordHeader } from "./crm-record-header.js";
|
|
26
|
+
import { CrmRecordChip, CrmRecordHeader } from "./crm-record-header.js";
|
|
26
27
|
import { CrmRecordInsightsZone } from "./crm-record-insights-zone.js";
|
|
27
28
|
import { useErasePersonAction } from "./erase-person-action.js";
|
|
29
|
+
import { LeadCampaignsCard, leadCampaignNames } from "./lead-campaigns-card.js";
|
|
28
30
|
import { LeadConvertDialog } from "./lead-convert-dialog.js";
|
|
29
31
|
import { LeadHistoryCard } from "./lead-history-card.js";
|
|
30
32
|
import { LeadPropertiesCard } from "./lead-properties-card.js";
|
|
@@ -43,7 +45,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
43
45
|
const { id, hostId, org, basePath } = props;
|
|
44
46
|
const firestore = useFirestore();
|
|
45
47
|
const routes = crmRoutes(basePath);
|
|
46
|
-
const { data: lead, status, fromCache } = useFirestoreDoc(()=>doc(firestore, '
|
|
48
|
+
const { data: lead, status, fromCache } = useFirestoreDoc(()=>doc(firestore, 'orgs', orgId, 'leads', id), [
|
|
47
49
|
firestore,
|
|
48
50
|
hostId,
|
|
49
51
|
id
|
|
@@ -52,6 +54,21 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
52
54
|
hostId
|
|
53
55
|
});
|
|
54
56
|
const roster = useOrgMemberOptions(orgId);
|
|
57
|
+
/*
|
|
58
|
+
* The site's campaigns, read once for the page (AGL-3274): the header
|
|
59
|
+
* names the ones the lead is filed under and the Campaigns card offers
|
|
60
|
+
* them. One listener, not one per surface — a lead belongs to one site,
|
|
61
|
+
* so the containers are that site's.
|
|
62
|
+
*/ const campaigns = useHostCampaigns(hostId, {
|
|
63
|
+
enabled: true
|
|
64
|
+
});
|
|
65
|
+
// A saved filing is written on the lead's Activity too (AGL-3274), one
|
|
66
|
+
// entry per campaign added or removed, by the member who saved it.
|
|
67
|
+
const logFiling = useCampaignFilingLog({
|
|
68
|
+
orgId,
|
|
69
|
+
hostId,
|
|
70
|
+
org: org
|
|
71
|
+
});
|
|
55
72
|
const [converting, setConverting] = useState(false);
|
|
56
73
|
const [unqualifying, setUnqualifying] = useState(false);
|
|
57
74
|
// The privacy erasure (AGL-2623), offered from the lead as from the
|
|
@@ -103,6 +120,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
103
120
|
children: [
|
|
104
121
|
/*#__PURE__*/ _jsx(LeadPropertiesCard, {
|
|
105
122
|
hostId: hostId,
|
|
123
|
+
orgId: orgId,
|
|
106
124
|
leadId: id,
|
|
107
125
|
lead: lead,
|
|
108
126
|
leadStatus: status,
|
|
@@ -114,7 +132,14 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
114
132
|
extraMenuItems: erase.menuItems,
|
|
115
133
|
banner: erase.banner,
|
|
116
134
|
erasurePending: erase.pendingSinceMs !== null,
|
|
117
|
-
org: org
|
|
135
|
+
org: org,
|
|
136
|
+
// The campaigns the lead is filed under (AGL-3274), by name
|
|
137
|
+
// beside the status. An id no container answers for draws no
|
|
138
|
+
// chip: the card below keeps it, the header only names.
|
|
139
|
+
extraChips: leadCampaignNames(lead, campaigns.options).map((name)=>/*#__PURE__*/ _jsx(CrmRecordChip, {
|
|
140
|
+
label: "Campaign",
|
|
141
|
+
value: name
|
|
142
|
+
}, name))
|
|
118
143
|
}),
|
|
119
144
|
/*#__PURE__*/ _jsx(CrmRecordInsightsZone, {
|
|
120
145
|
hostId: hostId,
|
|
@@ -123,6 +148,31 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
123
148
|
recordId: id,
|
|
124
149
|
name: label != null ? label : ''
|
|
125
150
|
}),
|
|
151
|
+
/*#__PURE__*/ _jsx(LeadCampaignsCard, {
|
|
152
|
+
hostId: hostId,
|
|
153
|
+
leadId: id,
|
|
154
|
+
lead: lead,
|
|
155
|
+
leadStatus: status,
|
|
156
|
+
fromCache: fromCache,
|
|
157
|
+
options: campaigns.options,
|
|
158
|
+
optionsReady: campaigns.ready,
|
|
159
|
+
onFiled: ({ added, removed })=>{
|
|
160
|
+
const named = (ids)=>ids.map((campaignId)=>{
|
|
161
|
+
var _ref;
|
|
162
|
+
var _campaigns_options_find;
|
|
163
|
+
return {
|
|
164
|
+
id: campaignId,
|
|
165
|
+
name: (_ref = (_campaigns_options_find = campaigns.options.find((option)=>option.value === campaignId)) == null ? void 0 : _campaigns_options_find.label) != null ? _ref : ''
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
void logFiling({
|
|
169
|
+
leadId: id
|
|
170
|
+
}, {
|
|
171
|
+
filed: named(added),
|
|
172
|
+
removed: named(removed)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}),
|
|
126
176
|
/*#__PURE__*/ _jsx(LeadHistoryCard, {
|
|
127
177
|
hostId: hostId,
|
|
128
178
|
leadId: id,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-detail-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n pluginDocsHelp,\n type CrmLeadFields,\n normalizeContactEmail,\n readErasureRequestedAtMs,\n} from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreDoc,\n useOrgDataScope,\n} from '@aglyn/tenant-feature-instance'\nimport { Stack, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useState } from 'react'\nimport { useOrgMemberOptions } from '../hooks/use-org-member-options'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport { CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { useErasePersonAction } from './erase-person-action'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport { LeadHistoryCard } from './lead-history-card'\nimport { LeadPropertiesCard } from './lead-properties-card'\nimport { LeadUnqualifyDialog } from './lead-unqualify-dialog'\nimport { RecordActivityCard } from './record-activity-card'\n\ntype LeadDocument = Record<string, unknown> & CrmLeadFields\n\n/**\n * `/crm/leads/{leadId}` — one lead (AGL-2608).\n *\n * The record behind a row of the Leads section: what the person did on the\n * site, who is working them, and the conversion that turns them into a\n * contact, a company and a deal. One document listen — `hosts/{hostId}/leads`\n * is host-scoped by path, so there is no `visibleTo` to filter — and one\n * roster request for the owner picker; the convert dialog's reads open only\n * when it does.\n */\nexport function LeadDetailPage(props: CrmDetailPageProps) {\n const { id, hostId, org, basePath } = props\n const firestore = useFirestore()\n const routes = crmRoutes(basePath)\n const {\n data: lead,\n status,\n fromCache,\n } = useFirestoreDoc<LeadDocument>(\n () => doc(firestore, 'hosts', hostId, 'leads', id),\n [firestore, hostId, id],\n )\n const { orgId } = useOrgDataScope({ hostId })\n const roster = useOrgMemberOptions(orgId)\n const [converting, setConverting] = useState(false)\n const [unqualifying, setUnqualifying] = useState(false)\n // The privacy erasure (AGL-2623), offered from the lead as from the\n // contact: the same request, filed by the lead's address.\n const leadEmail = lead ? normalizeContactEmail(lead['email']) : null\n const erase = useErasePersonAction({\n hostId,\n orgId,\n subject: lead && leadEmail ? { kind: 'lead', id, email: leadEmail } : null,\n requestedAtMs: readErasureRequestedAtMs(lead),\n })\n\n const label = lead ? String(lead['name'] || lead['email'] || id) : undefined\n\n if (status === 'error' || (status === 'success' && !lead)) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {status === 'error'\n ? 'This lead could not be read.'\n : 'This lead no longer exists — it may have been removed from the Inbox.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n if (!lead) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n loading\n />\n )\n }\n\n return (\n <>\n {/* The properties card is the record's lead card: it publishes the\n page heading and the trail, so the history card under it says what\n it holds rather than repeating the name. */}\n <Stack spacing={3}>\n <LeadPropertiesCard\n hostId={hostId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n basePath={basePath}\n roster={roster}\n onConvert={() => setConverting(true)}\n onUnqualify={() => setUnqualifying(true)}\n extraMenuItems={erase.menuItems}\n banner={erase.banner}\n erasurePending={erase.pendingSinceMs !== null}\n org={org}\n />\n {/* What an assistant says about where the lead stands (AGL-2917): read on its own site. */}\n <CrmRecordInsightsZone\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"lead\"\n recordId={id}\n name={label ?? ''}\n />\n <LeadHistoryCard hostId={hostId} leadId={id} lead={lead} />\n <RecordActivityCard hostId={hostId} org={org} leadId={id} />\n </Stack>\n <LeadConvertDialog\n open={converting}\n onClose={() => setConverting(false)}\n hostId={hostId}\n orgId={orgId}\n org={org as Record<string, unknown> | undefined}\n leadId={id}\n lead={lead}\n basePath={basePath}\n roster={roster}\n />\n <LeadUnqualifyDialog\n open={unqualifying}\n onClose={() => setUnqualifying(false)}\n hostId={hostId}\n leadId={id}\n leadLabel={label ?? id}\n />\n {erase.dialog}\n </>\n )\n}\nLeadDetailPage.displayName = 'LeadDetailPage'\n\nexport default LeadDetailPage\n"],"names":["pluginDocsHelp","normalizeContactEmail","readErasureRequestedAtMs","useFirestore","useFirestoreDoc","useOrgDataScope","Stack","Typography","doc","useState","useOrgMemberOptions","crmRoutes","CrmRecordHeader","CrmRecordInsightsZone","useErasePersonAction","LeadConvertDialog","LeadHistoryCard","LeadPropertiesCard","LeadUnqualifyDialog","RecordActivityCard","LeadDetailPage","props","id","hostId","org","basePath","firestore","routes","data","lead","status","fromCache","orgId","roster","converting","setConverting","unqualifying","setUnqualifying","leadEmail","erase","subject","kind","email","requestedAtMs","label","String","undefined","title","help","anchor","backHref","section","backLabel","variant","color","loading","spacing","leadId","leadStatus","onConvert","onUnqualify","extraMenuItems","menuItems","banner","erasurePending","pendingSinceMs","recordId","name","open","onClose","leadLabel","dialog","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-detail-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n pluginDocsHelp,\n type CrmLeadFields,\n normalizeContactEmail,\n readErasureRequestedAtMs,\n} from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreDoc,\n useHostCampaigns,\n useOrgDataScope,\n} from '@aglyn/tenant-feature-instance'\nimport { Stack, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useState } from 'react'\nimport { useCampaignFilingLog } from '../hooks/use-campaign-filing-log'\nimport { useOrgMemberOptions } from '../hooks/use-org-member-options'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { useErasePersonAction } from './erase-person-action'\nimport { LeadCampaignsCard, leadCampaignNames } from './lead-campaigns-card'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport { LeadHistoryCard } from './lead-history-card'\nimport { LeadPropertiesCard } from './lead-properties-card'\nimport { LeadUnqualifyDialog } from './lead-unqualify-dialog'\nimport { RecordActivityCard } from './record-activity-card'\n\ntype LeadDocument = Record<string, unknown> & CrmLeadFields\n\n/**\n * `/crm/leads/{leadId}` — one lead (AGL-2608).\n *\n * The record behind a row of the Leads section: what the person did on the\n * site, who is working them, and the conversion that turns them into a\n * contact, a company and a deal. One document listen — `hosts/{hostId}/leads`\n * is host-scoped by path, so there is no `visibleTo` to filter — and one\n * roster request for the owner picker; the convert dialog's reads open only\n * when it does.\n */\nexport function LeadDetailPage(props: CrmDetailPageProps) {\n const { id, hostId, org, basePath } = props\n const firestore = useFirestore()\n const routes = crmRoutes(basePath)\n const {\n data: lead,\n status,\n fromCache,\n } = useFirestoreDoc<LeadDocument>(\n () => doc(firestore, 'orgs', orgId, 'leads', id),\n [firestore, hostId, id],\n )\n const { orgId } = useOrgDataScope({ hostId })\n const roster = useOrgMemberOptions(orgId)\n /*\n * The site's campaigns, read once for the page (AGL-3274): the header\n * names the ones the lead is filed under and the Campaigns card offers\n * them. One listener, not one per surface — a lead belongs to one site,\n * so the containers are that site's.\n */\n const campaigns = useHostCampaigns(hostId, { enabled: true })\n // A saved filing is written on the lead's Activity too (AGL-3274), one\n // entry per campaign added or removed, by the member who saved it.\n const logFiling = useCampaignFilingLog({\n orgId,\n hostId,\n org: org as Record<string, unknown> | undefined,\n })\n const [converting, setConverting] = useState(false)\n const [unqualifying, setUnqualifying] = useState(false)\n // The privacy erasure (AGL-2623), offered from the lead as from the\n // contact: the same request, filed by the lead's address.\n const leadEmail = lead ? normalizeContactEmail(lead['email']) : null\n const erase = useErasePersonAction({\n hostId,\n orgId,\n subject: lead && leadEmail ? { kind: 'lead', id, email: leadEmail } : null,\n requestedAtMs: readErasureRequestedAtMs(lead),\n })\n\n const label = lead ? String(lead['name'] || lead['email'] || id) : undefined\n\n if (status === 'error' || (status === 'success' && !lead)) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {status === 'error'\n ? 'This lead could not be read.'\n : 'This lead no longer exists — it may have been removed from the Inbox.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n if (!lead) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n loading\n />\n )\n }\n\n return (\n <>\n {/* The properties card is the record's lead card: it publishes the\n page heading and the trail, so the history card under it says what\n it holds rather than repeating the name. */}\n <Stack spacing={3}>\n <LeadPropertiesCard\n hostId={hostId}\n orgId={orgId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n basePath={basePath}\n roster={roster}\n onConvert={() => setConverting(true)}\n onUnqualify={() => setUnqualifying(true)}\n extraMenuItems={erase.menuItems}\n banner={erase.banner}\n erasurePending={erase.pendingSinceMs !== null}\n org={org}\n // The campaigns the lead is filed under (AGL-3274), by name\n // beside the status. An id no container answers for draws no\n // chip: the card below keeps it, the header only names.\n extraChips={leadCampaignNames(lead, campaigns.options).map((name) => (\n <CrmRecordChip key={name} label=\"Campaign\" value={name} />\n ))}\n />\n {/* What an assistant says about where the lead stands (AGL-2917): read on its own site. */}\n <CrmRecordInsightsZone\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"lead\"\n recordId={id}\n name={label ?? ''}\n />\n <LeadCampaignsCard\n hostId={hostId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n options={campaigns.options}\n optionsReady={campaigns.ready}\n onFiled={({ added, removed }) => {\n const named = (ids: string[]) =>\n ids.map((campaignId) => ({\n id: campaignId,\n name: campaigns.options.find((option) => option.value === campaignId)?.label ?? '',\n }))\n void logFiling({ leadId: id }, { filed: named(added), removed: named(removed) })\n }}\n />\n <LeadHistoryCard hostId={hostId} leadId={id} lead={lead} />\n <RecordActivityCard hostId={hostId} org={org} leadId={id} />\n </Stack>\n <LeadConvertDialog\n open={converting}\n onClose={() => setConverting(false)}\n hostId={hostId}\n orgId={orgId}\n org={org as Record<string, unknown> | undefined}\n leadId={id}\n lead={lead}\n basePath={basePath}\n roster={roster}\n />\n <LeadUnqualifyDialog\n open={unqualifying}\n onClose={() => setUnqualifying(false)}\n hostId={hostId}\n leadId={id}\n leadLabel={label ?? id}\n />\n {erase.dialog}\n </>\n )\n}\nLeadDetailPage.displayName = 'LeadDetailPage'\n\nexport default LeadDetailPage\n"],"names":["pluginDocsHelp","normalizeContactEmail","readErasureRequestedAtMs","useFirestore","useFirestoreDoc","useHostCampaigns","useOrgDataScope","Stack","Typography","doc","useState","useCampaignFilingLog","useOrgMemberOptions","crmRoutes","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","useErasePersonAction","LeadCampaignsCard","leadCampaignNames","LeadConvertDialog","LeadHistoryCard","LeadPropertiesCard","LeadUnqualifyDialog","RecordActivityCard","LeadDetailPage","props","id","hostId","org","basePath","firestore","routes","data","lead","status","fromCache","orgId","roster","campaigns","enabled","logFiling","converting","setConverting","unqualifying","setUnqualifying","leadEmail","erase","subject","kind","email","requestedAtMs","label","String","undefined","title","help","anchor","backHref","section","backLabel","variant","color","loading","spacing","leadId","leadStatus","onConvert","onUnqualify","extraMenuItems","menuItems","banner","erasurePending","pendingSinceMs","extraChips","options","map","name","value","recordId","optionsReady","ready","onFiled","added","removed","named","ids","campaignId","find","option","filed","open","onClose","leadLabel","dialog","displayName"],"mappings":"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"}
|