@aglyn/plugins-crm 1.0.0-beta.163 → 1.0.0-beta.165

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 (40) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/contact-associations-card.js +14 -4
  3. package/src/lib/components/contact-associations-card.js.map +1 -1
  4. package/src/lib/components/contact-field-drawer.js +19 -2
  5. package/src/lib/components/contact-field-drawer.js.map +1 -1
  6. package/src/lib/components/fields-section.d.ts +2 -1
  7. package/src/lib/components/fields-section.js +6 -3
  8. package/src/lib/components/fields-section.js.map +1 -1
  9. package/src/lib/components/lead-campaigns-card.d.ts +59 -0
  10. package/src/lib/components/lead-campaigns-card.js +160 -0
  11. package/src/lib/components/lead-campaigns-card.js.map +1 -0
  12. package/src/lib/components/lead-detail-page.js +53 -3
  13. package/src/lib/components/lead-detail-page.js.map +1 -1
  14. package/src/lib/components/lead-history-card.js +5 -0
  15. package/src/lib/components/lead-history-card.js.map +1 -1
  16. package/src/lib/components/lead-properties-card.d.ts +12 -0
  17. package/src/lib/components/lead-properties-card.js +63 -5
  18. package/src/lib/components/lead-properties-card.js.map +1 -1
  19. package/src/lib/components/leads-bulk-bar.d.ts +6 -0
  20. package/src/lib/components/leads-bulk-bar.js +34 -3
  21. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  22. package/src/lib/components/leads-section.js +16 -2
  23. package/src/lib/components/leads-section.js.map +1 -1
  24. package/src/lib/components/new-lead-drawer.d.ts +14 -1
  25. package/src/lib/components/new-lead-drawer.js +41 -3
  26. package/src/lib/components/new-lead-drawer.js.map +1 -1
  27. package/src/lib/hooks/use-campaign-filing-log.d.ts +29 -0
  28. package/src/lib/hooks/use-campaign-filing-log.js +91 -0
  29. package/src/lib/hooks/use-campaign-filing-log.js.map +1 -0
  30. package/src/lib/model/campaign-filing-activity.d.ts +82 -0
  31. package/src/lib/model/campaign-filing-activity.js +88 -0
  32. package/src/lib/model/campaign-filing-activity.js.map +1 -0
  33. package/src/lib/server/campaign-filing-activity.d.ts +79 -0
  34. package/src/lib/server/campaign-filing-activity.js +93 -0
  35. package/src/lib/server/campaign-filing-activity.js.map +1 -0
  36. package/src/lib/server/lead-campaign-carry.js +25 -0
  37. package/src/lib/server/lead-campaign-carry.js.map +1 -1
  38. package/src/lib/server/lead-create.d.ts +15 -4
  39. package/src/lib/server/lead-create.js +68 -10
  40. package/src/lib/server/lead-create.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-crm",
3
- "version": "1.0.0-beta.163",
3
+ "version": "1.0.0-beta.165",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,18 +25,18 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.163",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.163",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.163",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.163",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.163",
33
- "@aglyn/shared-ui-next": "1.0.0-beta.163",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.163",
35
- "@aglyn/shared-util-email": "1.0.0-beta.163",
36
- "@aglyn/shared-util-http": "1.0.0-beta.163",
37
- "@aglyn/tenant-data-admin": "1.0.0-beta.163",
38
- "@aglyn/tenant-feature-instance": "1.0.0-beta.163",
39
- "@aglyn/tenant-runtime": "1.0.0-beta.163",
28
+ "@aglyn/aglyn": "1.0.0-beta.165",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.165",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.165",
31
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.165",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.165",
33
+ "@aglyn/shared-ui-next": "1.0.0-beta.165",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.165",
35
+ "@aglyn/shared-util-email": "1.0.0-beta.165",
36
+ "@aglyn/shared-util-http": "1.0.0-beta.165",
37
+ "@aglyn/tenant-data-admin": "1.0.0-beta.165",
38
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.165",
39
+ "@aglyn/tenant-runtime": "1.0.0-beta.165",
40
40
  "@dnd-kit/core": "^6.3.1",
41
41
  "@dnd-kit/utilities": "^3.2.2",
42
42
  "@swc/helpers": "0.5.23",
@@ -240,10 +240,20 @@ import { crmRoutes } from "../model/crm-routes.js";
240
240
  })
241
241
  ]
242
242
  }) : null,
243
- hostId ? /*#__PURE__*/ _jsx(CrmRecordAttributionZone, {
244
- hostId: hostId,
245
- recordKind: "contact",
246
- recordId: record.$id
243
+ hostId ? /*#__PURE__*/ _jsxs(Stack, {
244
+ spacing: 0.5,
245
+ children: [
246
+ /*#__PURE__*/ _jsx(CrmRecordAttributionZone, {
247
+ hostId: hostId,
248
+ recordKind: "contact",
249
+ recordId: record.$id
250
+ }),
251
+ /*#__PURE__*/ _jsx(Typography, {
252
+ variant: "caption",
253
+ color: "text.secondary",
254
+ children: 'Filing is separate — see Filed under campaigns.'
255
+ })
256
+ ]
247
257
  }) : null,
248
258
  /*#__PURE__*/ _jsxs(Stack, {
249
259
  spacing: 0.5,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-associations-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 CONTACT_SOURCE_LABELS,\n type ConsentGroup,\n type ContactSource,\n MARKETING_BASIS_LABELS,\n readMarketingBasis,\n} from '@aglyn/aglyn'\nimport { personKeyInBrowser } from '../model/person-key-web'\n/*\n * The component path and NOT the marketing barrel: that barrel is the entry\n * point the tenant's loader imports to activate the plugin's SITE half, so a\n * console card named there ships to every published page.\n */\nimport { CrmRecordAttributionZone } from './crm-attribution-zone'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useHostCampaigns,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Divider, Stack, Typography } from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport type { ContactRecord } from '../model/contact-record'\nimport { crmRoutes } from '../model/crm-routes'\n\nexport interface ContactAssociationsCardProps {\n /**\n * The site whose lead, campaigns and attribution are shown — the mounted\n * site, or at the organization level the holder's own; `null` for a row\n * no site has captured, when none of the three has an address.\n */\n hostId: string | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /** The document as read, for the consent fields the projection leaves out. */\n row: Record<string, unknown>\n consentGroup: ConsentGroup\n /** The listener's verdict on the row, for the stale-seed guard. */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The hub's own path, for the link to this person's lead. */\n basePath: string\n /**\n * The site's Bookings page narrowed to this person's address (AGL-2660),\n * or nothing where the site has no booking door — the page decides, and\n * the row is simply not drawn.\n */\n bookingsHref?: string | null\n}\n\n/**\n * WHERE THIS PERSON CAME FROM AND WHAT THEY ARE FILED UNDER (AGL-2596).\n *\n * Four facts that were in the v1 drawer and are not properties of the\n * person so much as of the RELATIONSHIP:\n *\n * - The SOURCES say which mechanism created the record — a form, a\n * checkout, an import.\n * - The ATTRIBUTION says which campaign led to it, recorded from the link\n * they followed and never editable. One keyed read, paid on opening the\n * page rather than per row of the list.\n * - The CONSENT is the basis this controller holds for marketing email:\n * opted in, opted out, or no record — three states that never collapse\n * into two, because \"no record\" is the commonest and is not a refusal.\n * - The FILING is which campaigns the team has PUT them in, a working set\n * kept by hand. It adds nobody to a send: a campaign's audience is its\n * lists and each email's own picker, and the helper says so where a\n * reader would otherwise assume the opposite.\n *\n * The filing is the one editable thing here and has its own Save, through\n * the same stale-seed guard the properties card uses: a campaign membership\n * written over a cached read could revert somebody else's filing. The save\n * goes to `crm/contact-update` (AGL-2804), because the filing is this\n * holder's facet and a facet is the server's to write; like every field that\n * route writes, it is refused to a plan without the CRM (AGL-2851).\n *\n * ## The lead (AGL-2612)\n *\n * A LEAD is the sales record a lead surface — a sign-up, a booking, a form\n * with lead routing — files beside the contact, under the capturing site at\n * `hosts/{hostId}/leads/{personKey}`. The lead's page links the contact\n * once converted; nothing linked back, so the two halves of one person\n * read as unrelated. One `getDoc` by the key the capture derives — the\n * address, normalized and hashed, computed here on WebCrypto — is what\n * answers \"does this site hold a lead for this person\", and it is a read\n * this page pays because opening the record IS the ask. Absence is a fact\n * too: a contact captured by an order or an import has no lead, and the\n * chip is simply not there.\n */\nexport function ContactAssociationsCard(props: ContactAssociationsCardProps) {\n const { hostId, record, row, consentGroup, seed, basePath, bookingsHref } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n const routes = crmRoutes(basePath)\n\n const [leadKey, setLeadKey] = useState<string | null>(null)\n const email = record.email\n useEffect(() => {\n let active = true\n setLeadKey(null)\n if (!hostId) return undefined\n void (async () => {\n const key = await personKeyInBrowser(email)\n if (!key || !active) return\n const snapshot = await getDoc(\n doc(firestore, 'hosts', hostId, 'leads', key),\n ).catch(() => null)\n if (active && snapshot?.exists()) setLeadKey(key)\n })().catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, hostId, email])\n\n const [campaigns, setCampaigns] = useState<string[]>(record.campaignIds)\n const [saving, setSaving] = useState(false)\n const recordId = record.$id\n useEffect(() => {\n setCampaigns(record.campaignIds)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n /*\n * The site's campaigns, read because this page IS the ask: one picker on\n * one record, and nothing in the list ever paid for the campaign list.\n */\n const siteCampaigns = useHostCampaigns(hostId ?? undefined, { enabled: Boolean(hostId) })\n\n const consent = useMemo(\n () => readMarketingBasis(row, consentGroup),\n [row, consentGroup],\n )\n\n const handleSaveFiling = useCallback(async () => {\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 // An empty selection is sent as an empty list rather than left out,\n // so \"filed under no campaign\" has one shape here and in the pass\n // that detaches a deleted campaign.\n () =>\n contactUpdate.updateOne(record.$id, {\n campaignIds: Aglyn.campaignMembershipValue(campaigns),\n }),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n enqueueSnackbar('Filing 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 }, [campaigns, contactUpdate, enqueueSnackbar, record.$id, seed.fromCache, seed.status])\n\n const sources = Object.keys(record.sources ?? {}) as ContactSource[]\n const consentDetail = [\n consent.basisAtMs\n ? `recorded ${new Date(consent.basisAtMs).toLocaleDateString()}`\n : null,\n consent.assertedBy === 'operator' ? 'entered by your team' : null,\n consent.otherGrant === 'other-host'\n ? 'opted in to another site in this workspace'\n : null,\n ].filter(Boolean)\n\n return (\n <CardDisplay\n header={'Relationship'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#what-each-site-keeps-to-itself' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Sources'}</Typography>\n {sources.length || leadKey ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip\n key={source}\n label={CONTACT_SOURCE_LABELS[source] ?? source}\n size=\"small\"\n />\n ))}\n {/*\n This site's lead for the person, when it holds one — a link,\n in the shape the lead's page uses to point back here.\n */}\n {leadKey ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.lead(leadKey)}\n size=\"small\"\n variant=\"outlined\"\n sx={{ py: 0, minHeight: 24, lineHeight: 1.5 }}\n >\n {'Lead on this site'}\n </Button>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No capture recorded.'}\n </Typography>\n )}\n </Stack>\n {/*\n The person's bookings on this site (AGL-2660): every booking\n taken with their address, past ones too, on the Bookings page —\n the appointments are that page's records, and this card only\n points at them.\n */}\n {bookingsHref ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Bookings'}</Typography>\n <Typography variant=\"body2\">\n <AppLink href={bookingsHref}>{'Bookings on this site'}</AppLink>\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Every booking made with this address, upcoming and past.'}\n </Typography>\n </Stack>\n ) : null}\n {hostId ? (\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"contact\"\n recordId={record.$id}\n />\n ) : null}\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Marketing email'}</Typography>\n <Typography variant=\"body2\">\n {MARKETING_BASIS_LABELS[consent.basis]}\n {consentDetail.length ? ` · ${consentDetail.join(' · ')}` : ''}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {consent.basis === 'unrecorded'\n ? 'Nothing was recorded either way — not a refusal, and not permission.'\n : `The basis this ${\n consentGroup.declared ? 'group' : 'site'\n } holds. Other sites in the workspace keep their own.`}\n </Typography>\n </Stack>\n <Divider />\n <CampaignPicker\n options={siteCampaigns.options}\n value={campaigns}\n onChange={setCampaigns}\n label=\"Filed under campaigns\"\n helperText=\"Your own filing. It never adds anyone to a send — a campaign mails its lists.\"\n empty={siteCampaigns.ready && !siteCampaigns.options.length}\n emptyText=\"This site has no campaigns yet.\"\n />\n <Stack direction=\"row\">\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || (siteCampaigns.ready && !siteCampaigns.options.length)}\n onClick={() => void handleSaveFiling()}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n </Stack>\n </Stack>\n </CardDisplay>\n )\n}\nContactAssociationsCard.displayName = 'ContactAssociationsCard'\n\nexport default ContactAssociationsCard\n"],"names":["Aglyn","CONTACT_SOURCE_LABELS","MARKETING_BASIS_LABELS","readMarketingBasis","personKeyInBrowser","CrmRecordAttributionZone","CampaignPicker","AppLink","CardDisplay","useSnackbar","useFirestore","useHostCampaigns","writeGuardedBySeed","Button","Chip","Divider","Stack","Typography","doc","getDoc","useCallback","useEffect","useMemo","useState","useContactUpdate","crmRoutes","ContactAssociationsCard","props","record","hostId","row","consentGroup","seed","basePath","bookingsHref","firestore","enqueueSnackbar","contactUpdate","routes","leadKey","setLeadKey","email","active","undefined","key","snapshot","catch","exists","campaigns","setCampaigns","campaignIds","saving","setSaving","recordId","$id","siteCampaigns","enabled","Boolean","consent","handleSaveFiling","verdict","subject","unreadable","status","fromCache","updateOne","campaignMembershipValue","ok","message","variant","persist","error","console","Error","allowDuplicate","sources","Object","keys","consentDetail","basisAtMs","Date","toLocaleDateString","assertedBy","otherGrant","filter","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","spacing","length","direction","sx","flexWrap","rowGap","map","source","label","size","component","componentVariant","nativeButton","href","lead","py","minHeight","lineHeight","color","recordKind","basis","join","declared","options","value","onChange","helperText","empty","ready","emptyText","disabled","onClick","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-associations-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 CONTACT_SOURCE_LABELS,\n type ConsentGroup,\n type ContactSource,\n MARKETING_BASIS_LABELS,\n readMarketingBasis,\n} from '@aglyn/aglyn'\nimport { personKeyInBrowser } from '../model/person-key-web'\n/*\n * The component path and NOT the marketing barrel: that barrel is the entry\n * point the tenant's loader imports to activate the plugin's SITE half, so a\n * console card named there ships to every published page.\n */\nimport { CrmRecordAttributionZone } from './crm-attribution-zone'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useHostCampaigns,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Divider, Stack, Typography } from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport type { ContactRecord } from '../model/contact-record'\nimport { crmRoutes } from '../model/crm-routes'\n\nexport interface ContactAssociationsCardProps {\n /**\n * The site whose lead, campaigns and attribution are shown — the mounted\n * site, or at the organization level the holder's own; `null` for a row\n * no site has captured, when none of the three has an address.\n */\n hostId: string | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /** The document as read, for the consent fields the projection leaves out. */\n row: Record<string, unknown>\n consentGroup: ConsentGroup\n /** The listener's verdict on the row, for the stale-seed guard. */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The hub's own path, for the link to this person's lead. */\n basePath: string\n /**\n * The site's Bookings page narrowed to this person's address (AGL-2660),\n * or nothing where the site has no booking door — the page decides, and\n * the row is simply not drawn.\n */\n bookingsHref?: string | null\n}\n\n/**\n * WHERE THIS PERSON CAME FROM AND WHAT THEY ARE FILED UNDER (AGL-2596).\n *\n * Four facts that were in the v1 drawer and are not properties of the\n * person so much as of the RELATIONSHIP:\n *\n * - The SOURCES say which mechanism created the record — a form, a\n * checkout, an import.\n * - The ATTRIBUTION says which campaign led to it, recorded from the link\n * they followed and never editable. One keyed read, paid on opening the\n * page rather than per row of the list.\n * - The CONSENT is the basis this controller holds for marketing email:\n * opted in, opted out, or no record — three states that never collapse\n * into two, because \"no record\" is the commonest and is not a refusal.\n * - The FILING is which campaigns the team has PUT them in, a working set\n * kept by hand. It adds nobody to a send: a campaign's audience is its\n * lists and each email's own picker, and the helper says so where a\n * reader would otherwise assume the opposite.\n *\n * The filing is the one editable thing here and has its own Save, through\n * the same stale-seed guard the properties card uses: a campaign membership\n * written over a cached read could revert somebody else's filing. The save\n * goes to `crm/contact-update` (AGL-2804), because the filing is this\n * holder's facet and a facet is the server's to write; like every field that\n * route writes, it is refused to a plan without the CRM (AGL-2851).\n *\n * ## The lead (AGL-2612)\n *\n * A LEAD is the sales record a lead surface — a sign-up, a booking, a form\n * with lead routing — files beside the contact, under the capturing site at\n * `hosts/{hostId}/leads/{personKey}`. The lead's page links the contact\n * once converted; nothing linked back, so the two halves of one person\n * read as unrelated. One `getDoc` by the key the capture derives — the\n * address, normalized and hashed, computed here on WebCrypto — is what\n * answers \"does this site hold a lead for this person\", and it is a read\n * this page pays because opening the record IS the ask. Absence is a fact\n * too: a contact captured by an order or an import has no lead, and the\n * chip is simply not there.\n */\nexport function ContactAssociationsCard(props: ContactAssociationsCardProps) {\n const { hostId, record, row, consentGroup, seed, basePath, bookingsHref } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n const routes = crmRoutes(basePath)\n\n const [leadKey, setLeadKey] = useState<string | null>(null)\n const email = record.email\n useEffect(() => {\n let active = true\n setLeadKey(null)\n if (!hostId) return undefined\n void (async () => {\n const key = await personKeyInBrowser(email)\n if (!key || !active) return\n const snapshot = await getDoc(\n doc(firestore, 'hosts', hostId, 'leads', key),\n ).catch(() => null)\n if (active && snapshot?.exists()) setLeadKey(key)\n })().catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, hostId, email])\n\n const [campaigns, setCampaigns] = useState<string[]>(record.campaignIds)\n const [saving, setSaving] = useState(false)\n const recordId = record.$id\n useEffect(() => {\n setCampaigns(record.campaignIds)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n /*\n * The site's campaigns, read because this page IS the ask: one picker on\n * one record, and nothing in the list ever paid for the campaign list.\n */\n const siteCampaigns = useHostCampaigns(hostId ?? undefined, { enabled: Boolean(hostId) })\n\n const consent = useMemo(\n () => readMarketingBasis(row, consentGroup),\n [row, consentGroup],\n )\n\n const handleSaveFiling = useCallback(async () => {\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 // An empty selection is sent as an empty list rather than left out,\n // so \"filed under no campaign\" has one shape here and in the pass\n // that detaches a deleted campaign.\n () =>\n contactUpdate.updateOne(record.$id, {\n campaignIds: Aglyn.campaignMembershipValue(campaigns),\n }),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n enqueueSnackbar('Filing 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 }, [campaigns, contactUpdate, enqueueSnackbar, record.$id, seed.fromCache, seed.status])\n\n const sources = Object.keys(record.sources ?? {}) as ContactSource[]\n const consentDetail = [\n consent.basisAtMs\n ? `recorded ${new Date(consent.basisAtMs).toLocaleDateString()}`\n : null,\n consent.assertedBy === 'operator' ? 'entered by your team' : null,\n consent.otherGrant === 'other-host'\n ? 'opted in to another site in this workspace'\n : null,\n ].filter(Boolean)\n\n return (\n <CardDisplay\n header={'Relationship'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#what-each-site-keeps-to-itself' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Sources'}</Typography>\n {sources.length || leadKey ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip\n key={source}\n label={CONTACT_SOURCE_LABELS[source] ?? source}\n size=\"small\"\n />\n ))}\n {/*\n This site's lead for the person, when it holds one — a link,\n in the shape the lead's page uses to point back here.\n */}\n {leadKey ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.lead(leadKey)}\n size=\"small\"\n variant=\"outlined\"\n sx={{ py: 0, minHeight: 24, lineHeight: 1.5 }}\n >\n {'Lead on this site'}\n </Button>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No capture recorded.'}\n </Typography>\n )}\n </Stack>\n {/*\n The person's bookings on this site (AGL-2660): every booking\n taken with their address, past ones too, on the Bookings page —\n the appointments are that page's records, and this card only\n points at them.\n */}\n {bookingsHref ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Bookings'}</Typography>\n <Typography variant=\"body2\">\n <AppLink href={bookingsHref}>{'Bookings on this site'}</AppLink>\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Every booking made with this address, upcoming and past.'}\n </Typography>\n </Stack>\n ) : null}\n {hostId ? (\n <Stack spacing={0.5}>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"contact\"\n recordId={record.$id}\n />\n {/* Attribution is the campaign that brought the person; the\n picker below is the team's own filing (AGL-3274). */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Filing is separate — see Filed under campaigns.'}\n </Typography>\n </Stack>\n ) : null}\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Marketing email'}</Typography>\n <Typography variant=\"body2\">\n {MARKETING_BASIS_LABELS[consent.basis]}\n {consentDetail.length ? ` · ${consentDetail.join(' · ')}` : ''}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {consent.basis === 'unrecorded'\n ? 'Nothing was recorded either way — not a refusal, and not permission.'\n : `The basis this ${\n consentGroup.declared ? 'group' : 'site'\n } holds. Other sites in the workspace keep their own.`}\n </Typography>\n </Stack>\n <Divider />\n <CampaignPicker\n options={siteCampaigns.options}\n value={campaigns}\n onChange={setCampaigns}\n label=\"Filed under campaigns\"\n helperText=\"Your own filing. It never adds anyone to a send — a campaign mails its lists.\"\n empty={siteCampaigns.ready && !siteCampaigns.options.length}\n emptyText=\"This site has no campaigns yet.\"\n />\n <Stack direction=\"row\">\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || (siteCampaigns.ready && !siteCampaigns.options.length)}\n onClick={() => void handleSaveFiling()}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n </Stack>\n </Stack>\n </CardDisplay>\n )\n}\nContactAssociationsCard.displayName = 'ContactAssociationsCard'\n\nexport default ContactAssociationsCard\n"],"names":["Aglyn","CONTACT_SOURCE_LABELS","MARKETING_BASIS_LABELS","readMarketingBasis","personKeyInBrowser","CrmRecordAttributionZone","CampaignPicker","AppLink","CardDisplay","useSnackbar","useFirestore","useHostCampaigns","writeGuardedBySeed","Button","Chip","Divider","Stack","Typography","doc","getDoc","useCallback","useEffect","useMemo","useState","useContactUpdate","crmRoutes","ContactAssociationsCard","props","record","hostId","row","consentGroup","seed","basePath","bookingsHref","firestore","enqueueSnackbar","contactUpdate","routes","leadKey","setLeadKey","email","active","undefined","key","snapshot","catch","exists","campaigns","setCampaigns","campaignIds","saving","setSaving","recordId","$id","siteCampaigns","enabled","Boolean","consent","handleSaveFiling","verdict","subject","unreadable","status","fromCache","updateOne","campaignMembershipValue","ok","message","variant","persist","error","console","Error","allowDuplicate","sources","Object","keys","consentDetail","basisAtMs","Date","toLocaleDateString","assertedBy","otherGrant","filter","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","spacing","length","direction","sx","flexWrap","rowGap","map","source","label","size","component","componentVariant","nativeButton","href","lead","py","minHeight","lineHeight","color","recordKind","basis","join","declared","options","value","onChange","helperText","empty","ready","emptyText","disabled","onClick","displayName"],"mappings":"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"}
@@ -21,13 +21,30 @@ import { useCallback, useEffect, useMemo, useState } from "react";
21
21
  /** How the object reads in the drawer's own sentences — singular, lowercase. */ const OBJECT_NOUN = {
22
22
  contact: 'contact',
23
23
  company: 'company',
24
- deal: 'deal'
24
+ deal: 'deal',
25
+ lead: 'lead'
26
+ };
27
+ /**
28
+ * Which objects a CSV import can fill a custom field on — see
29
+ * `CUSTOM_FIELD_OBJECT` in `record-facts.ts`, which decides what the
30
+ * mapping menu offers. The deal and lead files carry the standard columns
31
+ * only, so their drawers must not promise an import that would silently
32
+ * drop the column.
33
+ */ const IMPORTABLE = {
34
+ contact: true,
35
+ company: true,
36
+ deal: false,
37
+ lead: false
25
38
  };
26
39
  /**
27
40
  * What the drawer says about website forms, per object — see
28
41
  * {@link ContactFieldDrawerProps.object}.
29
42
  */ export function fieldDrawerMappingNote(object) {
30
- return object === 'contact' ? 'A website form field can save its answer into this field.' : `A website form field saves into contact fields only; a ${OBJECT_NOUN[object]} ` + 'field is filled on the record, by CSV import or over the API.';
43
+ if (object === 'contact') {
44
+ return 'A website form field can save its answer into this field.';
45
+ }
46
+ const filled = IMPORTABLE[object] ? 'is filled on the record, by CSV import or over the API.' : 'is filled on the record or over the API.';
47
+ return `A website form field saves into contact fields only; a ${OBJECT_NOUN[object]} ` + `field ${filled}`;
31
48
  }
32
49
  /** How many choices a select may declare, and how long each may be. */ const OPTIONS_MAX = 50;
33
50
  const OPTION_MAX_LENGTH = 120;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-field-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n CONTACT_FIELD_TYPE_LABELS,\n CONTACT_FIELD_TYPES,\n type ContactFieldType,\n type CrmFieldObject,\n isContactFieldType,\n normalizeContactFieldKey,\n} from '@aglyn/aglyn'\nimport {\n Button,\n Drawer,\n FormControlLabel,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ContactFieldDefinitionDoc } from '../hooks/use-contact-field-definitions'\n\n/** What the drawer hands back — the definition's editable half. */\nexport interface ContactFieldDraft {\n label: string\n key: string\n type: ContactFieldType\n /** The choices, for `select`; empty for every other type. */\n options: string[]\n required: boolean\n}\n\nexport interface ContactFieldDrawerProps {\n open: boolean\n onClose: () => void\n /** The definition being edited, or `null` to create one. */\n definition: ContactFieldDefinitionDoc | null\n /**\n * Every key the org has already used, retired ones included. A key is\n * the map key values are stored under, so a retired field's key is still\n * taken: a new field reusing it would read the old values back as its own.\n */\n takenKeys: readonly string[]\n /** Receives the draft; closing on success is the caller's job. */\n onSubmit: (draft: ContactFieldDraft) => Promise<void> | void\n /**\n * The record the field describes (AGL-2661) — the Fields section's\n * current tab. Contacts when unsaid. The drawer's copy follows it, and it\n * is the one thing here a form-field mapping cares about: a website form\n * saves into a CONTACT field only, because a submission is a fact about\n * a person, so the other objects' drawers say so instead of offering it.\n */\n object?: CrmFieldObject\n}\n\n/** How the object reads in the drawer's own sentences — singular, lowercase. */\nconst OBJECT_NOUN: Record<CrmFieldObject, string> = {\n contact: 'contact',\n company: 'company',\n deal: 'deal',\n}\n\n/**\n * What the drawer says about website forms, per object — see\n * {@link ContactFieldDrawerProps.object}.\n */\nexport function fieldDrawerMappingNote(object: CrmFieldObject): string {\n return object === 'contact'\n ? 'A website form field can save its answer into this field.'\n : `A website form field saves into contact fields only; a ${OBJECT_NOUN[object]} ` +\n 'field is filled on the record, by CSV import or over the API.'\n}\n\n/** How many choices a select may declare, and how long each may be. */\nconst OPTIONS_MAX = 50\nconst OPTION_MAX_LENGTH = 120\nconst LABEL_MAX_LENGTH = 80\n\n/** One choice per line, trimmed, blanks dropped, duplicates collapsed. */\nfunction parseOptions(text: string): string[] {\n const seen = new Set<string>()\n const options: string[] = []\n for (const line of text.split('\\n')) {\n const option = line.trim().slice(0, OPTION_MAX_LENGTH)\n if (!option || seen.has(option)) continue\n seen.add(option)\n options.push(option)\n if (options.length >= OPTIONS_MAX) break\n }\n return options\n}\n\n/**\n * The one form a custom contact field is created and edited through\n * (AGL-2601).\n *\n * ## The key follows the label until the author takes it over\n *\n * A merchant types \"Annual revenue\" and should not have to know that the\n * value is stored under `annual_revenue`. So the key is derived from the\n * label as they type — `normalizeContactFieldKey` — and shown beside it, and\n * only becomes the author's own the moment they edit it. That is the whole\n * of the grammar they are asked to learn, and the field is still there for\n * the author who wants `arr` instead.\n *\n * ## Two things do not change after creation\n *\n * The KEY is the map key every value is stored under, so changing it would\n * orphan every value already written; the TYPE is what every reader coerces\n * those values by, so changing it would make them unreadable. Both are\n * disabled on an existing field and say why. A rename is a new field and a\n * retire, which the section's list offers.\n */\nexport function ContactFieldDrawer(props: ContactFieldDrawerProps) {\n const { open, onClose, definition, takenKeys, onSubmit, object = 'contact' } = props\n const editing = Boolean(definition)\n const noun = OBJECT_NOUN[object]\n\n const [label, setLabel] = useState('')\n const [key, setKey] = useState('')\n const [keyTouched, setKeyTouched] = useState(false)\n const [type, setType] = useState<ContactFieldType>('text')\n const [optionsText, setOptionsText] = useState('')\n const [required, setRequired] = useState(false)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n // Seeded on every open, from the definition when there is one: a drawer\n // that kept the previous field's draft would offer it as the next one.\n useEffect(() => {\n if (!open) return\n setLabel(definition?.label ?? '')\n setKey(definition?.key ?? '')\n setKeyTouched(Boolean(definition))\n setType(definition?.type ?? 'text')\n setOptionsText((definition?.options ?? []).join('\\n'))\n setRequired(definition?.required === true)\n setBusy(false)\n setError(null)\n }, [open, definition])\n\n const derivedKey = useMemo(\n () => (keyTouched ? normalizeContactFieldKey(key) : normalizeContactFieldKey(label)),\n [keyTouched, key, label],\n )\n const shownKey = keyTouched ? key : (derivedKey ?? '')\n const keyTaken =\n !editing && derivedKey != null && takenKeys.includes(derivedKey)\n const options = useMemo(() => parseOptions(optionsText), [optionsText])\n\n const keyHelp = editing\n ? 'Values are stored under this key, so it cannot change once the field exists.'\n : keyTaken\n ? 'That key is already in use — a retired field keeps its key.'\n : shownKey && !derivedKey\n ? 'A key starts with a letter and uses letters, digits and underscores.'\n : 'Derived from the label; edit it to choose your own.'\n\n const canSubmit =\n !busy &&\n label.trim().length > 0 &&\n derivedKey != null &&\n !keyTaken &&\n (type !== 'select' || options.length > 0)\n\n const handleSubmit = useCallback(async () => {\n if (!canSubmit || derivedKey == null) return\n setBusy(true)\n setError(null)\n try {\n await onSubmit({\n label: label.trim().slice(0, LABEL_MAX_LENGTH),\n key: derivedKey,\n type,\n options: type === 'select' ? options : [],\n required,\n })\n } catch (caught) {\n console.error(caught)\n setError('Could not save the field. Try again.')\n } finally {\n setBusy(false)\n }\n }, [canSubmit, derivedKey, onSubmit, label, type, options, required])\n\n return (\n <Drawer anchor=\"right\" open={open} onClose={onClose}>\n <Stack spacing={2} sx={{ width: 360, p: 3 }}>\n <Typography variant=\"h6\">\n {editing ? `Edit ${noun} field` : `New ${noun} field`}\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {fieldDrawerMappingNote(object)}\n </Typography>\n <TextField\n size=\"small\"\n label=\"Label\"\n value={label}\n onChange={(event) => setLabel(event.target.value)}\n slotProps={{ htmlInput: { maxLength: LABEL_MAX_LENGTH } }}\n autoFocus={!editing}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Key\"\n value={shownKey}\n onChange={(event) => {\n setKeyTouched(true)\n setKey(event.target.value)\n }}\n disabled={editing}\n error={!editing && (keyTaken || (shownKey.length > 0 && !derivedKey))}\n helperText={keyHelp}\n slotProps={{ htmlInput: { maxLength: 40, spellCheck: false } }}\n fullWidth\n />\n <TextField\n select\n size=\"small\"\n label=\"Type\"\n value={type}\n onChange={(event) => {\n const next = event.target.value\n if (isContactFieldType(next)) setType(next)\n }}\n disabled={editing}\n helperText={\n editing\n ? 'Values are read by type, so it is fixed once the field exists.'\n : undefined\n }\n fullWidth\n >\n {CONTACT_FIELD_TYPES.map((option) => (\n <MenuItem key={option} value={option}>\n {CONTACT_FIELD_TYPE_LABELS[option]}\n </MenuItem>\n ))}\n </TextField>\n {type === 'select' ? (\n <TextField\n size=\"small\"\n label=\"Choices\"\n value={optionsText}\n onChange={(event) => setOptionsText(event.target.value)}\n helperText=\"One per line. A stored value has to be one of these.\"\n multiline\n minRows={3}\n fullWidth\n />\n ) : null}\n <FormControlLabel\n control={\n <Switch\n checked={required}\n onChange={(event) => setRequired(event.target.checked)}\n />\n }\n label={`Required on the ${noun} form`}\n />\n {error ? (\n <Typography variant=\"body2\" color=\"error\">\n {error}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!canSubmit}\n onClick={handleSubmit}\n >\n {editing ? 'Save' : 'Create field'}\n </Button>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n </Drawer>\n )\n}\nContactFieldDrawer.displayName = 'ContactFieldDrawer'\n\nexport default ContactFieldDrawer\n"],"names":["CONTACT_FIELD_TYPE_LABELS","CONTACT_FIELD_TYPES","isContactFieldType","normalizeContactFieldKey","Button","Drawer","FormControlLabel","MenuItem","Stack","Switch","TextField","Typography","useCallback","useEffect","useMemo","useState","OBJECT_NOUN","contact","company","deal","fieldDrawerMappingNote","object","OPTIONS_MAX","OPTION_MAX_LENGTH","LABEL_MAX_LENGTH","parseOptions","text","seen","Set","options","line","split","option","trim","slice","has","add","push","length","ContactFieldDrawer","props","open","onClose","definition","takenKeys","onSubmit","editing","Boolean","noun","label","setLabel","key","setKey","keyTouched","setKeyTouched","type","setType","optionsText","setOptionsText","required","setRequired","busy","setBusy","error","setError","join","derivedKey","shownKey","keyTaken","includes","keyHelp","canSubmit","handleSubmit","caught","console","anchor","spacing","sx","width","p","variant","color","size","value","onChange","event","target","slotProps","htmlInput","maxLength","autoFocus","fullWidth","disabled","helperText","spellCheck","select","next","undefined","map","multiline","minRows","control","checked","direction","onClick","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,yBAAyB,EACzBC,mBAAmB,EAGnBC,kBAAkB,EAClBC,wBAAwB,QACnB,eAAc;AACrB,SACEC,MAAM,EACNC,MAAM,EACNC,gBAAgB,EAChBC,QAAQ,EACRC,KAAK,EACLC,MAAM,EACNC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AAoCjE,8EAA8E,GAC9E,MAAMC,cAA8C;IAClDC,SAAS;IACTC,SAAS;IACTC,MAAM;AACR;AAEA;;;CAGC,GACD,OAAO,SAASC,uBAAuBC,MAAsB;IAC3D,OAAOA,WAAW,YACd,8DACA,CAAC,uDAAuD,EAAEL,WAAW,CAACK,OAAO,CAAC,CAAC,CAAC,GAC9E;AACR;AAEA,qEAAqE,GACrE,MAAMC,cAAc;AACpB,MAAMC,oBAAoB;AAC1B,MAAMC,mBAAmB;AAEzB,wEAAwE,GACxE,SAASC,aAAaC,IAAY;IAChC,MAAMC,OAAO,IAAIC;IACjB,MAAMC,UAAoB,EAAE;IAC5B,KAAK,MAAMC,QAAQJ,KAAKK,KAAK,CAAC,MAAO;QACnC,MAAMC,SAASF,KAAKG,IAAI,GAAGC,KAAK,CAAC,GAAGX;QACpC,IAAI,CAACS,UAAUL,KAAKQ,GAAG,CAACH,SAAS;QACjCL,KAAKS,GAAG,CAACJ;QACTH,QAAQQ,IAAI,CAACL;QACb,IAAIH,QAAQS,MAAM,IAAIhB,aAAa;IACrC;IACA,OAAOO;AACT;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASU,mBAAmBC,KAA8B;IAC/D,MAAM,EAAEC,IAAI,EAAEC,OAAO,EAAEC,UAAU,EAAEC,SAAS,EAAEC,QAAQ,EAAExB,SAAS,SAAS,EAAE,GAAGmB;IAC/E,MAAMM,UAAUC,QAAQJ;IACxB,MAAMK,OAAOhC,WAAW,CAACK,OAAO;IAEhC,MAAM,CAAC4B,OAAOC,SAAS,GAAGnC,SAAS;IACnC,MAAM,CAACoC,KAAKC,OAAO,GAAGrC,SAAS;IAC/B,MAAM,CAACsC,YAAYC,cAAc,GAAGvC,SAAS;IAC7C,MAAM,CAACwC,MAAMC,QAAQ,GAAGzC,SAA2B;IACnD,MAAM,CAAC0C,aAAaC,eAAe,GAAG3C,SAAS;IAC/C,MAAM,CAAC4C,UAAUC,YAAY,GAAG7C,SAAS;IACzC,MAAM,CAAC8C,MAAMC,QAAQ,GAAG/C,SAAS;IACjC,MAAM,CAACgD,OAAOC,SAAS,GAAGjD,SAAwB;IAElD,wEAAwE;IACxE,uEAAuE;IACvEF,UAAU;;QACR,IAAI,CAAC4B,MAAM;QACXS,iBAASP,8BAAAA,WAAYM,KAAK,mBAAI;QAC9BG,gBAAOT,8BAAAA,WAAYQ,GAAG,oBAAI;QAC1BG,cAAcP,QAAQJ;QACtBa,iBAAQb,8BAAAA,WAAYY,IAAI,oBAAI;QAC5BG,eAAe,UAACf,8BAAAA,WAAYd,OAAO,oBAAI,EAAE,EAAEoC,IAAI,CAAC;QAChDL,YAAYjB,CAAAA,8BAAAA,WAAYgB,QAAQ,MAAK;QACrCG,QAAQ;QACRE,SAAS;IACX,GAAG;QAACvB;QAAME;KAAW;IAErB,MAAMuB,aAAapD,QACjB,IAAOuC,aAAalD,yBAAyBgD,OAAOhD,yBAAyB8C,QAC7E;QAACI;QAAYF;QAAKF;KAAM;IAE1B,MAAMkB,WAAWd,aAAaF,MAAOe,qBAAAA,aAAc;IACnD,MAAME,WACJ,CAACtB,WAAWoB,cAAc,QAAQtB,UAAUyB,QAAQ,CAACH;IACvD,MAAMrC,UAAUf,QAAQ,IAAMW,aAAagC,cAAc;QAACA;KAAY;IAEtE,MAAMa,UAAUxB,UACZ,iFACAsB,WACE,gEACAD,YAAY,CAACD,aACX,yEACA;IAER,MAAMK,YACJ,CAACV,QACDZ,MAAMhB,IAAI,GAAGK,MAAM,GAAG,KACtB4B,cAAc,QACd,CAACE,YACAb,CAAAA,SAAS,YAAY1B,QAAQS,MAAM,GAAG,CAAA;IAEzC,MAAMkC,eAAe5D,YAAY;QAC/B,IAAI,CAAC2D,aAAaL,cAAc,MAAM;QACtCJ,QAAQ;QACRE,SAAS;QACT,IAAI;YACF,MAAMnB,SAAS;gBACbI,OAAOA,MAAMhB,IAAI,GAAGC,KAAK,CAAC,GAAGV;gBAC7B2B,KAAKe;gBACLX;gBACA1B,SAAS0B,SAAS,WAAW1B,UAAU,EAAE;gBACzC8B;YACF;QACF,EAAE,OAAOc,QAAQ;YACfC,QAAQX,KAAK,CAACU;YACdT,SAAS;QACX,SAAU;YACRF,QAAQ;QACV;IACF,GAAG;QAACS;QAAWL;QAAYrB;QAAUI;QAAOM;QAAM1B;QAAS8B;KAAS;IAEpE,qBACE,KAACtD;QAAOsE,QAAO;QAAQlC,MAAMA;QAAMC,SAASA;kBAC1C,cAAA,MAAClC;YAAMoE,SAAS;YAAGC,IAAI;gBAAEC,OAAO;gBAAKC,GAAG;YAAE;;8BACxC,KAACpE;oBAAWqE,SAAQ;8BACjBlC,UAAU,CAAC,KAAK,EAAEE,KAAK,MAAM,CAAC,GAAG,CAAC,IAAI,EAAEA,KAAK,MAAM,CAAC;;8BAEvD,KAACrC;oBAAWqE,SAAQ;oBAAQC,OAAM;8BAC/B7D,uBAAuBC;;8BAE1B,KAACX;oBACCwE,MAAK;oBACLjC,OAAM;oBACNkC,OAAOlC;oBACPmC,UAAU,CAACC,QAAUnC,SAASmC,MAAMC,MAAM,CAACH,KAAK;oBAChDI,WAAW;wBAAEC,WAAW;4BAAEC,WAAWjE;wBAAiB;oBAAE;oBACxDkE,WAAW,CAAC5C;oBACZ6C,SAAS;;8BAEX,KAACjF;oBACCwE,MAAK;oBACLjC,OAAM;oBACNkC,OAAOhB;oBACPiB,UAAU,CAACC;wBACT/B,cAAc;wBACdF,OAAOiC,MAAMC,MAAM,CAACH,KAAK;oBAC3B;oBACAS,UAAU9C;oBACViB,OAAO,CAACjB,WAAYsB,CAAAA,YAAaD,SAAS7B,MAAM,GAAG,KAAK,CAAC4B,UAAU;oBACnE2B,YAAYvB;oBACZiB,WAAW;wBAAEC,WAAW;4BAAEC,WAAW;4BAAIK,YAAY;wBAAM;oBAAE;oBAC7DH,SAAS;;8BAEX,KAACjF;oBACCqF,MAAM;oBACNb,MAAK;oBACLjC,OAAM;oBACNkC,OAAO5B;oBACP6B,UAAU,CAACC;wBACT,MAAMW,OAAOX,MAAMC,MAAM,CAACH,KAAK;wBAC/B,IAAIjF,mBAAmB8F,OAAOxC,QAAQwC;oBACxC;oBACAJ,UAAU9C;oBACV+C,YACE/C,UACI,mEACAmD;oBAENN,SAAS;8BAER1F,oBAAoBiG,GAAG,CAAC,CAAClE,uBACxB,KAACzB;4BAAsB4E,OAAOnD;sCAC3BhC,yBAAyB,CAACgC,OAAO;2BADrBA;;gBAKlBuB,SAAS,yBACR,KAAC7C;oBACCwE,MAAK;oBACLjC,OAAM;oBACNkC,OAAO1B;oBACP2B,UAAU,CAACC,QAAU3B,eAAe2B,MAAMC,MAAM,CAACH,KAAK;oBACtDU,YAAW;oBACXM,SAAS;oBACTC,SAAS;oBACTT,SAAS;qBAET;8BACJ,KAACrF;oBACC+F,uBACE,KAAC5F;wBACC6F,SAAS3C;wBACTyB,UAAU,CAACC,QAAUzB,YAAYyB,MAAMC,MAAM,CAACgB,OAAO;;oBAGzDrD,OAAO,CAAC,gBAAgB,EAAED,KAAK,KAAK,CAAC;;gBAEtCe,sBACC,KAACpD;oBAAWqE,SAAQ;oBAAQC,OAAM;8BAC/BlB;qBAED;8BACJ,MAACvD;oBAAM+F,WAAU;oBAAM3B,SAAS;;sCAC9B,KAACxE;4BACC4E,SAAQ;4BACRC,OAAM;4BACNW,UAAU,CAACrB;4BACXiC,SAAShC;sCAER1B,UAAU,SAAS;;sCAEtB,KAAC1C;4BAAOoG,SAAS9D;4BAASkD,UAAU/B;sCACjC;;;;;;;AAMb;AACAtB,mBAAmBkE,WAAW,GAAG;AAEjC,eAAelE,mBAAkB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-field-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n CONTACT_FIELD_TYPE_LABELS,\n CONTACT_FIELD_TYPES,\n type ContactFieldType,\n type CrmFieldObject,\n isContactFieldType,\n normalizeContactFieldKey,\n} from '@aglyn/aglyn'\nimport {\n Button,\n Drawer,\n FormControlLabel,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { ContactFieldDefinitionDoc } from '../hooks/use-contact-field-definitions'\n\n/** What the drawer hands back — the definition's editable half. */\nexport interface ContactFieldDraft {\n label: string\n key: string\n type: ContactFieldType\n /** The choices, for `select`; empty for every other type. */\n options: string[]\n required: boolean\n}\n\nexport interface ContactFieldDrawerProps {\n open: boolean\n onClose: () => void\n /** The definition being edited, or `null` to create one. */\n definition: ContactFieldDefinitionDoc | null\n /**\n * Every key the org has already used, retired ones included. A key is\n * the map key values are stored under, so a retired field's key is still\n * taken: a new field reusing it would read the old values back as its own.\n */\n takenKeys: readonly string[]\n /** Receives the draft; closing on success is the caller's job. */\n onSubmit: (draft: ContactFieldDraft) => Promise<void> | void\n /**\n * The record the field describes (AGL-2661) — the Fields section's\n * current tab. Contacts when unsaid. The drawer's copy follows it, and it\n * is the one thing here a form-field mapping cares about: a website form\n * saves into a CONTACT field only, because a submission is a fact about\n * a person, so the other objects' drawers say so instead of offering it.\n */\n object?: CrmFieldObject\n}\n\n/** How the object reads in the drawer's own sentences — singular, lowercase. */\nconst OBJECT_NOUN: Record<CrmFieldObject, string> = {\n contact: 'contact',\n company: 'company',\n deal: 'deal',\n lead: 'lead',\n}\n\n/**\n * Which objects a CSV import can fill a custom field on — see\n * `CUSTOM_FIELD_OBJECT` in `record-facts.ts`, which decides what the\n * mapping menu offers. The deal and lead files carry the standard columns\n * only, so their drawers must not promise an import that would silently\n * drop the column.\n */\nconst IMPORTABLE: Record<CrmFieldObject, boolean> = {\n contact: true,\n company: true,\n deal: false,\n lead: false,\n}\n\n/**\n * What the drawer says about website forms, per object — see\n * {@link ContactFieldDrawerProps.object}.\n */\nexport function fieldDrawerMappingNote(object: CrmFieldObject): string {\n if (object === 'contact') {\n return 'A website form field can save its answer into this field.'\n }\n const filled = IMPORTABLE[object]\n ? 'is filled on the record, by CSV import or over the API.'\n : 'is filled on the record or over the API.'\n return (\n `A website form field saves into contact fields only; a ${OBJECT_NOUN[object]} ` +\n `field ${filled}`\n )\n}\n\n/** How many choices a select may declare, and how long each may be. */\nconst OPTIONS_MAX = 50\nconst OPTION_MAX_LENGTH = 120\nconst LABEL_MAX_LENGTH = 80\n\n/** One choice per line, trimmed, blanks dropped, duplicates collapsed. */\nfunction parseOptions(text: string): string[] {\n const seen = new Set<string>()\n const options: string[] = []\n for (const line of text.split('\\n')) {\n const option = line.trim().slice(0, OPTION_MAX_LENGTH)\n if (!option || seen.has(option)) continue\n seen.add(option)\n options.push(option)\n if (options.length >= OPTIONS_MAX) break\n }\n return options\n}\n\n/**\n * The one form a custom contact field is created and edited through\n * (AGL-2601).\n *\n * ## The key follows the label until the author takes it over\n *\n * A merchant types \"Annual revenue\" and should not have to know that the\n * value is stored under `annual_revenue`. So the key is derived from the\n * label as they type — `normalizeContactFieldKey` — and shown beside it, and\n * only becomes the author's own the moment they edit it. That is the whole\n * of the grammar they are asked to learn, and the field is still there for\n * the author who wants `arr` instead.\n *\n * ## Two things do not change after creation\n *\n * The KEY is the map key every value is stored under, so changing it would\n * orphan every value already written; the TYPE is what every reader coerces\n * those values by, so changing it would make them unreadable. Both are\n * disabled on an existing field and say why. A rename is a new field and a\n * retire, which the section's list offers.\n */\nexport function ContactFieldDrawer(props: ContactFieldDrawerProps) {\n const { open, onClose, definition, takenKeys, onSubmit, object = 'contact' } = props\n const editing = Boolean(definition)\n const noun = OBJECT_NOUN[object]\n\n const [label, setLabel] = useState('')\n const [key, setKey] = useState('')\n const [keyTouched, setKeyTouched] = useState(false)\n const [type, setType] = useState<ContactFieldType>('text')\n const [optionsText, setOptionsText] = useState('')\n const [required, setRequired] = useState(false)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n // Seeded on every open, from the definition when there is one: a drawer\n // that kept the previous field's draft would offer it as the next one.\n useEffect(() => {\n if (!open) return\n setLabel(definition?.label ?? '')\n setKey(definition?.key ?? '')\n setKeyTouched(Boolean(definition))\n setType(definition?.type ?? 'text')\n setOptionsText((definition?.options ?? []).join('\\n'))\n setRequired(definition?.required === true)\n setBusy(false)\n setError(null)\n }, [open, definition])\n\n const derivedKey = useMemo(\n () => (keyTouched ? normalizeContactFieldKey(key) : normalizeContactFieldKey(label)),\n [keyTouched, key, label],\n )\n const shownKey = keyTouched ? key : (derivedKey ?? '')\n const keyTaken =\n !editing && derivedKey != null && takenKeys.includes(derivedKey)\n const options = useMemo(() => parseOptions(optionsText), [optionsText])\n\n const keyHelp = editing\n ? 'Values are stored under this key, so it cannot change once the field exists.'\n : keyTaken\n ? 'That key is already in use — a retired field keeps its key.'\n : shownKey && !derivedKey\n ? 'A key starts with a letter and uses letters, digits and underscores.'\n : 'Derived from the label; edit it to choose your own.'\n\n const canSubmit =\n !busy &&\n label.trim().length > 0 &&\n derivedKey != null &&\n !keyTaken &&\n (type !== 'select' || options.length > 0)\n\n const handleSubmit = useCallback(async () => {\n if (!canSubmit || derivedKey == null) return\n setBusy(true)\n setError(null)\n try {\n await onSubmit({\n label: label.trim().slice(0, LABEL_MAX_LENGTH),\n key: derivedKey,\n type,\n options: type === 'select' ? options : [],\n required,\n })\n } catch (caught) {\n console.error(caught)\n setError('Could not save the field. Try again.')\n } finally {\n setBusy(false)\n }\n }, [canSubmit, derivedKey, onSubmit, label, type, options, required])\n\n return (\n <Drawer anchor=\"right\" open={open} onClose={onClose}>\n <Stack spacing={2} sx={{ width: 360, p: 3 }}>\n <Typography variant=\"h6\">\n {editing ? `Edit ${noun} field` : `New ${noun} field`}\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {fieldDrawerMappingNote(object)}\n </Typography>\n <TextField\n size=\"small\"\n label=\"Label\"\n value={label}\n onChange={(event) => setLabel(event.target.value)}\n slotProps={{ htmlInput: { maxLength: LABEL_MAX_LENGTH } }}\n autoFocus={!editing}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Key\"\n value={shownKey}\n onChange={(event) => {\n setKeyTouched(true)\n setKey(event.target.value)\n }}\n disabled={editing}\n error={!editing && (keyTaken || (shownKey.length > 0 && !derivedKey))}\n helperText={keyHelp}\n slotProps={{ htmlInput: { maxLength: 40, spellCheck: false } }}\n fullWidth\n />\n <TextField\n select\n size=\"small\"\n label=\"Type\"\n value={type}\n onChange={(event) => {\n const next = event.target.value\n if (isContactFieldType(next)) setType(next)\n }}\n disabled={editing}\n helperText={\n editing\n ? 'Values are read by type, so it is fixed once the field exists.'\n : undefined\n }\n fullWidth\n >\n {CONTACT_FIELD_TYPES.map((option) => (\n <MenuItem key={option} value={option}>\n {CONTACT_FIELD_TYPE_LABELS[option]}\n </MenuItem>\n ))}\n </TextField>\n {type === 'select' ? (\n <TextField\n size=\"small\"\n label=\"Choices\"\n value={optionsText}\n onChange={(event) => setOptionsText(event.target.value)}\n helperText=\"One per line. A stored value has to be one of these.\"\n multiline\n minRows={3}\n fullWidth\n />\n ) : null}\n <FormControlLabel\n control={\n <Switch\n checked={required}\n onChange={(event) => setRequired(event.target.checked)}\n />\n }\n label={`Required on the ${noun} form`}\n />\n {error ? (\n <Typography variant=\"body2\" color=\"error\">\n {error}\n </Typography>\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!canSubmit}\n onClick={handleSubmit}\n >\n {editing ? 'Save' : 'Create field'}\n </Button>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n </Drawer>\n )\n}\nContactFieldDrawer.displayName = 'ContactFieldDrawer'\n\nexport default ContactFieldDrawer\n"],"names":["CONTACT_FIELD_TYPE_LABELS","CONTACT_FIELD_TYPES","isContactFieldType","normalizeContactFieldKey","Button","Drawer","FormControlLabel","MenuItem","Stack","Switch","TextField","Typography","useCallback","useEffect","useMemo","useState","OBJECT_NOUN","contact","company","deal","lead","IMPORTABLE","fieldDrawerMappingNote","object","filled","OPTIONS_MAX","OPTION_MAX_LENGTH","LABEL_MAX_LENGTH","parseOptions","text","seen","Set","options","line","split","option","trim","slice","has","add","push","length","ContactFieldDrawer","props","open","onClose","definition","takenKeys","onSubmit","editing","Boolean","noun","label","setLabel","key","setKey","keyTouched","setKeyTouched","type","setType","optionsText","setOptionsText","required","setRequired","busy","setBusy","error","setError","join","derivedKey","shownKey","keyTaken","includes","keyHelp","canSubmit","handleSubmit","caught","console","anchor","spacing","sx","width","p","variant","color","size","value","onChange","event","target","slotProps","htmlInput","maxLength","autoFocus","fullWidth","disabled","helperText","spellCheck","select","next","undefined","map","multiline","minRows","control","checked","direction","onClick","displayName"],"mappings":"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"}
@@ -2,7 +2,8 @@ import { type ConsolePluginPageProps } from '@aglyn/aglyn';
2
2
  export type ContactsFieldsSectionProps = Pick<ConsolePluginPageProps, 'hostId' | 'org'>;
3
3
  /**
4
4
  * `/crm/fields` — the custom fields a holder keeps on a person (AGL-2601),
5
- * and since AGL-2661 on a company and a deal too, one tab each.
5
+ * since AGL-2661 on a company and a deal too, and since AGL-3272 on a lead:
6
+ * one tab each, and every CRM record describable in the org's own words.
6
7
  *
7
8
  * Definitions live in `orgs/{orgId}/contactFields`, one document per field,
8
9
  * and the VALUES live under each contact facet's `custom` keyed by the
@@ -35,16 +35,19 @@ import { crmTaskCallScope } from "../model/task-routes.js";
35
35
  /** What each tab says the fields are for, above its list. */ const OBJECT_INTRO = {
36
36
  contact: 'The custom fields on a contact — text, number, date, choice, ' + 'checkbox or link. They show on every contact, as columns on the ' + 'list, and a form field can save into one.',
37
37
  company: 'The custom fields on a company. They show on every company’s page ' + 'and its edit form, as columns on the companies list, and a CSV ' + 'import can fill them.',
38
- deal: 'The custom fields on a deal. They show on every deal’s page and ' + 'its edit form, and as columns on the deals table.'
38
+ deal: 'The custom fields on a deal. They show on every deal’s page and ' + 'its edit form, and as columns on the deals table.',
39
+ lead: 'The custom fields on a lead. They show on every lead’s page and the ' + 'New lead drawer, and as columns on the leads list. They stay on the ' + 'lead when it converts — a contact’s fields are its own.'
39
40
  };
40
41
  /** The noun each tab's empty state and captions use. */ const OBJECT_NOUN = {
41
42
  contact: 'contact',
42
43
  company: 'company',
43
- deal: 'deal'
44
+ deal: 'deal',
45
+ lead: 'lead'
44
46
  };
45
47
  /**
46
48
  * `/crm/fields` — the custom fields a holder keeps on a person (AGL-2601),
47
- * and since AGL-2661 on a company and a deal too, one tab each.
49
+ * since AGL-2661 on a company and a deal too, and since AGL-3272 on a lead:
50
+ * one tab each, and every CRM record describable in the org's own words.
48
51
  *
49
52
  * Definitions live in `orgs/{orgId}/contactFields`, one document per field,
50
53
  * and the VALUES live under each contact facet's `custom` keyed by the
@@ -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":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SACEA,yBAAyB,EAEzBC,iBAAiB,EACjBC,eAAe,EACfC,uBAAuB,EACvBC,iBAAiB,EAEjBC,gBAAgB,EAChBC,sBAAsB,EACtBC,eAAe,EACfC,cAAc,QACT,eAAc;AACrB,OAAOC,yBAAyB,wDAAuD;AACvF,SACEC,wBAAwB,EACxBC,iBAAiB,EACjBC,YAAY,EACZC,UAAU,EACVC,gBAAgB,EAChBC,gBAAgB,QACX,yBAAwB;AAC/B,SACEC,WAAW,EACXC,OAAO,EACPC,MAAM,EACNC,sBAAsB,QACjB,uBAAsB;AAC7B,OAAOC,oBAEA,6DAA4D;AACnE,SAASC,WAAW,QAAQ,yDAAwD;AACpF,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,YAAY,EACZC,OAAO,EACPC,kBAAkB,QACb,iCAAgC;AACvC,SACEC,MAAM,EACNC,IAAI,EACJC,UAAU,EACVC,KAAK,EACLC,GAAG,EACHC,SAAS,EACTC,SAAS,EACTC,SAAS,EACTC,QAAQ,EACRC,IAAI,EACJC,UAAU,QACL,gBAAe;AACtB,SACEC,UAAU,EACVC,SAAS,EACTC,GAAG,EACHC,MAAM,EACNC,SAAS,EACTC,UAAU,QACL,qBAAoB;AAC3B,SAASC,WAAW,EAAEC,QAAQ,QAAQ,QAAO;AAC7C,SAEEC,0BAA0B,QACrB,4CAAwC;AAC/C,SAASC,WAAW,QAAQ,4BAAwB;AACpD,OAAOC,wBAAoD,4BAAwB;AACnF,SAASC,2BAA2B,QAAQ,gCAA4B;AACxE,SAASC,gBAAgB,QAAQ,0BAAsB;AAIvD,2DAA2D,GAC3D,MAAMC,eAA+C;IACnDC,SACE,kEACA,qEACA;IACFC,SACE,uEACA,oEACA;IACFC,MACE,qEACA;IACFC,MACE,yEACA,yEACA;AACJ;AAEA,sDAAsD,GACtD,MAAMC,cAA8C;IAClDJ,SAAS;IACTC,SAAS;IACTC,MAAM;IACNC,MAAM;AACR;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASE,sBAAsBC,KAAiC;;IACrE,MAAM,EAAEC,MAAM,EAAEC,GAAG,EAAE,GAAGF;IACxB,MAAMG,YAAYrC;IAClB,MAAM,EAAEsC,eAAe,EAAE,GAAGvC;IAC5B,MAAM,EAAEwC,OAAO,EAAE,GAAG3C;IACpB,yEAAyE;IACzE,0EAA0E;IAC1E,yEAAyE;IACzE,+DAA+D;IAC/D,MAAM,EAAE4C,KAAK,EAAEC,OAAOC,UAAU,EAAEC,YAAY,EAAE,GAAGpB,YAAY;QAAEY;QAAQC;IAAI;IAC7E,MAAMQ,gBAAQJ,yBAAAA,KAAO,CAAC,EAAE,mBAAI;IAC5B;;;;;;GAMC,GACD,MAAM,CAACK,QAAQC,UAAU,GAAGzB,SAAyB;IACrD,MAAM,EAAE0B,WAAW,EAAEN,KAAK,EAAEO,SAAS,EAAE,GAAG1B,2BAA2BsB,OAAOC;IAC5E,MAAMI,OAAOjB,WAAW,CAACa,OAAO;IAEhC;;;;;;;GAOC,GACD,MAAM,EAAEK,MAAMC,IAAI,EAAE,GAAGlD;IACvB,MAAM,CAACmD,aAAaC,eAAe,GAAGhC,SAAS;IAC/C,MAAMiC,wBAAwBlC,YAAY;QACxC,MAAMmC,YAAY7B,iBAAiBS,QAAQS;QAC3C,IAAI,CAACW,aAAaH,aAAa;QAC/B,MAAMI,YAAY,MAAMjB,QAAQ;YAC9BkB,OAAO;YACPC,aACE,mEACA,2EACA;YACFC,kBAAkB;QACpB,GACGC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAACL,WAAW;QAChBH,eAAe;QACf,IAAI;gBAIWS;YAHb,MAAMA,SAAS,MAAMrC,4BAA4B0B,MAAMI;YACvDjB,gBACE,CAAC,0BAA0B,EAAEwB,OAAOC,OAAO,CAACC,cAAc,GAAG,OAAO,EAAEF,OAAOC,OAAO,KAAK,IAAI,KAAK,IAAI,CAAC,CAAC,GACtG,CAAC,KAAK,EAAE,EAACD,gBAAAA,OAAOG,KAAK,YAAZH,gBAAgB,GAAGE,cAAc,GAAG,UAAU,EAAEF,OAAOG,KAAK,KAAK,IAAI,KAAK,KAAK,GACvFH,CAAAA,OAAOI,SAAS,GACb,qEACA,EAAC,GACP;gBAAEC,SAAS;YAAU;QAEzB,EAAE,OAAOC,OAAO;YACd9B,gBACE8B,iBAAiBC,QAAQD,MAAME,OAAO,GAAG,8CACzC;gBAAEH,SAAS;YAAU;QAEzB,SAAU;YACRd,eAAe;QACjB;IACF,GAAG;QAAClB;QAAQS;QAAOQ;QAAab;QAASY;QAAMb;KAAgB;IAE/D,MAAM,CAACiC,YAAYC,cAAc,GAAGnD,SAAS;IAC7C,MAAM,CAACoD,SAASC,WAAW,GAAGrD,SAA2C;IACzE,MAAM,CAACsD,QAAQC,UAAU,GAAGvD,SAAS;IAErC,MAAMwD,WAAWzD,YACf,CAAC0D;QACC,IAAI,CAACtC,OAAO,MAAM,IAAI6B,MAAM;QAC5B,OAAOrD,IAAIqB,WAAWG,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAE7D,gBAAgBoG,aAAa,EAAED;IAC3E,GACA;QAACzC;QAAWG;KAAM;IAGpB,MAAMwC,aAAa5D,YAAY;QAC7BsD,WAAW;QACXF,cAAc;IAChB,GAAG,EAAE;IACL,MAAMS,WAAW7D,YAAY,CAAC8D;QAC5BR,WAAWQ;QACXV,cAAc;IAChB,GAAG,EAAE;IAEL;;;;;;;;;;GAUC,GACD,MAAMW,eAAe/D,YACnB,OAAOgE;QACL,IAAI,CAAC5C,OAAO;QACZ,MAAM6C,MAAM,IAAIC;QAChB,IAAIb,SAAS;YACX,MAAMc,UAAU,MAAMrF,mBACpB;gBAAEsF,SAAS;gBAASxC;YAAU,GAC9B;gBACE,MAAM9B,UAAU2D,SAASJ,QAAQgB,GAAG,GAAG;oBACrCC,OAAON,MAAMM,KAAK;mBACdjB,QAAQkB,IAAI,KAAK,WAAW;oBAAEC,SAASR,MAAMQ,OAAO;gBAAC,IAAI,CAAC;oBAC9DC,UAAUT,MAAMS,QAAQ;oBACxBC,WAAWT;;YAEf;YAEF,IAAI,CAACE,QAAQQ,EAAE,EAAE;gBACf,OAAO,KAAKzD,gBAAgBiD,QAAQjB,OAAO,EAAE;oBAC3CH,SAAS;oBACT6B,SAAS;gBACX;YACF;YACA1D,gBAAgB,eAAe;gBAAE6B,SAAS;gBAAW6B,SAAS;YAAM;QACtE,OAAO;YACL,MAAMC,QACJlD,YAAYmD,MAAM,CAChB,CAACC,SAASjB,aAAekB,KAAKC,GAAG,CAACF,SAASG,OAAOpB,WAAWe,KAAK,KAAK,IACvE,CAAC,KACC;YACN,MAAMhF,OACJD,IACEF,WAAWuB,WAAWG,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAE7D,gBAAgBoG,aAAa,GACvErG,sBAEF;gBACE6H,KAAKnB,MAAMmB,GAAG;gBACdb,OAAON,MAAMM,KAAK;gBAClBC,MAAMP,MAAMO,IAAI;eACZP,MAAMO,IAAI,KAAK,WAAW;gBAAEC,SAASR,MAAMQ,OAAO;YAAC,IAAI,CAAC;gBAC5DC,UAAUT,MAAMS,QAAQ;gBACxBI;gBACAO,WAAW;gBACX,6DAA6D;gBAC7D3D;gBACAV,QAAQQ;eACL5D,uBAAuB;gBAACC;aAAgB;gBAC3CyH,WAAWpB;gBACXS,WAAWT;;YAGf/C,gBAAgB,CAAC,OAAO,EAAE8C,MAAMM,KAAK,CAAC,OAAO,CAAC,EAAE;gBAC9CvB,SAAS;gBACT6B,SAAS;YACX;QACF;QACAxB,cAAc;QACdE,WAAW;IACb,GACA;QAAClC;QAAOiC;QAASzB;QAAW6B;QAAUvC;QAAiBS;QAAaV;QAAWM;QAAcE;KAAO;IAGtG;;;;;;;;;;GAUC,GACD,MAAM6D,OAAOtF,YACX,OAAO8D,YAAuCyB;QAC5C,IAAI,CAACnE,SAASmC,QAAQ;QACtB,MAAMiC,QAAQ7D,YAAY8D,SAAS,CAAC,CAACC,QAAUA,MAAMrB,GAAG,KAAKP,WAAWO,GAAG;QAC3E,MAAMsB,SAASH,QAAQD;QACvB,IAAIC,QAAQ,KAAKG,SAAS,KAAKA,UAAUhE,YAAYiE,MAAM,EAAE;QAC7D,MAAMC,OAAO;eAAIlE;SAAY;QAC7BkE,IAAI,CAACL,MAAM,GAAG7D,WAAW,CAACgE,OAAO;QACjCE,IAAI,CAACF,OAAO,GAAGhE,WAAW,CAAC6D,MAAM;QACjChC,UAAUM,WAAWO,GAAG;QACxB,IAAI;YACF,MAAMF,UAAU,MAAMrF,mBACpB;gBAAEsF,SAAS;gBAAexC;YAAU,GACpC;gBACE,MAAMkE,QAAQ/F,WAAWkB;gBACzB,MAAMgD,MAAM,IAAIC;gBAChB2B,KAAKE,OAAO,CAAC,CAACL,OAAOM;oBACnB,IAAIN,MAAMb,KAAK,KAAKmB,UAAU;oBAC9BF,MAAMG,MAAM,CAACxC,SAASiC,MAAMrB,GAAG,GAAG;wBAAEQ,OAAOmB;wBAAUtB,WAAWT;oBAAI;gBACtE;gBACA,MAAM6B,MAAMI,MAAM;YACpB;YAEF,IAAI,CAAC/B,QAAQQ,EAAE,EAAE;gBACfzD,gBAAgBiD,QAAQjB,OAAO,EAAE;oBAAEH,SAAS;oBAAW6B,SAAS;gBAAM;YACxE;QACF,EAAE,OAAOuB,OAAO;YACdC,QAAQD,KAAK,CAACA;YACdjF,gBAAgB,yBAAyB;gBAAE6B,SAAS;gBAASsD,gBAAgB;YAAK;QACpF,SAAU;YACR7C,UAAU;QACZ;IACF,GACA;QAACpC;QAAOmC;QAAQ5B;QAAaC;QAAWX;QAAWwC;QAAUvC;KAAgB;IAG/E;;;;;;GAMC,GACD,MAAMoF,gBAAgBtG,YACpB,OAAO8D;QACL,IAAI,CAAC1C,SAASmC,QAAQ;QACtB,IAAI,CAACO,WAAWsB,SAAS,EAAE;YACzB,MAAMmB,WAAW,MAAMpF,QAAQ;gBAC7BkB,OAAO,CAAC,QAAQ,EAAEyB,WAAWQ,KAAK,CAAC,EAAE,CAAC;gBACtChC,aACE,CAAC,gBAAgB,EAAET,KAAK,sCAAsC,CAAC,GAC/D,kEACA;gBACFU,kBAAkB;YACpB,EACE,oEAAoE;YACpE,yDAAyD;aACxDC,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;YACf,IAAI,CAAC8D,UAAU;QACjB;QACA/C,UAAUM,WAAWO,GAAG;QACxB,IAAI;YACF,MAAMvE,UAAU2D,SAASK,WAAWO,GAAG,GAAG;gBACxCe,WAAWtB,WAAWsB,SAAS,GAAG,OAAOlB,KAAKD,GAAG;gBACjDS,WAAW,IAAIR;YACjB;YACAhD,gBAAgB4C,WAAWsB,SAAS,GAAG,mBAAmB,iBAAiB;gBACzErC,SAAS;gBACT6B,SAAS;YACX;QACF,EAAE,OAAOuB,OAAO;YACdC,QAAQD,KAAK,CAACA;YACdjF,gBAAgB,yBAAyB;gBAAE6B,SAAS;gBAASsD,gBAAgB;YAAK;QACpF,SAAU;YACR7C,UAAU;QACZ;IACF,GACA;QAACpC;QAAOmC;QAAQpC;QAASsC;QAAUvC;QAAiBW;KAAK;IAG3D;;;;;;;;GAQC,GACD,MAAM2E,eAAexG,YACnB,OAAO8D;QACL,IAAI,CAAC1C,SAASmC,UAAU,CAACO,WAAWsB,SAAS,EAAE;QAC/C,MAAMmB,WAAW,MAAMpF,QAAQ;YAC7BkB,OAAO,CAAC,QAAQ,EAAEyB,WAAWQ,KAAK,CAAC,EAAE,CAAC;YACtChC,aACE,oEACA,CAAC,CAAC,EAAEwB,WAAWqB,GAAG,CAAC,cAAc,EAAEtD,KAAK,sBAAsB,CAAC,GAC/D,qEACA;YACFU,kBAAkB;YAClBkE,yBAAyB;gBAAEC,OAAO;YAAQ;QAC5C,GACGlE,IAAI,CAAC,IAAM,MACXC,KAAK,CAAC,IAAM;QACf,IAAI,CAAC8D,UAAU;QACf/C,UAAUM,WAAWO,GAAG;QACxB,IAAI;YACF,MAAM1E,UAAU8D,SAASK,WAAWO,GAAG;YACvCnD,gBAAgB,iBAAiB;gBAAE6B,SAAS;gBAAW6B,SAAS;YAAM;QACxE,EAAE,OAAOuB,OAAO;YACdC,QAAQD,KAAK,CAACA;YACdjF,gBAAgB,yBAAyB;gBAAE6B,SAAS;gBAASsD,gBAAgB;YAAK;QACpF,SAAU;YACR7C,UAAU;QACZ;IACF,GACA;QAACpC;QAAOmC;QAAQpC;QAASsC;QAAUvC;QAAiBW;KAAK;IAG3D,MAAM8E,aAAa,CAAC7C,aAAgE;YAClF;gBACEqB,KAAK;gBACLb,OAAO;gBACPsC,oBAAM,KAACtI;oBAAQuI,MAAMzI,iBAAiByI,IAAI;oBAAEC,MAAM;;gBAClDC,SAAS,IAAMlD,SAASC;YAC1B;YACA;gBACEqB,KAAK;gBACLb,OAAOR,WAAWsB,SAAS,GAAG,YAAY;gBAC1CwB,oBACE,KAACtI;oBACCuI,MAAM/C,WAAWsB,SAAS,GAAGrH,yBAAyB8I,IAAI,GAAG7I,kBAAkB6I,IAAI;oBACnFC,MAAM;;gBAGVE,aAAa,CAAClD,WAAWsB,SAAS;gBAClC6B,UAAUC,QAAQ3D;gBAClB4D,gBAAgB5D,SAAS,iCAAiC6D;gBAC1DL,SAAS,IAAM,KAAKT,cAAcxC;YACpC;eACIA,WAAWsB,SAAS,GACpB;gBACE;oBACED,KAAK;oBACLb,OAAO;oBACPsC,oBAAM,KAACtI;wBAAQuI,MAAM1I,iBAAiB0I,IAAI;wBAAEC,MAAM;;oBAClDE,aAAa;oBACbC,UAAUC,QAAQ3D;oBAClB4D,gBAAgB5D,SAAS,iCAAiC6D;oBAC1DL,SAAS,IAAM,KAAKP,aAAa1C;gBACnC;aACD,GACD,EAAE;SACP;IAED,qBACE,MAACzF;QACCgJ,QAAQ;QACRC,MAAMzJ,eAAe,iBAAiB;YAAE0J,QAAQ;QAAkB;QAClEC,cAAc;QACdC,cAAc;QACdC,iBAAgB;QAChBC,aAAa;YACXC,QAAQxG,sBACN,MAAClC;gBAAMqG,WAAU;gBAAMsC,SAAS;gBAAGC,IAAI;oBAAEC,YAAY;gBAAS;;kCAC5D,KAAChJ;wBAAO+H,MAAK;wBAAQC,SAAS,IAAM,KAAK7E;wBAAyB+E,UAAUjF;kCACzEA,cAAc,iBAAiB;;kCAElC,KAACjD;wBAAOgE,SAAQ;wBAAYgE,SAASnD;kCAClC;;;iBAGH;QACN;;0BAEA,MAAC1E;gBAAM2I,SAAS;;kCACd,KAACrI;wBACCwI,OAAOvG;wBACPwG,UAAU,CAACC,QAAQrC;4BACjB,IAAInI,iBAAiBmI,OAAOnE,UAAUmE;wBACxC;wBACAsC,cAAW;kCAEV1K,kBAAkB2K,GAAG,CAAC,CAAC1C,sBACtB,KAACvG;gCAAgB6I,OAAOtC;gCAAOpB,OAAO9G,uBAAuB,CAACkI,MAAM;+BAA1DA;;kCAGd,KAACjG;wBAAWsD,SAAQ;wBAAQ2D,OAAM;kCAC/B,GAAGnG,YAAY,CAACkB,OAAO,CAAC,6CAA6C,CAAC,GACrE;;oBAEHH,cAAc,CAACF,sBACd,KAAC3B;wBAAWsD,SAAQ;wBAAQ2D,OAAM;kCAC/B,CAAC,4CAA4C,EAAE7E,KAAK,QAAQ,CAAC;yBAE9D,CAACR,QAAQ,OAAOM,YAAYiE,MAAM,KAAK,kBACzC,KAAC9H;wBACCwG,OAAO,CAAC,UAAU,EAAEzC,KAAK,WAAW,CAAC;wBACrCS,aAAa,CAAC,yCAAyC,EAAET,KAAK,uBAAuB,CAAC;wBACtF+F,QACExG,sBACE,KAACrC;4BAAOgE,SAAQ;4BAAYgE,SAASnD;sCAClC;6BAEDwD;uCAIR,MAAC1I;wBAAYoI,MAAK;;0CAChB,KAACxH;0CACC,cAAA,MAACC;;sDACC,KAACF;4CAAUyI,IAAI;gDAAEO,OAAO;4CAAG;sDAAI;;sDAC/B,KAAChJ;sDAAW;;sDACZ,KAACA;sDAAW;;sDACZ,KAACA;sDAAW;;sDACZ,KAACA;sDAAW;;sDACZ,KAACA;4CAAUiJ,OAAM;;;;;0CAGrB,KAAClJ;0CACEuC,YAAYyG,GAAG,CAAC,CAACtE,YAAY0B;wCA6CrBnI;wCACgCyG;yDA7CvC,MAACvE;wCAECgJ,KAAK;wCACLT,IAAIhE,WAAWsB,SAAS,GAAG;4CAAEoD,SAAS;wCAAI,IAAIpB;;0DAE9C,KAAC/H;0DACC,cAAA,MAACH;oDAAMqG,WAAU;oDAAMsC,SAAS;;sEAC9B,MAAC5I;4DACC6H,MAAK;4DACLG,UAAUzB,UAAU,KAAK0B,QAAQ3D;4DACjCwD,SAAS,IAAM,KAAKzB,KAAKxB,YAAY,CAAC;;8EAEtC,KAACxF;oEAAQuI,MAAM3I,WAAW2I,IAAI;oEAAEC,MAAM;;8EACtC,KAACvI;8EAAQ,CAAC,KAAK,EAAEuF,WAAWQ,KAAK,CAAC,GAAG,CAAC;;;;sEAExC,MAACrF;4DACC6H,MAAK;4DACLG,UAAUzB,UAAU7D,YAAYiE,MAAM,GAAG,KAAKsB,QAAQ3D;4DACtDwD,SAAS,IAAM,KAAKzB,KAAKxB,YAAY;;8EAErC,KAACxF;oEAAQuI,MAAM5I,aAAa4I,IAAI;oEAAEC,MAAM;;8EACxC,KAACvI;8EAAQ,CAAC,KAAK,EAAEuF,WAAWQ,KAAK,CAAC,KAAK,CAAC;;;;;;;0DAI9C,KAACjF;0DACC,cAAA,MAACH;oDACCqG,WAAU;oDACVsC,SAAS;oDACTC,IAAI;wDAAEW,UAAU;wDAAQV,YAAY;oDAAS;;sEAE7C,KAACtI;4DAAWsD,SAAQ;sEAASe,WAAWQ,KAAK;;wDAC5CR,WAAWsB,SAAS,iBACnB,KAACpG;4DAAK8H,MAAK;4DAAQ/D,SAAQ;4DAAW2D,OAAM;4DAAUpC,OAAM;6DAC1D;;;;0DAGR,KAACjF;0DACC,cAAA,KAACI;oDAAWsD,SAAQ;oDAAQ2D,OAAM;oDAAiBoB,IAAI;wDAAEY,YAAY;oDAAY;8DAC9E5E,WAAWqB,GAAG;;;0DAGnB,KAAC9F;0DACC,cAAA,MAACI;oDAAWsD,SAAQ;;yDACjB1F,6CAAAA,yBAAyB,CAACyG,WAAWS,IAAI,CAAC,YAA1ClH,6CAA8CyG,WAAWS,IAAI;wDAC7DT,WAAWS,IAAI,KAAK,cAAYT,sBAAAA,WAAWU,OAAO,qBAAlBV,oBAAoB8B,MAAM,IACvD,CAAC,GAAG,EAAE9B,WAAWU,OAAO,CAACoB,MAAM,CAAC,QAAQ,CAAC,GACzC;;;;0DAGR,KAACvG;0DACC,cAAA,KAACI;oDAAWsD,SAAQ;oDAAQ2D,OAAM;8DAC/B5C,WAAWW,QAAQ,GAAG,QAAQ;;;0DAGnC,KAACpF;gDAAUiJ,OAAM;gDAAQR,IAAI;oDAAEO,OAAO;gDAAG;0DACvC,cAAA,KAAC5J;oDAAe6F,OAAOR,WAAWQ,KAAK;oDAAEqE,OAAOhC,WAAW7C;;;;uCAvDxDA,WAAWO,GAAG;;;;;oBA8D5B1C,YAAYiE,MAAM,iBACjB,KAACnG;wBAAWsD,SAAQ;wBAAU2D,OAAM;kCACjC,CAAC,SAAS,EAAE7E,KAAK,wCAAwC,CAAC,GACzD,CAAC,gCAAgC,EAAEA,KAAK,qBAAqB,CAAC,GAC9D;yBAEF;;;0BAEN,KAACzB;gBACCwI,MAAMzF;gBACN0F,SAAS,IAAMzF,cAAc;gBAC7BU,YAAYT;gBACZ5B,QAAQA;gBACRqH,WAAWnH,YAAYyG,GAAG,CAAC,CAACtE,aAAeA,WAAWqB,GAAG;gBACzD4D,UAAUhF;;;;AAIlB;AACAlD,sBAAsBmI,WAAW,GAAG;AAEpC,eAAenI,sBAAqB"}
@@ -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;