@aglyn/plugins-crm 1.0.0 → 1.1.0

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 (154) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/activity-link-fields.d.ts +34 -0
  3. package/src/lib/components/activity-link-fields.js +141 -0
  4. package/src/lib/components/activity-link-fields.js.map +1 -0
  5. package/src/lib/components/activity-list.d.ts +8 -0
  6. package/src/lib/components/activity-list.js +4 -2
  7. package/src/lib/components/activity-list.js.map +1 -1
  8. package/src/lib/components/activity-queries.d.ts +7 -2
  9. package/src/lib/components/activity-queries.js +7 -3
  10. package/src/lib/components/activity-queries.js.map +1 -1
  11. package/src/lib/components/company-contact-roles-card.d.ts +42 -0
  12. package/src/lib/components/company-contact-roles-card.js +135 -0
  13. package/src/lib/components/company-contact-roles-card.js.map +1 -0
  14. package/src/lib/components/company-detail-page.js +12 -1
  15. package/src/lib/components/company-detail-page.js.map +1 -1
  16. package/src/lib/components/company-edit-drawer.js +9 -6
  17. package/src/lib/components/company-edit-drawer.js.map +1 -1
  18. package/src/lib/components/company-picker.js +4 -2
  19. package/src/lib/components/company-picker.js.map +1 -1
  20. package/src/lib/components/contact-associations-card.js +15 -1
  21. package/src/lib/components/contact-associations-card.js.map +1 -1
  22. package/src/lib/components/contact-detail-page.js +13 -14
  23. package/src/lib/components/contact-detail-page.js.map +1 -1
  24. package/src/lib/components/contact-list-columns.js +5 -4
  25. package/src/lib/components/contact-list-columns.js.map +1 -1
  26. package/src/lib/components/contact-properties-card.js +41 -20
  27. package/src/lib/components/contact-properties-card.js.map +1 -1
  28. package/src/lib/components/contact-roles-editor.d.ts +54 -0
  29. package/src/lib/components/contact-roles-editor.js +270 -0
  30. package/src/lib/components/contact-roles-editor.js.map +1 -0
  31. package/src/lib/components/contacts-section.js +6 -4
  32. package/src/lib/components/contacts-section.js.map +1 -1
  33. package/src/lib/components/crm-console-sections.js +1 -1
  34. package/src/lib/components/crm-console-sections.js.map +1 -1
  35. package/src/lib/components/crm-record-picker.d.ts +19 -0
  36. package/src/lib/components/crm-record-picker.js +40 -11
  37. package/src/lib/components/crm-record-picker.js.map +1 -1
  38. package/src/lib/components/crm-related-to-picker.d.ts +33 -0
  39. package/src/lib/components/crm-related-to-picker.js +135 -0
  40. package/src/lib/components/crm-related-to-picker.js.map +1 -0
  41. package/src/lib/components/crm-site-picker.d.ts +13 -1
  42. package/src/lib/components/crm-site-picker.js +15 -6
  43. package/src/lib/components/crm-site-picker.js.map +1 -1
  44. package/src/lib/components/crm-transfer-buttons.d.ts +5 -0
  45. package/src/lib/components/crm-transfer-buttons.js +9 -6
  46. package/src/lib/components/crm-transfer-buttons.js.map +1 -1
  47. package/src/lib/components/deal-contact-roles-card.js +21 -239
  48. package/src/lib/components/deal-contact-roles-card.js.map +1 -1
  49. package/src/lib/components/deal-detail-page.js +2 -1
  50. package/src/lib/components/deal-detail-page.js.map +1 -1
  51. package/src/lib/components/deal-edit-drawer.js +11 -7
  52. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  53. package/src/lib/components/deals-section.js +5 -4
  54. package/src/lib/components/deals-section.js.map +1 -1
  55. package/src/lib/components/log-activity-dialog.d.ts +27 -9
  56. package/src/lib/components/log-activity-dialog.js +96 -32
  57. package/src/lib/components/log-activity-dialog.js.map +1 -1
  58. package/src/lib/components/new-contact-drawer.d.ts +7 -3
  59. package/src/lib/components/new-contact-drawer.js +27 -15
  60. package/src/lib/components/new-contact-drawer.js.map +1 -1
  61. package/src/lib/components/pipeline-stages-dialog.js +5 -1
  62. package/src/lib/components/pipeline-stages-dialog.js.map +1 -1
  63. package/src/lib/components/record-activity-card.d.ts +11 -2
  64. package/src/lib/components/record-activity-card.js +90 -5
  65. package/src/lib/components/record-activity-card.js.map +1 -1
  66. package/src/lib/components/settings-section.d.ts +4 -2
  67. package/src/lib/components/settings-section.js +7 -5
  68. package/src/lib/components/settings-section.js.map +1 -1
  69. package/src/lib/hooks/crm-activity-client-reads.d.ts +8 -0
  70. package/src/lib/hooks/crm-activity-client-reads.js +50 -0
  71. package/src/lib/hooks/crm-activity-client-reads.js.map +1 -0
  72. package/src/lib/hooks/use-campaign-filing-log.d.ts +2 -1
  73. package/src/lib/hooks/use-campaign-filing-log.js +10 -3
  74. package/src/lib/hooks/use-campaign-filing-log.js.map +1 -1
  75. package/src/lib/hooks/use-crm-org-mount.d.ts +8 -6
  76. package/src/lib/hooks/use-crm-org-mount.js +2 -1
  77. package/src/lib/hooks/use-crm-org-mount.js.map +1 -1
  78. package/src/lib/hooks/use-crm-scope.d.ts +17 -5
  79. package/src/lib/hooks/use-crm-scope.js +12 -5
  80. package/src/lib/hooks/use-crm-scope.js.map +1 -1
  81. package/src/lib/model/contact-holder.d.ts +12 -0
  82. package/src/lib/model/contact-holder.js +20 -1
  83. package/src/lib/model/contact-holder.js.map +1 -1
  84. package/src/lib/model/contact-record.d.ts +5 -0
  85. package/src/lib/model/contact-record.js +7 -1
  86. package/src/lib/model/contact-record.js.map +1 -1
  87. package/src/lib/model/contact-update.d.ts +6 -0
  88. package/src/lib/model/contact-update.js +1 -0
  89. package/src/lib/model/contact-update.js.map +1 -1
  90. package/src/lib/model/crm-activity-link.d.ts +117 -0
  91. package/src/lib/model/crm-activity-link.js +113 -0
  92. package/src/lib/model/crm-activity-link.js.map +1 -0
  93. package/src/lib/model/crm-api.d.ts +4 -1
  94. package/src/lib/model/crm-api.js +9 -5
  95. package/src/lib/model/crm-api.js.map +1 -1
  96. package/src/lib/model/deal-board-model.js +5 -3
  97. package/src/lib/model/deal-board-model.js.map +1 -1
  98. package/src/lib/model/deal-contact-roles.d.ts +15 -1
  99. package/src/lib/model/deal-contact-roles.js +17 -3
  100. package/src/lib/model/deal-contact-roles.js.map +1 -1
  101. package/src/lib/model/settle-stage-names.d.ts +25 -0
  102. package/src/lib/model/settle-stage-names.js +29 -0
  103. package/src/lib/model/settle-stage-names.js.map +1 -0
  104. package/src/lib/native-nav.js +1 -1
  105. package/src/lib/native-nav.js.map +1 -1
  106. package/src/lib/server/add-contact-by-hand.d.ts +84 -0
  107. package/src/lib/server/add-contact-by-hand.js +212 -0
  108. package/src/lib/server/add-contact-by-hand.js.map +1 -0
  109. package/src/lib/server/api-v1/contacts.js +61 -23
  110. package/src/lib/server/api-v1/contacts.js.map +1 -1
  111. package/src/lib/server/api-v1/crm-activities.js +13 -3
  112. package/src/lib/server/api-v1/crm-activities.js.map +1 -1
  113. package/src/lib/server/api-v1/crm-pipelines.d.ts +1 -1
  114. package/src/lib/server/api-v1/crm-pipelines.js +8 -2
  115. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -1
  116. package/src/lib/server/api-v1/crm-shared.d.ts +16 -9
  117. package/src/lib/server/api-v1/crm-shared.js +23 -11
  118. package/src/lib/server/api-v1/crm-shared.js.map +1 -1
  119. package/src/lib/server/api-v1/openapi.js +3 -3
  120. package/src/lib/server/api-v1/openapi.js.map +1 -1
  121. package/src/lib/server/automation-steps.js +11 -9
  122. package/src/lib/server/automation-steps.js.map +1 -1
  123. package/src/lib/server/campaign-filing-activity.js +7 -2
  124. package/src/lib/server/campaign-filing-activity.js.map +1 -1
  125. package/src/lib/server/contact-update.d.ts +4 -0
  126. package/src/lib/server/contact-update.js +71 -10
  127. package/src/lib/server/contact-update.js.map +1 -1
  128. package/src/lib/server/convert-host-lead.js +13 -2
  129. package/src/lib/server/convert-host-lead.js.map +1 -1
  130. package/src/lib/server/convert-open-lead.js +8 -1
  131. package/src/lib/server/convert-open-lead.js.map +1 -1
  132. package/src/lib/server/crm-activity-company.d.ts +25 -0
  133. package/src/lib/server/crm-activity-company.js +34 -0
  134. package/src/lib/server/crm-activity-company.js.map +1 -0
  135. package/src/lib/server/crm-inbound-email.js +21 -15
  136. package/src/lib/server/crm-inbound-email.js.map +1 -1
  137. package/src/lib/server/crm-sharing.js +9 -2
  138. package/src/lib/server/crm-sharing.js.map +1 -1
  139. package/src/lib/server/email-send.js +10 -8
  140. package/src/lib/server/email-send.js.map +1 -1
  141. package/src/lib/server/merge-contacts.js +6 -2
  142. package/src/lib/server/merge-contacts.js.map +1 -1
  143. package/src/lib/server/org-caller.d.ts +8 -0
  144. package/src/lib/server/org-caller.js +10 -0
  145. package/src/lib/server/org-caller.js.map +1 -1
  146. package/src/lib/server/record-timeline.d.ts +3 -1
  147. package/src/lib/server/record-timeline.js +11 -3
  148. package/src/lib/server/record-timeline.js.map +1 -1
  149. package/src/lib/server.js +152 -43
  150. package/src/lib/server.js.map +1 -1
  151. package/src/lib/transfer/contacts.js +83 -48
  152. package/src/lib/transfer/contacts.js.map +1 -1
  153. package/src/lib/transfer/fields.js +1 -2
  154. package/src/lib/transfer/fields.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/settings-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 type AglynOrgBilling,\n canManageOrg,\n type ConsolePluginPageProps,\n CRM_ASSIGNMENT_RULES_MAX,\n CRM_ASSIGNMENT_RULES_PATH,\n CRM_AUTO_CREATE_COMPANIES_PATH,\n CRM_DEFAULT_RECORD_SCOPE_PATH,\n CRM_ROUND_ROBIN_POOL_MAX,\n CRM_ROUND_ROBIN_POOL_PATH,\n type CrmAssignmentRule,\n crmDefaultScopeOf,\n crmHostDefaultOwnerSegments,\n describeAssignmentRule,\n orgAutoCreatesCompanies,\n type OrgRole,\n pluginDocsHelp,\n readCrmAssignmentSettings,\n roundRobinOrder,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { mdiArrowDown, mdiArrowUp, mdiDeleteOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu 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 useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Checkbox,\n FormControlLabel,\n FormGroup,\n FormHelperText,\n IconButton,\n MenuItem,\n Stack,\n Switch,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport { deleteField, doc, FieldPath, updateDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport AssignmentRuleDrawer from './assignment-rule-drawer'\nimport EmailTemplatesCard from './email-templates-card'\nimport EmailCaptureCard from './email-capture-card'\nimport RecipesCard from './recipes-card'\nimport SendingAddressesCard from './sending-addresses-card'\nimport SharingRulesCard from './sharing-rules-card'\n\nexport type CrmSettingsSectionProps = Pick<ConsolePluginPageProps, 'hostId' | 'org'>\n\n/**\n * Whether the signed-in member may change an org-wide CRM setting, and\n * whether that is known yet.\n *\n * The org document's client branch admits an OWNER or ADMIN and nobody\n * else — `canManageOrg()` in the rules — so the question is the caller's\n * org role, read off their own membership document, which the rules let a\n * member read for themselves. A scoped collaborator, an editor, a viewer:\n * each sees the switch and cannot move it, with the reason beside it,\n * rather than a switch that moves and snaps back with a bare\n * `permission-denied`. `ready` separates \"no\" from \"not yet\", so the\n * control disables with a reason instead of hiding until the read lands.\n */\nexport function useCanManageCrmSettings(orgId: string | undefined): {\n canManage: boolean\n ready: boolean\n} {\n const firestore = useFirestore()\n const { data: user } = useUser()\n const uid = user?.uid ?? ''\n const { data: member, status } = useFirestoreDoc<{ role?: OrgRole }>(\n () => (orgId && uid ? doc(firestore, 'orgs', orgId, 'members', uid) : null),\n [firestore, orgId, uid],\n )\n return {\n canManage: canManageOrg(member?.role ?? null),\n ready: Boolean(orgId && uid) && status !== 'loading',\n }\n}\n\nexport interface AutoCreateCompaniesCardProps {\n /** The site the section is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * \"Create companies from work email domains\" — the org's one switch over\n * what a capture does with a company nobody has filed yet (AGL-2613).\n *\n * ## What the switch decides, and what it does not\n *\n * A contact captured from `jane@acme.com` is linked to the company at\n * `acme.com` whether this is on or off, provided exactly one such company\n * is visible to the capturing site. The switch decides the case where NO\n * company carries the domain: on, the capture creates one named after the\n * domain and links the contact; off — the default — it creates nothing and\n * the contact waits for a person to file them. Public mailbox domains never\n * create a company either way; a workspace's consumer list is not a list\n * of accounts.\n *\n * ## Written where it is read\n *\n * One dotted-path `update()` onto `orgs/{orgId}`, so the org's other keys\n * are untouched and the map under `crm` can grow a key per setting. The\n * shell's org listener delivers the new value back, which is what the\n * switch reflects; a local copy holds the click only until then.\n */\nexport function AutoCreateCompaniesCard(props: AutoCreateCompaniesCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n\n const stored = orgAutoCreatesCompanies(org as Record<string, unknown> | undefined)\n const [checked, setChecked] = useState(stored)\n const [busy, setBusy] = useState(false)\n // The stored value wins whenever it changes: the click is optimistic, and\n // the org document is what the capture door will actually read.\n useEffect(() => setChecked(stored), [stored])\n\n const handleChange = async (next: boolean) => {\n // The switch is disabled for a member who may not move it; the guard\n // stands anyway, because a disabled control still delivers a change\n // event in some environments and the rules would refuse the write.\n if (!orgId || !canManage) return\n setChecked(next)\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_AUTO_CREATE_COMPANIES_PATH]: next,\n })\n enqueueSnackbar(\n next\n ? 'Companies will be created from work email domains'\n : 'Companies will no longer be created from email domains',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n setChecked(stored)\n enqueueSnackbar('The setting could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n const ready = scopeReady && roleReady\n return (\n <CardDisplay\n header={'Companies'}\n help={pluginDocsHelp('crmSettings', { anchor: '#companies' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <FormControlLabel\n control={\n <Switch\n size=\"small\"\n checked={checked}\n disabled={!ready || !canManage || busy}\n onChange={(event) => void handleChange(event.target.checked)}\n />\n }\n label=\"Create companies from work email domains\"\n />\n <FormHelperText>\n {'A contact captured with a work email address is linked to the ' +\n 'company whose domain matches it. When this is on and no such ' +\n 'company exists yet, one is created from the domain — acme.com ' +\n 'becomes Acme — and the contact is linked to it. Public mailbox ' +\n 'domains such as gmail.com never create a company.'}\n </FormHelperText>\n {ready && !canManage ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Only a workspace owner or admin can change this.'}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nAutoCreateCompaniesCard.displayName = 'AutoCreateCompaniesCard'\n\n/**\n * \"Default sharing for new records\" (AGL-3662) — what a new contact,\n * company, deal or task starts shared with, set apart from the org's\n * defaults for new datasets and media.\n *\n * Changes nothing that already exists: narrowing an address book from a\n * select would hide records from sites already working them, with no\n * confirmation. Each record's own sharing is where that is decided.\n *\n * Reads `crmDefaultScopeOf`, the same reader the capture door and every CRM\n * creator stamp from, so the control cannot show one thing while records\n * get another — including for an org that has never set it, which inherits\n * the dataset default it shared before the split.\n */\nexport function DefaultRecordSharingCard(props: AutoCreateCompaniesCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n\n const stored =\n crmDefaultScopeOf(org as Record<string, unknown> | undefined) ?? 'host'\n const [value, setValue] = useState<'org' | 'host'>(stored)\n const [busy, setBusy] = useState(false)\n useEffect(() => setValue(stored), [stored])\n\n const handleChange = async (next: 'org' | 'host') => {\n if (!orgId || !canManage) return\n setValue(next)\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_DEFAULT_RECORD_SCOPE_PATH]: next,\n })\n enqueueSnackbar('Default sharing updated', {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n setValue(stored)\n enqueueSnackbar('The setting could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n const ready = scopeReady && roleReady\n return (\n <CardDisplay\n header={'Default sharing for new records'}\n help={pluginDocsHelp('crmSharing', {\n anchor: '#access-read-only-or-read-and-edit',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1} sx={{ maxWidth: 480 }}>\n <TextField\n select\n size=\"small\"\n label=\"New contacts, companies, deals and tasks are shared with\"\n value={value}\n disabled={!ready || !canManage || busy}\n onChange={(event) =>\n void handleChange(event.target.value as 'org' | 'host')\n }\n helperText={\n 'Only affects records created from now on. Datasets and media ' +\n 'have defaults of their own, on the organization Data and ' +\n 'Media pages.'\n }\n >\n <MenuItem value=\"org\">{'All sites'}</MenuItem>\n <MenuItem value=\"host\">\n {'The site they came in on, and its consent group'}\n </MenuItem>\n </TextField>\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n </CardDisplay>\n )\n}\nDefaultRecordSharingCard.displayName = 'DefaultRecordSharingCard'\n\n/** The caption every card shows a member who may read but not change it. */\nfunction ManagersOnlyNote(props: { ready: boolean; canManage: boolean }) {\n return props.ready && !props.canManage ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Only a workspace owner or admin can change this.'}\n </Typography>\n ) : null\n}\nManagersOnlyNote.displayName = 'ManagersOnlyNote'\n\n/** The `value` of the default-owner picker's \"nobody\" entry. */\nconst NO_DEFAULT_OWNER = ''\n\nexport interface AssignmentCardProps {\n /** The site the section is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * \"Default owner\" — who gets the records captured on a site when no\n * assignment rule claims them (AGL-2618).\n *\n * Per site, on the org document: the reader is the org-level assignment\n * pass, which reads one document for every site's default, and the writer\n * is the same owner-or-admin the rest of the `crm` map admits. Under a\n * site the card is that site's one picker; at the ORGANIZATION level\n * (AGL-2630) it is one picker per site in the org, because the map is per\n * site and this is the one place all of it is on screen at once. The\n * field is addressed by `FieldPath` segments rather than a dotted string\n * because a host id is a document id and may contain a dot — joined with\n * dots, the write would land beside the setting rather than in it.\n * Clearing a picker deletes its field, so an org that once set a default\n * and unset it reads exactly like one that never did.\n */\nexport function DefaultOwnerCard(props: AssignmentCardProps) {\n const { hostId, org } = props\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n const roster = useOrgMemberDirectory(orgId)\n const mount = useCrmOrgMount()\n const sites = useMemo<ReadonlyArray<{ id: string; name: string | null }>>(\n () =>\n hostId\n ? [{ id: hostId, name: null }]\n : (mount?.hosts ?? []).map((host) => ({ id: host.id, name: host.name })),\n [hostId, mount?.hosts],\n )\n const ready = scopeReady && roleReady && (Boolean(hostId) || Boolean(mount?.hostsReady))\n return (\n <CardDisplay\n header={'Default owner'}\n help={pluginDocsHelp('crmSettings', { anchor: '#default-owner' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {sites.map((site) => (\n <DefaultOwnerPicker\n key={site.id}\n orgId={orgId}\n siteId={site.id}\n siteName={site.name}\n org={org}\n roster={roster}\n canManage={canManage}\n ready={ready}\n />\n ))}\n {!hostId && ready && sites.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet, so there is nobody to route to.'}\n </Typography>\n ) : null}\n <FormHelperText>\n {'Every contact captured on a site — a form, a sign-up, a booking, ' +\n 'an order — that no assignment rule claims is handed to that ' +\n \"site's default owner, and they are notified. A contact added by \" +\n 'hand or imported with an owner keeps the owner you chose.'}\n </FormHelperText>\n {roster.error ? (\n <Typography variant=\"caption\" color=\"error\">\n {roster.error}\n </Typography>\n ) : null}\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n </CardDisplay>\n )\n}\nDefaultOwnerCard.displayName = 'DefaultOwnerCard'\n\n/** One site's slot in the org-wide default-owner map, as a picker. */\nfunction DefaultOwnerPicker(props: {\n orgId: string | undefined\n siteId: string\n /** How the site reads; `null` under the site itself, where \"this site\" will do. */\n siteName: string | null\n org?: Partial<AglynOrgBilling> | null\n roster: ReturnType<typeof useOrgMemberDirectory>\n canManage: boolean\n ready: boolean\n}) {\n const { orgId, siteId, siteName, org, roster, canManage, ready } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const siteLabel = siteName ?? 'this site'\n\n const stored =\n readCrmAssignmentSettings(org as Record<string, unknown> | undefined)\n .hostDefaultOwners[siteId] ?? NO_DEFAULT_OWNER\n const [value, setValue] = useState(stored)\n const [busy, setBusy] = useState(false)\n useEffect(() => setValue(stored), [stored])\n\n const handleChange = async (next: string) => {\n if (!orgId || !canManage) return\n setValue(next)\n setBusy(true)\n try {\n await updateDoc(\n doc(firestore, 'orgs', orgId),\n new FieldPath(...crmHostDefaultOwnerSegments(siteId)),\n next || deleteField(),\n )\n enqueueSnackbar(\n next\n ? `New contacts on ${siteLabel} go to ${roster.nameOf(next)} unless a rule says otherwise`\n : `New contacts on ${siteLabel} stay unassigned unless a rule says otherwise`,\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n setValue(stored)\n enqueueSnackbar('The default owner could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n // A stored owner the roster no longer lists is still shown, by uid, so\n // the picker never silently reads \"Nobody\" for a setting that is there.\n const options = useMemo(\n () =>\n stored && !roster.members.some((member) => member.uid === stored)\n ? [...roster.members, { uid: stored, label: `${stored} (former member)` }]\n : roster.members,\n [roster.members, stored],\n )\n return (\n <TextField\n select\n size=\"small\"\n label={siteName ? `Default owner · ${siteName}` : 'Default owner for this site'}\n value={value}\n onChange={(event) => void handleChange(event.target.value)}\n disabled={!ready || !canManage || busy || roster.loading}\n // \"Nobody\" is the empty value, which a select would otherwise\n // render as a blank rather than as the choice it is. The label is\n // shrunk with it: a field that always draws a value has no empty\n // state for the label to sit in, and left to itself it decides from\n // the value alone and paints the label over the choice.\n slotProps={{ select: { displayEmpty: true }, inputLabel: { shrink: true } }}\n sx={{ maxWidth: 360 }}\n >\n <MenuItem value={NO_DEFAULT_OWNER}>{'Nobody — leave unassigned'}</MenuItem>\n {options.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n )\n}\nDefaultOwnerPicker.displayName = 'DefaultOwnerPicker'\n\n/**\n * \"Assignment rules\" — the ordered list the capture pass tries first\n * (AGL-2618), with Add rule in a drawer, reorder and delete.\n *\n * ## The whole list is the unit of write\n *\n * The rules are an array on the org document and every edit — a new rule,\n * a move, a delete — writes the whole array by its dotted path. A per-rule\n * write has no address (an array element is not a field), and the list is\n * bounded at fifty, so the write is small. The array the card writes is\n * the array it read off the org prop plus the one edit, so two admins\n * editing at once last-write-wins at the granularity of one edit, which\n * is what a list this short and this rarely edited needs.\n *\n * ## First match wins, so order is the meaning\n *\n * A rule's position is a fact about it — \"bookings go to Kim, and\n * everything else to Sam\" is two rules in that order and nonsense in the\n * other — which is why the reorder controls sit in the first column and\n * the row reads \"1st, 2nd…\" rather than a name. Up and down rather than\n * drag, the way the custom-field list does it: a drag handle needs a\n * pointer, and the reorder is rare.\n */\nexport function AssignmentRulesCard(props: AssignmentCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n const roster = useOrgMemberDirectory(orgId)\n const settings = useMemo(\n () => readCrmAssignmentSettings(org as Record<string, unknown> | undefined),\n [org],\n )\n const { rules } = settings\n const [drawerOpen, setDrawerOpen] = useState(false)\n const [busy, setBusy] = useState(false)\n\n const writeRules = useCallback(\n async (next: CrmAssignmentRule[], said: string) => {\n if (!orgId || !canManage) return\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_ASSIGNMENT_RULES_PATH]: next,\n })\n enqueueSnackbar(said, { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The rules could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n throw error\n } finally {\n setBusy(false)\n }\n },\n [firestore, orgId, canManage, enqueueSnackbar],\n )\n\n const move = (index: number, by: -1 | 1) => {\n const target = index + by\n if (target < 0 || target >= rules.length) return\n const next = [...rules]\n ;[next[index], next[target]] = [next[target], next[index]]\n void writeRules(next, 'Rule moved').catch(() => undefined)\n }\n const remove = (rule: CrmAssignmentRule) => {\n void writeRules(\n rules.filter((entry) => entry.id !== rule.id),\n 'Rule deleted',\n ).catch(() => undefined)\n }\n const handleAdd = async (rule: CrmAssignmentRule) => {\n await writeRules([...rules, rule], 'Rule added')\n setDrawerOpen(false)\n }\n\n const ready = scopeReady && roleReady\n const atCap = rules.length >= CRM_ASSIGNMENT_RULES_MAX\n const canEdit = ready && canManage && !busy\n const ordinal = (index: number) => {\n const n = index + 1\n const suffix =\n n % 100 >= 11 && n % 100 <= 13\n ? 'th'\n : (['th', 'st', 'nd', 'rd'] as const)[n % 10] ?? 'th'\n return `${n}${suffix}`\n }\n return (\n <CardDisplay\n header={'Assignment rules'}\n help={pluginDocsHelp('crmSettings', { anchor: '#assignment-rules' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\n <Button\n variant=\"contained\"\n disabled={!canEdit || atCap}\n onClick={() => setDrawerOpen(true)}\n >\n {'Add rule'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Tried in order for every new contact captured on any site in the ' +\n 'workspace; the first rule whose every condition holds assigns the ' +\n 'owner. A contact no rule claims goes to the capturing site’s ' +\n 'default owner, or stays unassigned.'}\n </Typography>\n {rules.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No rules yet. Add one to route new contacts by where they came from.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell sx={{ width: 120 }}>{'Order'}</TableCell>\n <TableCell>{'When'}</TableCell>\n <TableCell>{'Assign to'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {rules.map((rule, index) => {\n const described = describeAssignmentRule(rule, roster.nameOf)\n return (\n <TableRow key={rule.id} hover>\n <TableCell>\n <Stack direction=\"row\" spacing={0} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\" sx={{ minWidth: 32 }}>\n {ordinal(index)}\n </Typography>\n <IconButton\n size=\"small\"\n disabled={!canEdit || index === 0}\n onClick={() => move(index, -1)}\n >\n <MdiIcon path={mdiArrowUp.path} size={0.7} />\n <SrOnly>{`Move rule ${index + 1} up`}</SrOnly>\n </IconButton>\n <IconButton\n size=\"small\"\n disabled={!canEdit || index === rules.length - 1}\n onClick={() => move(index, 1)}\n >\n <MdiIcon path={mdiArrowDown.path} size={0.7} />\n <SrOnly>{`Move rule ${index + 1} down`}</SrOnly>\n </IconButton>\n </Stack>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\">{described.when}</Typography>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\">{described.assign}</Typography>\n </TableCell>\n <TableCell align=\"right\" sx={{ width: 56 }}>\n <RowActionsMenu\n label={`Rule ${index + 1}`}\n items={[\n {\n key: 'delete',\n label: 'Delete rule',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !canEdit,\n onClick: () => remove(rule),\n },\n ]}\n />\n </TableCell>\n </TableRow>\n )\n })}\n </TableBody>\n </ScrollTable>\n )}\n {atCap ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`A workspace keeps at most ${CRM_ASSIGNMENT_RULES_MAX} rules. Delete one to add another.`}\n </Typography>\n ) : null}\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n <AssignmentRuleDrawer\n open={drawerOpen}\n onClose={() => setDrawerOpen(false)}\n members={roster.members}\n membersLoading={roster.loading}\n poolSize={settings.pool.memberUids.length}\n existingIds={rules.map((rule) => rule.id)}\n onSubmit={handleAdd}\n />\n </CardDisplay>\n )\n}\nAssignmentRulesCard.displayName = 'AssignmentRulesCard'\n\n/**\n * \"Round robin\" — the members handed records in turn (AGL-2618).\n *\n * The pool is the roster with a checkbox per member, and its ORDER is the\n * order members were checked: a member checked later joins the end of the\n * rotation, and unchecking removes them without disturbing the others. The\n * pointer — who got the last record — is the server's to move and is only\n * read here, as \"next up\", so an admin can see where the rotation stands\n * without being able to put a thumb on it.\n */\nexport function RoundRobinCard(props: AssignmentCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n const roster = useOrgMemberDirectory(orgId)\n const { pool } = useMemo(\n () => readCrmAssignmentSettings(org as Record<string, unknown> | undefined),\n [org],\n )\n const [busy, setBusy] = useState(false)\n\n const toggle = async (uid: string, inPool: boolean) => {\n if (!orgId || !canManage) return\n const next = inPool\n ? [...pool.memberUids.filter((entry) => entry !== uid), uid]\n : pool.memberUids.filter((entry) => entry !== uid)\n if (next.length > CRM_ROUND_ROBIN_POOL_MAX) return\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_ROUND_ROBIN_POOL_PATH]: next,\n })\n enqueueSnackbar(\n inPool\n ? `${roster.nameOf(uid)} joined the rotation`\n : `${roster.nameOf(uid)} left the rotation`,\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The pool could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n const ready = scopeReady && roleReady\n const canEdit = ready && canManage && !busy && !roster.loading\n const order = roundRobinOrder(pool.memberUids, pool.lastAssignedUid)\n const nextUp = order[0]\n // The rotation names people the way the checkboxes above do — with the\n // address, since two members can share a display name.\n const rotationName = (uid: string): string => {\n const member = roster.members.find((row) => row.uid === uid)\n return member ? crmMemberPickerLabel(member) : roster.nameOf(uid)\n }\n return (\n <CardDisplay\n header={'Round robin'}\n help={pluginDocsHelp('crmSettings', { anchor: '#round-robin' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The members a round-robin rule, or an automation set to round ' +\n 'robin, hands records to in turn. Each new record goes to the ' +\n 'member after the last one who got a record, wrapping round.'}\n </Typography>\n {roster.loading ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading the team…'}\n </Typography>\n ) : roster.error ? (\n <Typography variant=\"body2\" color=\"error\">\n {roster.error}\n </Typography>\n ) : (\n <FormGroup>\n {roster.members.map((member) => (\n <FormControlLabel\n key={member.uid}\n control={\n <Checkbox\n size=\"small\"\n checked={pool.memberUids.includes(member.uid)}\n disabled={!canEdit}\n onChange={(event) => void toggle(member.uid, event.target.checked)}\n />\n }\n label={crmMemberPickerLabel(member)}\n />\n ))}\n </FormGroup>\n )}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {pool.memberUids.length === 0\n ? 'Nobody is in the rotation; a round-robin rule is skipped until somebody is.'\n : `Rotation: ${pool.memberUids.map(rotationName).join(' → ')}. Next up: ${rotationName(nextUp)}.`}\n </Typography>\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n </CardDisplay>\n )\n}\nRoundRobinCard.displayName = 'RoundRobinCard'\n\n/**\n * `/crm/settings` — what the CRM does on its own, for every site in the\n * workspace (AGL-2613).\n *\n * A stack of cards, one per concern, so a later setting arrives as a card\n * beside this one rather than a field inside it. Every card writes the org\n * document, because a CRM setting is a fact about how the business files\n * people and not about one site; the section is reached from a site's hub\n * or from the organization's (AGL-2630), and writes the same document from\n * either. The one per-site setting — the default owner — is the site's\n * slot in an org-wide map: under a site the card names the site it is for,\n * and at the organization level it lists every site's slot.\n *\n * The Recipes card (AGL-2639) is the one card that mounts at the\n * ORGANIZATION level only. It installs an automation onto a site of the\n * reader's choosing, which is the org's need; under a site the Actions\n * page's own Recipes menu is the door, and a second one here would write\n * the same action by a different route. Last, because the welcome recipe\n * leans on the round-robin pool set up in the card above it.\n *\n * The Email templates card (AGL-2658) is the one card every CRM editor may\n * write to — a template is working material, not policy — and mounts at\n * both levels, listing the site's letters under a site and the whole\n * workspace's from the organization's hub.\n *\n * Your sending addresses (AGL-2975) sits directly under Email capture,\n * because it is where a member who copies that address from an alias looks\n * next: it is the reader's OWN list, at both levels, and writes through the\n * core route rather than the org document.\n */\nexport function CrmSettingsSection(props: CrmSettingsSectionProps) {\n const { hostId, org } = props\n const mount = useCrmOrgMount()\n // The Email capture card (AGL-2657) rotates on the same owner-or-admin\n // bar every other card writes on; the scope and the role are resolved\n // here once and handed down, since the card itself writes no document.\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n return (\n <Stack spacing={3}>\n <DefaultRecordSharingCard hostId={hostId} org={org} />\n <AutoCreateCompaniesCard hostId={hostId} org={org} />\n <DefaultOwnerCard hostId={hostId} org={org} />\n <AssignmentRulesCard hostId={hostId} org={org} />\n <RoundRobinCard hostId={hostId} org={org} />\n <EmailTemplatesCard hostId={hostId} org={org} />\n <EmailCaptureCard hostId={hostId} canManage={canManage} ready={scopeReady && roleReady} />\n <SendingAddressesCard orgId={orgId} ready={scopeReady} />\n {/* Sharing rules (AGL-3336) are the organization's: mounted where its\n sites are listed, at the org level only — before Recipes, which\n stays last. */}\n {mount ? (\n <SharingRulesCard\n orgId={orgId ?? null}\n org={org}\n canManage={canManage}\n ready={scopeReady && roleReady}\n />\n ) : null}\n {mount ? <RecipesCard org={org} /> : null}\n </Stack>\n )\n}\nCrmSettingsSection.displayName = 'CrmSettingsSection'\n\nexport default CrmSettingsSection\n"],"names":["canManageOrg","CRM_ASSIGNMENT_RULES_MAX","CRM_ASSIGNMENT_RULES_PATH","CRM_AUTO_CREATE_COMPANIES_PATH","CRM_DEFAULT_RECORD_SCOPE_PATH","CRM_ROUND_ROBIN_POOL_MAX","CRM_ROUND_ROBIN_POOL_PATH","crmDefaultScopeOf","crmHostDefaultOwnerSegments","describeAssignmentRule","orgAutoCreatesCompanies","pluginDocsHelp","readCrmAssignmentSettings","roundRobinOrder","crmMemberPickerLabel","mdiArrowDown","mdiArrowUp","mdiDeleteOutline","CardDisplay","MdiIcon","SrOnly","RowActionsMenu","ScrollTable","useSnackbar","useFirestore","useFirestoreDoc","useUser","Button","Checkbox","FormControlLabel","FormGroup","FormHelperText","IconButton","MenuItem","Stack","Switch","TableBody","TableCell","TableHead","TableRow","TextField","Typography","deleteField","doc","FieldPath","updateDoc","useCallback","useEffect","useMemo","useState","useCrmOrgMount","useCrmScope","useOrgMemberDirectory","AssignmentRuleDrawer","EmailTemplatesCard","EmailCaptureCard","RecipesCard","SendingAddressesCard","SharingRulesCard","useCanManageCrmSettings","orgId","firestore","data","user","uid","member","status","canManage","role","ready","Boolean","AutoCreateCompaniesCard","props","hostId","org","enqueueSnackbar","scopeReady","roleReady","stored","checked","setChecked","busy","setBusy","handleChange","next","variant","persist","error","console","allowDuplicate","header","help","anchor","contentGutterX","contentGutterY","spacing","control","size","disabled","onChange","event","target","label","color","displayName","DefaultRecordSharingCard","value","setValue","sx","maxWidth","select","helperText","ManagersOnlyNote","NO_DEFAULT_OWNER","DefaultOwnerCard","roster","mount","sites","id","name","hosts","map","host","hostsReady","site","DefaultOwnerPicker","siteId","siteName","length","siteLabel","hostDefaultOwners","nameOf","options","members","some","loading","slotProps","displayEmpty","inputLabel","shrink","AssignmentRulesCard","settings","rules","drawerOpen","setDrawerOpen","writeRules","said","move","index","by","catch","undefined","remove","rule","filter","entry","handleAdd","atCap","canEdit","ordinal","n","suffix","contentBordered","HeaderProps","action","onClick","width","align","described","hover","direction","alignItems","minWidth","path","when","assign","items","key","icon","destructive","open","onClose","membersLoading","poolSize","pool","memberUids","existingIds","onSubmit","RoundRobinCard","toggle","inPool","order","lastAssignedUid","nextUp","rotationName","find","row","includes","join","CrmSettingsSection"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/settings-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 type AglynOrgBilling,\n canManageOrg,\n type ConsolePluginPageProps,\n CRM_ASSIGNMENT_RULES_MAX,\n CRM_ASSIGNMENT_RULES_PATH,\n CRM_AUTO_CREATE_COMPANIES_PATH,\n CRM_DEFAULT_RECORD_SCOPE_PATH,\n CRM_ROUND_ROBIN_POOL_MAX,\n CRM_ROUND_ROBIN_POOL_PATH,\n type CrmAssignmentRule,\n crmDefaultScopeOf,\n crmHostDefaultOwnerSegments,\n describeAssignmentRule,\n orgAutoCreatesCompanies,\n type OrgRole,\n pluginDocsHelp,\n readCrmAssignmentSettings,\n roundRobinOrder,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { mdiArrowDown, mdiArrowUp, mdiDeleteOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu 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 useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Checkbox,\n FormControlLabel,\n FormGroup,\n FormHelperText,\n IconButton,\n MenuItem,\n Stack,\n Switch,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport { deleteField, doc, FieldPath, updateDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport AssignmentRuleDrawer from './assignment-rule-drawer'\nimport EmailTemplatesCard from './email-templates-card'\nimport EmailCaptureCard from './email-capture-card'\nimport RecipesCard from './recipes-card'\nimport SendingAddressesCard from './sending-addresses-card'\nimport SharingRulesCard from './sharing-rules-card'\n\nexport type CrmSettingsSectionProps = Pick<ConsolePluginPageProps, 'hostId' | 'org'>\n\n/**\n * Whether the signed-in member may change an org-wide CRM setting, and\n * whether that is known yet.\n *\n * The org document's client branch admits an OWNER or ADMIN and nobody\n * else — `canManageOrg()` in the rules — so the question is the caller's\n * org role, read off their own membership document, which the rules let a\n * member read for themselves. A scoped collaborator, an editor, a viewer:\n * each sees the switch and cannot move it, with the reason beside it,\n * rather than a switch that moves and snaps back with a bare\n * `permission-denied`. `ready` separates \"no\" from \"not yet\", so the\n * control disables with a reason instead of hiding until the read lands.\n */\nexport function useCanManageCrmSettings(orgId: string | undefined): {\n canManage: boolean\n ready: boolean\n} {\n const firestore = useFirestore()\n const { data: user } = useUser()\n const uid = user?.uid ?? ''\n const { data: member, status } = useFirestoreDoc<{ role?: OrgRole }>(\n () => (orgId && uid ? doc(firestore, 'orgs', orgId, 'members', uid) : null),\n [firestore, orgId, uid],\n )\n return {\n canManage: canManageOrg(member?.role ?? null),\n ready: Boolean(orgId && uid) && status !== 'loading',\n }\n}\n\nexport interface AutoCreateCompaniesCardProps {\n /** The site the section is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * \"Create companies from work email domains\" — the org's one switch over\n * what a capture does with a company nobody has filed yet (AGL-2613).\n *\n * ## What the switch decides, and what it does not\n *\n * A contact captured from `jane@acme.com` is linked to the company at\n * `acme.com` whether this is on or off, provided exactly one such company\n * is visible to the capturing site. The switch decides the case where NO\n * company carries the domain: on, the capture creates one named after the\n * domain and links the contact; off — the default — it creates nothing and\n * the contact waits for a person to file them. Public mailbox domains never\n * create a company either way; a workspace's consumer list is not a list\n * of accounts.\n *\n * ## Written where it is read\n *\n * One dotted-path `update()` onto `orgs/{orgId}`, so the org's other keys\n * are untouched and the map under `crm` can grow a key per setting. The\n * shell's org listener delivers the new value back, which is what the\n * switch reflects; a local copy holds the click only until then.\n */\nexport function AutoCreateCompaniesCard(props: AutoCreateCompaniesCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n\n const stored = orgAutoCreatesCompanies(org as Record<string, unknown> | undefined)\n const [checked, setChecked] = useState(stored)\n const [busy, setBusy] = useState(false)\n // The stored value wins whenever it changes: the click is optimistic, and\n // the org document is what the capture door will actually read.\n useEffect(() => setChecked(stored), [stored])\n\n const handleChange = async (next: boolean) => {\n // The switch is disabled for a member who may not move it; the guard\n // stands anyway, because a disabled control still delivers a change\n // event in some environments and the rules would refuse the write.\n if (!orgId || !canManage) return\n setChecked(next)\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_AUTO_CREATE_COMPANIES_PATH]: next,\n })\n enqueueSnackbar(\n next\n ? 'Companies will be created from work email domains'\n : 'Companies will no longer be created from email domains',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n setChecked(stored)\n enqueueSnackbar('The setting could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n const ready = scopeReady && roleReady\n return (\n <CardDisplay\n header={'Companies'}\n help={pluginDocsHelp('crmSettings', { anchor: '#companies' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <FormControlLabel\n control={\n <Switch\n size=\"small\"\n checked={checked}\n disabled={!ready || !canManage || busy}\n onChange={(event) => void handleChange(event.target.checked)}\n />\n }\n label=\"Create companies from work email domains\"\n />\n <FormHelperText>\n {'A contact captured with a work email address is linked to the ' +\n 'company whose domain matches it. When this is on and no such ' +\n 'company exists yet, one is created from the domain — acme.com ' +\n 'becomes Acme — and the contact is linked to it. Public mailbox ' +\n 'domains such as gmail.com never create a company.'}\n </FormHelperText>\n {ready && !canManage ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Only a workspace owner or admin can change this.'}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nAutoCreateCompaniesCard.displayName = 'AutoCreateCompaniesCard'\n\n/**\n * \"Default sharing for new records\" (AGL-3662) — what a new contact,\n * company, deal or task starts shared with, set apart from the org's\n * defaults for new datasets and media.\n *\n * Changes nothing that already exists: narrowing an address book from a\n * select would hide records from sites already working them, with no\n * confirmation. Each record's own sharing is where that is decided.\n *\n * Reads `crmDefaultScopeOf`, the same reader the capture door and every CRM\n * creator stamp from, so the control cannot show one thing while records\n * get another — including for an org that has never set it, which shares\n * with every site unless it set the older dataset default to keep them\n * apart. Keeping each site's records apart is the agency's choice: one\n * client's collaborator then sees only that client's people.\n */\nexport function DefaultRecordSharingCard(props: AutoCreateCompaniesCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n\n const stored =\n crmDefaultScopeOf(org as Record<string, unknown> | undefined) ?? 'org'\n const [value, setValue] = useState<'org' | 'host'>(stored)\n const [busy, setBusy] = useState(false)\n useEffect(() => setValue(stored), [stored])\n\n const handleChange = async (next: 'org' | 'host') => {\n if (!orgId || !canManage) return\n setValue(next)\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_DEFAULT_RECORD_SCOPE_PATH]: next,\n })\n enqueueSnackbar('Default sharing updated', {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n setValue(stored)\n enqueueSnackbar('The setting could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n const ready = scopeReady && roleReady\n return (\n <CardDisplay\n header={'Default sharing for new records'}\n help={pluginDocsHelp('crmSharing', {\n anchor: '#access-read-only-or-read-and-edit',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1} sx={{ maxWidth: 480 }}>\n <TextField\n select\n size=\"small\"\n label=\"New contacts, companies, deals and tasks are shared with\"\n value={value}\n disabled={!ready || !canManage || busy}\n onChange={(event) =>\n void handleChange(event.target.value as 'org' | 'host')\n }\n helperText={\n 'Only affects records created from now on. Datasets and media ' +\n 'have defaults of their own, on the organization Data and ' +\n 'Media pages.'\n }\n >\n <MenuItem value=\"org\">{'All sites in the organization'}</MenuItem>\n <MenuItem value=\"host\">\n {'Only the site they came in on (keeps clients apart, for agencies)'}\n </MenuItem>\n </TextField>\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n </CardDisplay>\n )\n}\nDefaultRecordSharingCard.displayName = 'DefaultRecordSharingCard'\n\n/** The caption every card shows a member who may read but not change it. */\nfunction ManagersOnlyNote(props: { ready: boolean; canManage: boolean }) {\n return props.ready && !props.canManage ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Only a workspace owner or admin can change this.'}\n </Typography>\n ) : null\n}\nManagersOnlyNote.displayName = 'ManagersOnlyNote'\n\n/** The `value` of the default-owner picker's \"nobody\" entry. */\nconst NO_DEFAULT_OWNER = ''\n\nexport interface AssignmentCardProps {\n /** The site the section is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * \"Default owner\" — who gets the records captured on a site when no\n * assignment rule claims them (AGL-2618).\n *\n * Per site, on the org document: the reader is the org-level assignment\n * pass, which reads one document for every site's default, and the writer\n * is the same owner-or-admin the rest of the `crm` map admits. Under a\n * site the card is that site's one picker; at the ORGANIZATION level\n * (AGL-2630) it is one picker per site in the org, because the map is per\n * site and this is the one place all of it is on screen at once. The\n * field is addressed by `FieldPath` segments rather than a dotted string\n * because a host id is a document id and may contain a dot — joined with\n * dots, the write would land beside the setting rather than in it.\n * Clearing a picker deletes its field, so an org that once set a default\n * and unset it reads exactly like one that never did.\n */\nexport function DefaultOwnerCard(props: AssignmentCardProps) {\n const { hostId, org } = props\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n const roster = useOrgMemberDirectory(orgId)\n const mount = useCrmOrgMount()\n const sites = useMemo<ReadonlyArray<{ id: string; name: string | null }>>(\n () =>\n hostId\n ? [{ id: hostId, name: null }]\n : (mount?.hosts ?? []).map((host) => ({ id: host.id, name: host.name })),\n [hostId, mount?.hosts],\n )\n const ready = scopeReady && roleReady && (Boolean(hostId) || Boolean(mount?.hostsReady))\n return (\n <CardDisplay\n header={'Default owner'}\n help={pluginDocsHelp('crmSettings', { anchor: '#default-owner' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {sites.map((site) => (\n <DefaultOwnerPicker\n key={site.id}\n orgId={orgId}\n siteId={site.id}\n siteName={site.name}\n org={org}\n roster={roster}\n canManage={canManage}\n ready={ready}\n />\n ))}\n {!hostId && ready && sites.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet, so there is nobody to route to.'}\n </Typography>\n ) : null}\n <FormHelperText>\n {'Every contact captured on a site — a form, a sign-up, a booking, ' +\n 'an order — that no assignment rule claims is handed to that ' +\n \"site's default owner, and they are notified. A contact added by \" +\n 'hand or imported with an owner keeps the owner you chose.'}\n </FormHelperText>\n {roster.error ? (\n <Typography variant=\"caption\" color=\"error\">\n {roster.error}\n </Typography>\n ) : null}\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n </CardDisplay>\n )\n}\nDefaultOwnerCard.displayName = 'DefaultOwnerCard'\n\n/** One site's slot in the org-wide default-owner map, as a picker. */\nfunction DefaultOwnerPicker(props: {\n orgId: string | undefined\n siteId: string\n /** How the site reads; `null` under the site itself, where \"this site\" will do. */\n siteName: string | null\n org?: Partial<AglynOrgBilling> | null\n roster: ReturnType<typeof useOrgMemberDirectory>\n canManage: boolean\n ready: boolean\n}) {\n const { orgId, siteId, siteName, org, roster, canManage, ready } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const siteLabel = siteName ?? 'this site'\n\n const stored =\n readCrmAssignmentSettings(org as Record<string, unknown> | undefined)\n .hostDefaultOwners[siteId] ?? NO_DEFAULT_OWNER\n const [value, setValue] = useState(stored)\n const [busy, setBusy] = useState(false)\n useEffect(() => setValue(stored), [stored])\n\n const handleChange = async (next: string) => {\n if (!orgId || !canManage) return\n setValue(next)\n setBusy(true)\n try {\n await updateDoc(\n doc(firestore, 'orgs', orgId),\n new FieldPath(...crmHostDefaultOwnerSegments(siteId)),\n next || deleteField(),\n )\n enqueueSnackbar(\n next\n ? `New contacts on ${siteLabel} go to ${roster.nameOf(next)} unless a rule says otherwise`\n : `New contacts on ${siteLabel} stay unassigned unless a rule says otherwise`,\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n setValue(stored)\n enqueueSnackbar('The default owner could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n // A stored owner the roster no longer lists is still shown, by uid, so\n // the picker never silently reads \"Nobody\" for a setting that is there.\n const options = useMemo(\n () =>\n stored && !roster.members.some((member) => member.uid === stored)\n ? [...roster.members, { uid: stored, label: `${stored} (former member)` }]\n : roster.members,\n [roster.members, stored],\n )\n return (\n <TextField\n select\n size=\"small\"\n label={siteName ? `Default owner · ${siteName}` : 'Default owner for this site'}\n value={value}\n onChange={(event) => void handleChange(event.target.value)}\n disabled={!ready || !canManage || busy || roster.loading}\n // \"Nobody\" is the empty value, which a select would otherwise\n // render as a blank rather than as the choice it is. The label is\n // shrunk with it: a field that always draws a value has no empty\n // state for the label to sit in, and left to itself it decides from\n // the value alone and paints the label over the choice.\n slotProps={{ select: { displayEmpty: true }, inputLabel: { shrink: true } }}\n sx={{ maxWidth: 360 }}\n >\n <MenuItem value={NO_DEFAULT_OWNER}>{'Nobody — leave unassigned'}</MenuItem>\n {options.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n )\n}\nDefaultOwnerPicker.displayName = 'DefaultOwnerPicker'\n\n/**\n * \"Assignment rules\" — the ordered list the capture pass tries first\n * (AGL-2618), with Add rule in a drawer, reorder and delete.\n *\n * ## The whole list is the unit of write\n *\n * The rules are an array on the org document and every edit — a new rule,\n * a move, a delete — writes the whole array by its dotted path. A per-rule\n * write has no address (an array element is not a field), and the list is\n * bounded at fifty, so the write is small. The array the card writes is\n * the array it read off the org prop plus the one edit, so two admins\n * editing at once last-write-wins at the granularity of one edit, which\n * is what a list this short and this rarely edited needs.\n *\n * ## First match wins, so order is the meaning\n *\n * A rule's position is a fact about it — \"bookings go to Kim, and\n * everything else to Sam\" is two rules in that order and nonsense in the\n * other — which is why the reorder controls sit in the first column and\n * the row reads \"1st, 2nd…\" rather than a name. Up and down rather than\n * drag, the way the custom-field list does it: a drag handle needs a\n * pointer, and the reorder is rare.\n */\nexport function AssignmentRulesCard(props: AssignmentCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n const roster = useOrgMemberDirectory(orgId)\n const settings = useMemo(\n () => readCrmAssignmentSettings(org as Record<string, unknown> | undefined),\n [org],\n )\n const { rules } = settings\n const [drawerOpen, setDrawerOpen] = useState(false)\n const [busy, setBusy] = useState(false)\n\n const writeRules = useCallback(\n async (next: CrmAssignmentRule[], said: string) => {\n if (!orgId || !canManage) return\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_ASSIGNMENT_RULES_PATH]: next,\n })\n enqueueSnackbar(said, { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The rules could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n throw error\n } finally {\n setBusy(false)\n }\n },\n [firestore, orgId, canManage, enqueueSnackbar],\n )\n\n const move = (index: number, by: -1 | 1) => {\n const target = index + by\n if (target < 0 || target >= rules.length) return\n const next = [...rules]\n ;[next[index], next[target]] = [next[target], next[index]]\n void writeRules(next, 'Rule moved').catch(() => undefined)\n }\n const remove = (rule: CrmAssignmentRule) => {\n void writeRules(\n rules.filter((entry) => entry.id !== rule.id),\n 'Rule deleted',\n ).catch(() => undefined)\n }\n const handleAdd = async (rule: CrmAssignmentRule) => {\n await writeRules([...rules, rule], 'Rule added')\n setDrawerOpen(false)\n }\n\n const ready = scopeReady && roleReady\n const atCap = rules.length >= CRM_ASSIGNMENT_RULES_MAX\n const canEdit = ready && canManage && !busy\n const ordinal = (index: number) => {\n const n = index + 1\n const suffix =\n n % 100 >= 11 && n % 100 <= 13\n ? 'th'\n : (['th', 'st', 'nd', 'rd'] as const)[n % 10] ?? 'th'\n return `${n}${suffix}`\n }\n return (\n <CardDisplay\n header={'Assignment rules'}\n help={pluginDocsHelp('crmSettings', { anchor: '#assignment-rules' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\n <Button\n variant=\"contained\"\n disabled={!canEdit || atCap}\n onClick={() => setDrawerOpen(true)}\n >\n {'Add rule'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Tried in order for every new contact captured on any site in the ' +\n 'workspace; the first rule whose every condition holds assigns the ' +\n 'owner. A contact no rule claims goes to the capturing site’s ' +\n 'default owner, or stays unassigned.'}\n </Typography>\n {rules.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No rules yet. Add one to route new contacts by where they came from.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell sx={{ width: 120 }}>{'Order'}</TableCell>\n <TableCell>{'When'}</TableCell>\n <TableCell>{'Assign to'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {rules.map((rule, index) => {\n const described = describeAssignmentRule(rule, roster.nameOf)\n return (\n <TableRow key={rule.id} hover>\n <TableCell>\n <Stack direction=\"row\" spacing={0} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\" sx={{ minWidth: 32 }}>\n {ordinal(index)}\n </Typography>\n <IconButton\n size=\"small\"\n disabled={!canEdit || index === 0}\n onClick={() => move(index, -1)}\n >\n <MdiIcon path={mdiArrowUp.path} size={0.7} />\n <SrOnly>{`Move rule ${index + 1} up`}</SrOnly>\n </IconButton>\n <IconButton\n size=\"small\"\n disabled={!canEdit || index === rules.length - 1}\n onClick={() => move(index, 1)}\n >\n <MdiIcon path={mdiArrowDown.path} size={0.7} />\n <SrOnly>{`Move rule ${index + 1} down`}</SrOnly>\n </IconButton>\n </Stack>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\">{described.when}</Typography>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\">{described.assign}</Typography>\n </TableCell>\n <TableCell align=\"right\" sx={{ width: 56 }}>\n <RowActionsMenu\n label={`Rule ${index + 1}`}\n items={[\n {\n key: 'delete',\n label: 'Delete rule',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !canEdit,\n onClick: () => remove(rule),\n },\n ]}\n />\n </TableCell>\n </TableRow>\n )\n })}\n </TableBody>\n </ScrollTable>\n )}\n {atCap ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`A workspace keeps at most ${CRM_ASSIGNMENT_RULES_MAX} rules. Delete one to add another.`}\n </Typography>\n ) : null}\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n <AssignmentRuleDrawer\n open={drawerOpen}\n onClose={() => setDrawerOpen(false)}\n members={roster.members}\n membersLoading={roster.loading}\n poolSize={settings.pool.memberUids.length}\n existingIds={rules.map((rule) => rule.id)}\n onSubmit={handleAdd}\n />\n </CardDisplay>\n )\n}\nAssignmentRulesCard.displayName = 'AssignmentRulesCard'\n\n/**\n * \"Round robin\" — the members handed records in turn (AGL-2618).\n *\n * The pool is the roster with a checkbox per member, and its ORDER is the\n * order members were checked: a member checked later joins the end of the\n * rotation, and unchecking removes them without disturbing the others. The\n * pointer — who got the last record — is the server's to move and is only\n * read here, as \"next up\", so an admin can see where the rotation stands\n * without being able to put a thumb on it.\n */\nexport function RoundRobinCard(props: AssignmentCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n const roster = useOrgMemberDirectory(orgId)\n const { pool } = useMemo(\n () => readCrmAssignmentSettings(org as Record<string, unknown> | undefined),\n [org],\n )\n const [busy, setBusy] = useState(false)\n\n const toggle = async (uid: string, inPool: boolean) => {\n if (!orgId || !canManage) return\n const next = inPool\n ? [...pool.memberUids.filter((entry) => entry !== uid), uid]\n : pool.memberUids.filter((entry) => entry !== uid)\n if (next.length > CRM_ROUND_ROBIN_POOL_MAX) return\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_ROUND_ROBIN_POOL_PATH]: next,\n })\n enqueueSnackbar(\n inPool\n ? `${roster.nameOf(uid)} joined the rotation`\n : `${roster.nameOf(uid)} left the rotation`,\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The pool could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n const ready = scopeReady && roleReady\n const canEdit = ready && canManage && !busy && !roster.loading\n const order = roundRobinOrder(pool.memberUids, pool.lastAssignedUid)\n const nextUp = order[0]\n // The rotation names people the way the checkboxes above do — with the\n // address, since two members can share a display name.\n const rotationName = (uid: string): string => {\n const member = roster.members.find((row) => row.uid === uid)\n return member ? crmMemberPickerLabel(member) : roster.nameOf(uid)\n }\n return (\n <CardDisplay\n header={'Round robin'}\n help={pluginDocsHelp('crmSettings', { anchor: '#round-robin' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The members a round-robin rule, or an automation set to round ' +\n 'robin, hands records to in turn. Each new record goes to the ' +\n 'member after the last one who got a record, wrapping round.'}\n </Typography>\n {roster.loading ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading the team…'}\n </Typography>\n ) : roster.error ? (\n <Typography variant=\"body2\" color=\"error\">\n {roster.error}\n </Typography>\n ) : (\n <FormGroup>\n {roster.members.map((member) => (\n <FormControlLabel\n key={member.uid}\n control={\n <Checkbox\n size=\"small\"\n checked={pool.memberUids.includes(member.uid)}\n disabled={!canEdit}\n onChange={(event) => void toggle(member.uid, event.target.checked)}\n />\n }\n label={crmMemberPickerLabel(member)}\n />\n ))}\n </FormGroup>\n )}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {pool.memberUids.length === 0\n ? 'Nobody is in the rotation; a round-robin rule is skipped until somebody is.'\n : `Rotation: ${pool.memberUids.map(rotationName).join(' → ')}. Next up: ${rotationName(nextUp)}.`}\n </Typography>\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n </CardDisplay>\n )\n}\nRoundRobinCard.displayName = 'RoundRobinCard'\n\n/**\n * `/crm/settings` — what the CRM does on its own, for every site in the\n * workspace (AGL-2613).\n *\n * A stack of cards, one per concern, so a later setting arrives as a card\n * beside this one rather than a field inside it. Every card writes the org\n * document, because a CRM setting is a fact about how the business files\n * people and not about one site; the section is reached from a site's hub\n * or from the organization's (AGL-2630), and writes the same document from\n * either. The one per-site setting — the default owner — is the site's\n * slot in an org-wide map: under a site the card names the site it is for,\n * and at the organization level it lists every site's slot.\n *\n * The Recipes card (AGL-2639) is the one card that mounts at the\n * ORGANIZATION level only. It installs an automation onto a site of the\n * reader's choosing, which is the org's need; under a site the Actions\n * page's own Recipes menu is the door, and a second one here would write\n * the same action by a different route. Last, because the welcome recipe\n * leans on the round-robin pool set up in the card above it.\n *\n * The Email templates card (AGL-2658) is the one card every CRM editor may\n * write to — a template is working material, not policy — and mounts at\n * both levels, listing the site's letters under a site and the whole\n * workspace's from the organization's hub.\n *\n * Your sending addresses (AGL-2975) sits directly under Email capture,\n * because it is where a member who copies that address from an alias looks\n * next: it is the reader's OWN list, at both levels, and writes through the\n * core route rather than the org document.\n */\nexport function CrmSettingsSection(props: CrmSettingsSectionProps) {\n const { hostId, org } = props\n const mount = useCrmOrgMount()\n // The Email capture card (AGL-2657) rotates on the same owner-or-admin\n // bar every other card writes on; the scope and the role are resolved\n // here once and handed down, since the card itself writes no document.\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n return (\n <Stack spacing={3}>\n <DefaultRecordSharingCard hostId={hostId} org={org} />\n <AutoCreateCompaniesCard hostId={hostId} org={org} />\n <DefaultOwnerCard hostId={hostId} org={org} />\n <AssignmentRulesCard hostId={hostId} org={org} />\n <RoundRobinCard hostId={hostId} org={org} />\n <EmailTemplatesCard hostId={hostId} org={org} />\n <EmailCaptureCard hostId={hostId} canManage={canManage} ready={scopeReady && roleReady} />\n <SendingAddressesCard orgId={orgId} ready={scopeReady} />\n {/* Sharing rules (AGL-3336) are the organization's: mounted where its\n sites are listed, at the org level only — before Recipes, which\n stays last. */}\n {mount ? (\n <SharingRulesCard\n orgId={orgId ?? null}\n org={org}\n canManage={canManage}\n ready={scopeReady && roleReady}\n />\n ) : null}\n {mount ? <RecipesCard org={org} /> : null}\n </Stack>\n )\n}\nCrmSettingsSection.displayName = 'CrmSettingsSection'\n\nexport default CrmSettingsSection\n"],"names":["canManageOrg","CRM_ASSIGNMENT_RULES_MAX","CRM_ASSIGNMENT_RULES_PATH","CRM_AUTO_CREATE_COMPANIES_PATH","CRM_DEFAULT_RECORD_SCOPE_PATH","CRM_ROUND_ROBIN_POOL_MAX","CRM_ROUND_ROBIN_POOL_PATH","crmDefaultScopeOf","crmHostDefaultOwnerSegments","describeAssignmentRule","orgAutoCreatesCompanies","pluginDocsHelp","readCrmAssignmentSettings","roundRobinOrder","crmMemberPickerLabel","mdiArrowDown","mdiArrowUp","mdiDeleteOutline","CardDisplay","MdiIcon","SrOnly","RowActionsMenu","ScrollTable","useSnackbar","useFirestore","useFirestoreDoc","useUser","Button","Checkbox","FormControlLabel","FormGroup","FormHelperText","IconButton","MenuItem","Stack","Switch","TableBody","TableCell","TableHead","TableRow","TextField","Typography","deleteField","doc","FieldPath","updateDoc","useCallback","useEffect","useMemo","useState","useCrmOrgMount","useCrmScope","useOrgMemberDirectory","AssignmentRuleDrawer","EmailTemplatesCard","EmailCaptureCard","RecipesCard","SendingAddressesCard","SharingRulesCard","useCanManageCrmSettings","orgId","firestore","data","user","uid","member","status","canManage","role","ready","Boolean","AutoCreateCompaniesCard","props","hostId","org","enqueueSnackbar","scopeReady","roleReady","stored","checked","setChecked","busy","setBusy","handleChange","next","variant","persist","error","console","allowDuplicate","header","help","anchor","contentGutterX","contentGutterY","spacing","control","size","disabled","onChange","event","target","label","color","displayName","DefaultRecordSharingCard","value","setValue","sx","maxWidth","select","helperText","ManagersOnlyNote","NO_DEFAULT_OWNER","DefaultOwnerCard","roster","mount","sites","id","name","hosts","map","host","hostsReady","site","DefaultOwnerPicker","siteId","siteName","length","siteLabel","hostDefaultOwners","nameOf","options","members","some","loading","slotProps","displayEmpty","inputLabel","shrink","AssignmentRulesCard","settings","rules","drawerOpen","setDrawerOpen","writeRules","said","move","index","by","catch","undefined","remove","rule","filter","entry","handleAdd","atCap","canEdit","ordinal","n","suffix","contentBordered","HeaderProps","action","onClick","width","align","described","hover","direction","alignItems","minWidth","path","when","assign","items","key","icon","destructive","open","onClose","membersLoading","poolSize","pool","memberUids","existingIds","onSubmit","RoundRobinCard","toggle","inPool","order","lastAssignedUid","nextUp","rotationName","find","row","includes","join","CrmSettingsSection"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { type Firestore } from 'firebase/firestore';
2
+ import type { CrmActivityRecordReads } from '../model/crm-activity-link';
3
+ /**
4
+ * The client's reads for the activity rollup (AGL-4123), `orgs/{orgId}/…` by
5
+ * id. A record the rules refuse this reader rejects here, and the rollup
6
+ * treats that as a record that names no company.
7
+ */
8
+ export declare function crmActivityClientReads(firestore: Firestore, orgId: string): CrmActivityRecordReads;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { CRM_COLLECTIONS } from "@aglyn/aglyn";
18
+ import { doc, getDoc } from "firebase/firestore";
19
+ /**
20
+ * How long a save waits on one record. A durable console tab reads through
21
+ * whichever tab holds the lease, and a hidden tab wakes once a minute, so an
22
+ * unbounded read could hold the **Log** button that long (AGL-3806) for what
23
+ * is bookkeeping beside the entry.
24
+ */ const READ_DEADLINE_MS = 4000;
25
+ function withinDeadline(read) {
26
+ let timer;
27
+ const deadline = new Promise((resolve)=>{
28
+ timer = setTimeout(()=>resolve(null), READ_DEADLINE_MS);
29
+ });
30
+ return Promise.race([
31
+ read,
32
+ deadline
33
+ ]).finally(()=>clearTimeout(timer));
34
+ }
35
+ /**
36
+ * The client's reads for the activity rollup (AGL-4123), `orgs/{orgId}/…` by
37
+ * id. A record the rules refuse this reader rejects here, and the rollup
38
+ * treats that as a record that names no company.
39
+ */ export function crmActivityClientReads(firestore, orgId) {
40
+ const read = async (collection, id)=>{
41
+ const snapshot = await withinDeadline(getDoc(doc(firestore, 'orgs', orgId, collection, id)));
42
+ return (snapshot == null ? void 0 : snapshot.exists()) ? snapshot.data() : null;
43
+ };
44
+ return {
45
+ deal: (id)=>read(CRM_COLLECTIONS.deals, id),
46
+ contact: (id)=>read('contacts', id)
47
+ };
48
+ }
49
+
50
+ //# sourceMappingURL=crm-activity-client-reads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/crm-activity-client-reads.ts"],"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 { CRM_COLLECTIONS } from '@aglyn/aglyn'\nimport { doc, type Firestore, getDoc } from 'firebase/firestore'\nimport type { CrmActivityRecordReads } from '../model/crm-activity-link'\n\n/**\n * How long a save waits on one record. A durable console tab reads through\n * whichever tab holds the lease, and a hidden tab wakes once a minute, so an\n * unbounded read could hold the **Log** button that long (AGL-3806) for what\n * is bookkeeping beside the entry.\n */\nconst READ_DEADLINE_MS = 4_000\n\nfunction withinDeadline<T>(read: Promise<T>): Promise<T | null> {\n let timer: ReturnType<typeof setTimeout> | undefined\n const deadline = new Promise<null>((resolve) => {\n timer = setTimeout(() => resolve(null), READ_DEADLINE_MS)\n })\n return Promise.race([read, deadline]).finally(() => clearTimeout(timer))\n}\n\n/**\n * The client's reads for the activity rollup (AGL-4123), `orgs/{orgId}/…` by\n * id. A record the rules refuse this reader rejects here, and the rollup\n * treats that as a record that names no company.\n */\nexport function crmActivityClientReads(\n firestore: Firestore,\n orgId: string,\n): CrmActivityRecordReads {\n const read = async (collection: string, id: string) => {\n const snapshot = await withinDeadline(getDoc(doc(firestore, 'orgs', orgId, collection, id)))\n return snapshot?.exists() ? (snapshot.data() as Record<string, unknown>) : null\n }\n return {\n deal: (id) => read(CRM_COLLECTIONS.deals, id),\n contact: (id) => read('contacts', id),\n }\n}\n"],"names":["CRM_COLLECTIONS","doc","getDoc","READ_DEADLINE_MS","withinDeadline","read","timer","deadline","Promise","resolve","setTimeout","race","finally","clearTimeout","crmActivityClientReads","firestore","orgId","collection","id","snapshot","exists","data","deal","deals","contact"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,eAAe,QAAQ,eAAc;AAC9C,SAASC,GAAG,EAAkBC,MAAM,QAAQ,qBAAoB;AAGhE;;;;;CAKC,GACD,MAAMC,mBAAmB;AAEzB,SAASC,eAAkBC,IAAgB;IACzC,IAAIC;IACJ,MAAMC,WAAW,IAAIC,QAAc,CAACC;QAClCH,QAAQI,WAAW,IAAMD,QAAQ,OAAON;IAC1C;IACA,OAAOK,QAAQG,IAAI,CAAC;QAACN;QAAME;KAAS,EAAEK,OAAO,CAAC,IAAMC,aAAaP;AACnE;AAEA;;;;CAIC,GACD,OAAO,SAASQ,uBACdC,SAAoB,EACpBC,KAAa;IAEb,MAAMX,OAAO,OAAOY,YAAoBC;QACtC,MAAMC,WAAW,MAAMf,eAAeF,OAAOD,IAAIc,WAAW,QAAQC,OAAOC,YAAYC;QACvF,OAAOC,CAAAA,4BAAAA,SAAUC,MAAM,MAAMD,SAASE,IAAI,KAAiC;IAC7E;IACA,OAAO;QACLC,MAAM,CAACJ,KAAOb,KAAKL,gBAAgBuB,KAAK,EAAEL;QAC1CM,SAAS,CAACN,KAAOb,KAAK,YAAYa;IACpC;AACF"}
@@ -14,7 +14,8 @@ export interface CampaignFilingChange {
14
14
  * so the entry that records it is written the same way, into the org's
15
15
  * activity collection the log dialog writes, with the scope a record made
16
16
  * on this site carries (`crmScopeTokens`), the signed-in member as author,
17
- * and the CRM's own id as the filing plugin.
17
+ * and the CRM's own id as the filing plugin. The entry rolls up to the
18
+ * record's company (AGL-4123) like any other.
18
19
  *
19
20
  * The site an entry carries is the hook's, or — for a surface that files
20
21
  * leads from several sites at once, the organization-level bulk bar — the
@@ -20,6 +20,8 @@ import { useFirestore, useUser, useUserName } from "@aglyn/tenant-feature-instan
20
20
  import { collection, doc, setDoc } from "firebase/firestore";
21
21
  import { useCallback } from "react";
22
22
  import { buildCampaignFilingActivity } from "../model/campaign-filing-activity.js";
23
+ import { resolveCrmActivityCompany } from "../model/crm-activity-link.js";
24
+ import { crmActivityClientReads } from "./crm-activity-client-reads.js";
23
25
  /**
24
26
  * FILES A MEMBER'S FILING ON THE RECORD'S TIMELINE (AGL-3274), from the
25
27
  * console.
@@ -29,7 +31,8 @@ import { buildCampaignFilingActivity } from "../model/campaign-filing-activity.j
29
31
  * so the entry that records it is written the same way, into the org's
30
32
  * activity collection the log dialog writes, with the scope a record made
31
33
  * on this site carries (`crmScopeTokens`), the signed-in member as author,
32
- * and the CRM's own id as the filing plugin.
34
+ * and the CRM's own id as the filing plugin. The entry rolls up to the
35
+ * record's company (AGL-4123) like any other.
33
36
  *
34
37
  * The site an entry carries is the hook's, or — for a surface that files
35
38
  * leads from several sites at once, the organization-level bulk bar — the
@@ -60,7 +63,11 @@ import { buildCampaignFilingActivity } from "../model/campaign-filing-activity.j
60
63
  }))
61
64
  ];
62
65
  if (!entries.length) return;
63
- const visibleTo = crmScopeTokens(org, consentGroupForHost(org, siteHostId));
66
+ const group = consentGroupForHost(org, siteHostId);
67
+ const visibleTo = crmScopeTokens(org, group);
68
+ const stamped = await resolveCrmActivityCompany(link, {
69
+ groupId: group.groupId
70
+ }, crmActivityClientReads(firestore, orgId));
64
71
  const atMs = Date.now();
65
72
  const activities = collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.activities);
66
73
  for (const entry of entries){
@@ -69,7 +76,7 @@ import { buildCampaignFilingActivity } from "../model/campaign-filing-activity.j
69
76
  await setDoc(doc(activities, createResourceUid()), _extends({}, buildCampaignFilingActivity({
70
77
  action: entry.action,
71
78
  campaign: entry.campaign,
72
- link,
79
+ link: stamped,
73
80
  hostId: siteHostId,
74
81
  visibleTo,
75
82
  atMs,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-campaign-filing-log.ts"],"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 CRM_COLLECTIONS,\n consentGroupForHost,\n createResourceUid,\n crmScopeTokens,\n type CrmActivityLink,\n} from '@aglyn/aglyn'\nimport { useFirestore, useUser, useUserName } from '@aglyn/tenant-feature-instance'\nimport { collection, doc, setDoc } from 'firebase/firestore'\nimport { useCallback } from 'react'\nimport {\n buildCampaignFilingActivity,\n type CampaignFilingRef,\n} from '../model/campaign-filing-activity'\n\n/** What one surface's save changed, by campaign, with the names it showed. */\nexport interface CampaignFilingChange {\n filed?: readonly CampaignFilingRef[]\n removed?: readonly CampaignFilingRef[]\n}\n\n/**\n * FILES A MEMBER'S FILING ON THE RECORD'S TIMELINE (AGL-3274), from the\n * console.\n *\n * The lead's Campaigns card and the Leads list's bulk bar write the\n * membership client-direct — a one-field update the rules already admit —\n * so the entry that records it is written the same way, into the org's\n * activity collection the log dialog writes, with the scope a record made\n * on this site carries (`crmScopeTokens`), the signed-in member as author,\n * and the CRM's own id as the filing plugin.\n *\n * The site an entry carries is the hook's, or — for a surface that files\n * leads from several sites at once, the organization-level bulk bar — the\n * one a call names for its record. With neither there is no scope to stamp,\n * and nothing is written.\n *\n * Bookkeeping beside the act: the membership has landed by the time this\n * runs, so a write that fails is logged and the filing stands — the card\n * shows the campaigns from the document, not from the timeline.\n */\nexport function useCampaignFilingLog(input: {\n orgId: string | null | undefined\n /** The site the entry carries as provenance: the lead's, or the contact's viewing site. */\n hostId: string | null | undefined\n org: Record<string, unknown> | null | undefined\n}) {\n const { orgId, hostId, org } = input\n const firestore = useFirestore()\n const { data: user } = useUser()\n const authorName = useUserName()\n const uid = user?.uid\n\n return useCallback(\n async (\n link: CrmActivityLink,\n change: CampaignFilingChange,\n recordHostId?: string | null,\n ): Promise<void> => {\n const siteHostId = hostId || recordHostId || null\n if (!orgId || !siteHostId || !uid) return\n const entries = [\n ...(change.filed ?? []).map((campaign) => ({ action: 'filed' as const, campaign })),\n ...(change.removed ?? []).map((campaign) => ({ action: 'removed' as const, campaign })),\n ]\n if (!entries.length) return\n const visibleTo = crmScopeTokens(org, consentGroupForHost(org, siteHostId))\n const atMs = Date.now()\n const activities = collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.activities)\n for (const entry of entries) {\n try {\n // Named by the platform's own id, as every console resource is.\n await setDoc(doc(activities, createResourceUid()), {\n ...buildCampaignFilingActivity({\n action: entry.action,\n campaign: entry.campaign,\n link,\n hostId: siteHostId,\n visibleTo,\n atMs,\n byUid: uid,\n byName: authorName || null,\n }),\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n } catch (error) {\n console.error('[crm] the filing could not be written on the timeline', error)\n }\n }\n },\n [firestore, orgId, hostId, org, uid, authorName],\n )\n}\n\nexport default useCampaignFilingLog\n"],"names":["CRM_COLLECTIONS","consentGroupForHost","createResourceUid","crmScopeTokens","useFirestore","useUser","useUserName","collection","doc","setDoc","useCallback","buildCampaignFilingActivity","useCampaignFilingLog","input","orgId","hostId","org","firestore","data","user","authorName","uid","link","change","recordHostId","siteHostId","entries","filed","map","campaign","action","removed","length","visibleTo","atMs","Date","now","activities","entry","byUid","byName","createdAt","updatedAt","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,eAAe,EACfC,mBAAmB,EACnBC,iBAAiB,EACjBC,cAAc,QAET,eAAc;AACrB,SAASC,YAAY,EAAEC,OAAO,EAAEC,WAAW,QAAQ,iCAAgC;AACnF,SAASC,UAAU,EAAEC,GAAG,EAAEC,MAAM,QAAQ,qBAAoB;AAC5D,SAASC,WAAW,QAAQ,QAAO;AACnC,SACEC,2BAA2B,QAEtB,uCAAmC;AAQ1C;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC,qBAAqBC,KAKpC;IACC,MAAM,EAAEC,KAAK,EAAEC,MAAM,EAAEC,GAAG,EAAE,GAAGH;IAC/B,MAAMI,YAAYb;IAClB,MAAM,EAAEc,MAAMC,IAAI,EAAE,GAAGd;IACvB,MAAMe,aAAad;IACnB,MAAMe,MAAMF,wBAAAA,KAAME,GAAG;IAErB,OAAOX,YACL,OACEY,MACAC,QACAC;YAKMD,eACAA;QAJN,MAAME,aAAaV,UAAUS,gBAAgB;QAC7C,IAAI,CAACV,SAAS,CAACW,cAAc,CAACJ,KAAK;QACnC,MAAMK,UAAU;eACX,EAACH,gBAAAA,OAAOI,KAAK,YAAZJ,gBAAgB,EAAE,EAAEK,GAAG,CAAC,CAACC,WAAc,CAAA;oBAAEC,QAAQ;oBAAkBD;gBAAS,CAAA;eAC7E,EAACN,kBAAAA,OAAOQ,OAAO,YAAdR,kBAAkB,EAAE,EAAEK,GAAG,CAAC,CAACC,WAAc,CAAA;oBAAEC,QAAQ;oBAAoBD;gBAAS,CAAA;SACrF;QACD,IAAI,CAACH,QAAQM,MAAM,EAAE;QACrB,MAAMC,YAAY9B,eAAea,KAAKf,oBAAoBe,KAAKS;QAC/D,MAAMS,OAAOC,KAAKC,GAAG;QACrB,MAAMC,aAAa9B,WAAWU,WAAW,QAAQH,OAAOd,gBAAgBqC,UAAU;QAClF,KAAK,MAAMC,SAASZ,QAAS;YAC3B,IAAI;gBACF,gEAAgE;gBAChE,MAAMjB,OAAOD,IAAI6B,YAAYnC,sBAAsB,aAC9CS,4BAA4B;oBAC7BmB,QAAQQ,MAAMR,MAAM;oBACpBD,UAAUS,MAAMT,QAAQ;oBACxBP;oBACAP,QAAQU;oBACRQ;oBACAC;oBACAK,OAAOlB;oBACPmB,QAAQpB,cAAc;gBACxB;oBACAqB,WAAW,IAAIN;oBACfO,WAAW,IAAIP;;YAEnB,EAAE,OAAOQ,OAAO;gBACdC,QAAQD,KAAK,CAAC,yDAAyDA;YACzE;QACF;IACF,GACA;QAAC1B;QAAWH;QAAOC;QAAQC;QAAKK;QAAKD;KAAW;AAEpD;AAEA,eAAeR,qBAAoB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-campaign-filing-log.ts"],"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 CRM_COLLECTIONS,\n consentGroupForHost,\n createResourceUid,\n crmScopeTokens,\n type CrmActivityLink,\n} from '@aglyn/aglyn'\nimport { useFirestore, useUser, useUserName } from '@aglyn/tenant-feature-instance'\nimport { collection, doc, setDoc } from 'firebase/firestore'\nimport { useCallback } from 'react'\nimport {\n buildCampaignFilingActivity,\n type CampaignFilingRef,\n} from '../model/campaign-filing-activity'\nimport { resolveCrmActivityCompany } from '../model/crm-activity-link'\nimport { crmActivityClientReads } from './crm-activity-client-reads'\n\n/** What one surface's save changed, by campaign, with the names it showed. */\nexport interface CampaignFilingChange {\n filed?: readonly CampaignFilingRef[]\n removed?: readonly CampaignFilingRef[]\n}\n\n/**\n * FILES A MEMBER'S FILING ON THE RECORD'S TIMELINE (AGL-3274), from the\n * console.\n *\n * The lead's Campaigns card and the Leads list's bulk bar write the\n * membership client-direct — a one-field update the rules already admit —\n * so the entry that records it is written the same way, into the org's\n * activity collection the log dialog writes, with the scope a record made\n * on this site carries (`crmScopeTokens`), the signed-in member as author,\n * and the CRM's own id as the filing plugin. The entry rolls up to the\n * record's company (AGL-4123) like any other.\n *\n * The site an entry carries is the hook's, or — for a surface that files\n * leads from several sites at once, the organization-level bulk bar — the\n * one a call names for its record. With neither there is no scope to stamp,\n * and nothing is written.\n *\n * Bookkeeping beside the act: the membership has landed by the time this\n * runs, so a write that fails is logged and the filing stands — the card\n * shows the campaigns from the document, not from the timeline.\n */\nexport function useCampaignFilingLog(input: {\n orgId: string | null | undefined\n /** The site the entry carries as provenance: the lead's, or the contact's viewing site. */\n hostId: string | null | undefined\n org: Record<string, unknown> | null | undefined\n}) {\n const { orgId, hostId, org } = input\n const firestore = useFirestore()\n const { data: user } = useUser()\n const authorName = useUserName()\n const uid = user?.uid\n\n return useCallback(\n async (\n link: CrmActivityLink,\n change: CampaignFilingChange,\n recordHostId?: string | null,\n ): Promise<void> => {\n const siteHostId = hostId || recordHostId || null\n if (!orgId || !siteHostId || !uid) return\n const entries = [\n ...(change.filed ?? []).map((campaign) => ({ action: 'filed' as const, campaign })),\n ...(change.removed ?? []).map((campaign) => ({ action: 'removed' as const, campaign })),\n ]\n if (!entries.length) return\n const group = consentGroupForHost(org, siteHostId)\n const visibleTo = crmScopeTokens(org, group)\n const stamped = await resolveCrmActivityCompany(\n link,\n { groupId: group.groupId },\n crmActivityClientReads(firestore, orgId),\n )\n const atMs = Date.now()\n const activities = collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.activities)\n for (const entry of entries) {\n try {\n // Named by the platform's own id, as every console resource is.\n await setDoc(doc(activities, createResourceUid()), {\n ...buildCampaignFilingActivity({\n action: entry.action,\n campaign: entry.campaign,\n link: stamped,\n hostId: siteHostId,\n visibleTo,\n atMs,\n byUid: uid,\n byName: authorName || null,\n }),\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n } catch (error) {\n console.error('[crm] the filing could not be written on the timeline', error)\n }\n }\n },\n [firestore, orgId, hostId, org, uid, authorName],\n )\n}\n\nexport default useCampaignFilingLog\n"],"names":["CRM_COLLECTIONS","consentGroupForHost","createResourceUid","crmScopeTokens","useFirestore","useUser","useUserName","collection","doc","setDoc","useCallback","buildCampaignFilingActivity","resolveCrmActivityCompany","crmActivityClientReads","useCampaignFilingLog","input","orgId","hostId","org","firestore","data","user","authorName","uid","link","change","recordHostId","siteHostId","entries","filed","map","campaign","action","removed","length","group","visibleTo","stamped","groupId","atMs","Date","now","activities","entry","byUid","byName","createdAt","updatedAt","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,eAAe,EACfC,mBAAmB,EACnBC,iBAAiB,EACjBC,cAAc,QAET,eAAc;AACrB,SAASC,YAAY,EAAEC,OAAO,EAAEC,WAAW,QAAQ,iCAAgC;AACnF,SAASC,UAAU,EAAEC,GAAG,EAAEC,MAAM,QAAQ,qBAAoB;AAC5D,SAASC,WAAW,QAAQ,QAAO;AACnC,SACEC,2BAA2B,QAEtB,uCAAmC;AAC1C,SAASC,yBAAyB,QAAQ,gCAA4B;AACtE,SAASC,sBAAsB,QAAQ,iCAA6B;AAQpE;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASC,qBAAqBC,KAKpC;IACC,MAAM,EAAEC,KAAK,EAAEC,MAAM,EAAEC,GAAG,EAAE,GAAGH;IAC/B,MAAMI,YAAYf;IAClB,MAAM,EAAEgB,MAAMC,IAAI,EAAE,GAAGhB;IACvB,MAAMiB,aAAahB;IACnB,MAAMiB,MAAMF,wBAAAA,KAAME,GAAG;IAErB,OAAOb,YACL,OACEc,MACAC,QACAC;YAKMD,eACAA;QAJN,MAAME,aAAaV,UAAUS,gBAAgB;QAC7C,IAAI,CAACV,SAAS,CAACW,cAAc,CAACJ,KAAK;QACnC,MAAMK,UAAU;eACX,EAACH,gBAAAA,OAAOI,KAAK,YAAZJ,gBAAgB,EAAE,EAAEK,GAAG,CAAC,CAACC,WAAc,CAAA;oBAAEC,QAAQ;oBAAkBD;gBAAS,CAAA;eAC7E,EAACN,kBAAAA,OAAOQ,OAAO,YAAdR,kBAAkB,EAAE,EAAEK,GAAG,CAAC,CAACC,WAAc,CAAA;oBAAEC,QAAQ;oBAAoBD;gBAAS,CAAA;SACrF;QACD,IAAI,CAACH,QAAQM,MAAM,EAAE;QACrB,MAAMC,QAAQlC,oBAAoBiB,KAAKS;QACvC,MAAMS,YAAYjC,eAAee,KAAKiB;QACtC,MAAME,UAAU,MAAMzB,0BACpBY,MACA;YAAEc,SAASH,MAAMG,OAAO;QAAC,GACzBzB,uBAAuBM,WAAWH;QAEpC,MAAMuB,OAAOC,KAAKC,GAAG;QACrB,MAAMC,aAAanC,WAAWY,WAAW,QAAQH,OAAOhB,gBAAgB0C,UAAU;QAClF,KAAK,MAAMC,SAASf,QAAS;YAC3B,IAAI;gBACF,gEAAgE;gBAChE,MAAMnB,OAAOD,IAAIkC,YAAYxC,sBAAsB,aAC9CS,4BAA4B;oBAC7BqB,QAAQW,MAAMX,MAAM;oBACpBD,UAAUY,MAAMZ,QAAQ;oBACxBP,MAAMa;oBACNpB,QAAQU;oBACRS;oBACAG;oBACAK,OAAOrB;oBACPsB,QAAQvB,cAAc;gBACxB;oBACAwB,WAAW,IAAIN;oBACfO,WAAW,IAAIP;;YAEnB,EAAE,OAAOQ,OAAO;gBACdC,QAAQD,KAAK,CAAC,yDAAyDA;YACzE;QACF;IACF,GACA;QAAC7B;QAAWH;QAAOC;QAAQC;QAAKK;QAAKD;KAAW;AAEpD;AAEA,eAAeR,qBAAoB"}
@@ -14,14 +14,16 @@ import { type ComponentType, type ReactNode } from 'react';
14
14
  */
15
15
  export interface CrmOrgMount extends ConsolePluginOrgMount {
16
16
  /**
17
- * The site the next create stamps from: the reader's last pick this
18
- * session, or the org's only site when it has exactly one, else `null`
19
- * until somebody picks. A record is always captured BY a site — its
20
- * `hostId`, its `visibleTo`, the consent group its profile lives on all
21
- * come from one — and at the org level nothing else can say which.
17
+ * The site the next create comes from: the reader's last pick this
18
+ * session, or the org's only site when it has exactly one, else `null`.
19
+ * In an org that keeps each site's records apart a record must come from
20
+ * a site — its `visibleTo` and the consent group its profile lives on
21
+ * follow from it — so a create holds until one is picked; in an org whose
22
+ * records belong to every site, `null` is an answer: a record from no site.
22
23
  */
23
24
  createHostId: string | null;
24
- setCreateHostId: (hostId: string) => void;
25
+ /** Picks the site the next create comes from; `null` picks none. */
26
+ setCreateHostId: (hostId: string | null) => void;
25
27
  /** How a site reads on screen — its name, or its id when the list cannot name it. */
26
28
  siteName: (hostId: string) => string;
27
29
  /**
@@ -34,7 +34,8 @@ function readRememberedPick(orgId) {
34
34
  }
35
35
  function writeRememberedPick(orgId, hostId) {
36
36
  try {
37
- window.sessionStorage.setItem(pickStorageKey(orgId), hostId);
37
+ if (hostId) window.sessionStorage.setItem(pickStorageKey(orgId), hostId);
38
+ else window.sessionStorage.removeItem(pickStorageKey(orgId));
38
39
  } catch (unused) {
39
40
  // A browser that refuses storage still gets the pick for this page.
40
41
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-crm-org-mount.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 type { ConsolePluginOrgHost, ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport {\n type ComponentType,\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n} from 'react'\n\n/**\n * The organization-level mount, as every CRM surface beneath the hub reads\n * it (AGL-2630).\n *\n * The shell hands the hub page an org and its sites; the hub publishes them\n * through this context so a drawer four components down — the one that has\n * to ask which site a new record is captured by — reaches them without four\n * components threading a prop they never read. `useCrmScope` reads it too:\n * a surface handed `hostId: null` learns from here which org it is under,\n * which is what lets every existing `useCrmScope({ hostId, org })` call\n * serve the org level unchanged.\n */\nexport interface CrmOrgMount extends ConsolePluginOrgMount {\n /**\n * The site the next create stamps from: the reader's last pick this\n * session, or the org's only site when it has exactly one, else `null`\n * until somebody picks. A record is always captured BY a site — its\n * `hostId`, its `visibleTo`, the consent group its profile lives on all\n * come from one — and at the org level nothing else can say which.\n */\n createHostId: string | null\n setCreateHostId: (hostId: string) => void\n /** How a site reads on screen — its name, or its id when the list cannot name it. */\n siteName: (hostId: string) => string\n /**\n * The subdomain a console URL under `/hosts/[host]` takes, or `null` for\n * a site the list did not answer for — named, never linked.\n */\n siteSubdomain: (hostId: string) => string | null\n /**\n * The site's own CRM hub — `${hostsPath}/${subdomain}/crm` — or `null`\n * for a site whose subdomain the list could not answer, which is named\n * and not linked.\n */\n siteHubHref: (hostId: string) => string | null\n}\n\nconst CrmOrgMountContext = createContext<CrmOrgMount | null>(null)\n\n/**\n * The session's memory of the picked site, per org. Session storage rather\n * than local: the pick is a working convenience for one sitting, and a\n * site remembered across weeks would stamp a record onto a site the reader\n * had forgotten choosing.\n */\nconst pickStorageKey = (orgId: string) => `aglyn.crm.createSite.${orgId}`\n\nfunction readRememberedPick(orgId: string): string | null {\n try {\n return window.sessionStorage.getItem(pickStorageKey(orgId))\n } catch {\n return null\n }\n}\n\nfunction writeRememberedPick(orgId: string, hostId: string): void {\n try {\n window.sessionStorage.setItem(pickStorageKey(orgId), hostId)\n } catch {\n // A browser that refuses storage still gets the pick for this page.\n }\n}\n\n/**\n * Publishes the org-level mount to every surface beneath it.\n *\n * Mounted by the hub page ONLY when the shell handed it an `orgMount`; under\n * a site there is no provider and `useCrmOrgMount` answers `null`, which is\n * how a surface tells the two levels apart without a second prop.\n */\nexport function CrmOrgMountProvider(props: {\n mount: ConsolePluginOrgMount\n children: ReactNode\n}) {\n const { mount, children } = props\n const { orgId, orgSlug, hosts, hostsReady, hostsPath } = mount\n const [picked, setPicked] = useState<string | null>(null)\n // Read after mount rather than in the initializer, so the server and the\n // first client paint agree: the pick only ever affects a drawer somebody\n // has since opened.\n useEffect(() => {\n setPicked(readRememberedPick(orgId))\n }, [orgId])\n\n const byId = useMemo(() => {\n const index = new Map<string, ConsolePluginOrgHost>()\n for (const host of hosts) index.set(host.id, host)\n return index\n }, [hosts])\n\n /*\n * A remembered site the list no longer carries is forgotten rather than\n * stamped — the reader may have lost the site, or the site may be gone.\n * An org with exactly one site needs no picker at all; nothing else is\n * picked silently, because a wrong guess files a person under a brand\n * that never met them.\n */\n const createHostId = useMemo(() => {\n if (picked && byId.has(picked)) return picked\n if (hostsReady && hosts.length === 1) return hosts[0].id\n return null\n }, [picked, byId, hostsReady, hosts])\n\n const setCreateHostId = useCallback(\n (hostId: string) => {\n setPicked(hostId)\n writeRememberedPick(orgId, hostId)\n },\n [orgId],\n )\n const siteName = useCallback(\n (hostId: string) => byId.get(hostId)?.name || hostId,\n [byId],\n )\n const siteSubdomain = useCallback(\n (hostId: string) => byId.get(hostId)?.subdomain ?? null,\n [byId],\n )\n const siteHubHref = useCallback(\n (hostId: string) => {\n const subdomain = byId.get(hostId)?.subdomain\n return subdomain ? `${hostsPath}/${encodeURIComponent(subdomain)}/crm` : null\n },\n [byId, hostsPath],\n )\n\n const value = useMemo<CrmOrgMount>(\n () => ({\n orgId,\n orgSlug,\n hosts,\n hostsReady,\n hostsPath,\n createHostId,\n setCreateHostId,\n siteName,\n siteSubdomain,\n siteHubHref,\n }),\n [\n orgId,\n orgSlug,\n hosts,\n hostsReady,\n hostsPath,\n createHostId,\n setCreateHostId,\n siteName,\n siteSubdomain,\n siteHubHref,\n ],\n )\n return (\n <CrmOrgMountContext.Provider value={value}>\n {children}\n </CrmOrgMountContext.Provider>\n )\n}\nCrmOrgMountProvider.displayName = 'CrmOrgMountProvider'\n\n/** The org-level mount, or `null` under a site. */\nexport function useCrmOrgMount(): CrmOrgMount | null {\n return useContext(CrmOrgMountContext)\n}\n\n/**\n * `Card` beneath the org-level mount the SHELL hands it as a prop (AGL-2636).\n *\n * The hub page mounts the provider itself, because the shell hands the hub\n * page an `orgMount` and the page owns everything under it. A widget on the\n * org's `orgDashboard` slot is dropped into a console page the CRM does not\n * own — the org's sites list — and the console never imports a plugin, so\n * the page cannot mount this plugin's provider around the slot. The slot\n * hands each widget the same `orgMount` instead, and this puts the provider\n * where the hub page puts it: around the surface, one level down. `Card`\n * then reads `useCrmOrgMount()` and `useCrmScope({ hostId: null })` exactly\n * as it does under the hub, and stays one component for both mounts.\n *\n * Handed no mount — a slot that names a site, or a shell older than the org\n * zone — the card renders as it is, and `useCrmOrgMount` answers `null` the\n * way it does under a site.\n */\nexport function withCrmOrgMount<P extends object>(\n Card: ComponentType<P>,\n): ComponentType<P & { orgMount?: ConsolePluginOrgMount }> {\n function CrmOrgMounted(props: P & { orgMount?: ConsolePluginOrgMount }) {\n const { orgMount, ...rest } = props\n const card = <Card {...(rest as P)} />\n return orgMount ? (\n <CrmOrgMountProvider mount={orgMount}>{card}</CrmOrgMountProvider>\n ) : (\n card\n )\n }\n CrmOrgMounted.displayName = `CrmOrgMounted(${\n Card.displayName ?? (Card as { name?: string }).name ?? 'Card'\n })`\n return CrmOrgMounted\n}\n\n/**\n * THE SITE A CREATE OPENED FROM A RECORD DEFAULTS TO (AGL-2630).\n *\n * A task, an activity or a deal filed from a contact's page belongs, nine\n * times in ten, with the site that captured the contact — not with the\n * site the reader last picked in a list's drawer. So a record page wraps\n * its cards in this, naming its record's own capturing site, and every\n * create under it defaults its Site picker there. A pick the reader makes\n * INSIDE such a create is held for that page alone; the session's pick,\n * which a create opened from a list keeps defaulting to, is untouched.\n *\n * Nothing under a site, nothing for a record no site has captured, and\n * nothing for a site the mount's list does not carry — the reader may not\n * have it: the children render as they are and the session's pick stands.\n */\nexport function CrmCreateSiteDefault(props: {\n hostId: string | null | undefined\n children: ReactNode\n}) {\n const { hostId, children } = props\n const mount = useContext(CrmOrgMountContext)\n const [picked, setPicked] = useState<string | null>(null)\n // Another record on the same page starts from its own site again.\n useEffect(() => {\n setPicked(null)\n }, [hostId])\n const value = useMemo<CrmOrgMount | null>(() => {\n if (!mount || !hostId) return mount\n if (!mount.hosts.some((host) => host.id === hostId)) return mount\n return {\n ...mount,\n createHostId: picked ?? hostId,\n setCreateHostId: setPicked,\n }\n }, [mount, hostId, picked])\n return (\n <CrmOrgMountContext.Provider value={value}>\n {children}\n </CrmOrgMountContext.Provider>\n )\n}\nCrmCreateSiteDefault.displayName = 'CrmCreateSiteDefault'\n\nexport default useCrmOrgMount\n"],"names":["createContext","useCallback","useContext","useEffect","useMemo","useState","CrmOrgMountContext","pickStorageKey","orgId","readRememberedPick","window","sessionStorage","getItem","writeRememberedPick","hostId","setItem","CrmOrgMountProvider","props","mount","children","orgSlug","hosts","hostsReady","hostsPath","picked","setPicked","byId","index","Map","host","set","id","createHostId","has","length","setCreateHostId","siteName","get","name","siteSubdomain","subdomain","siteHubHref","encodeURIComponent","value","Provider","displayName","useCrmOrgMount","withCrmOrgMount","Card","CrmOrgMounted","orgMount","rest","card","CrmCreateSiteDefault","some"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-crm-org-mount.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 type { ConsolePluginOrgHost, ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport {\n type ComponentType,\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n} from 'react'\n\n/**\n * The organization-level mount, as every CRM surface beneath the hub reads\n * it (AGL-2630).\n *\n * The shell hands the hub page an org and its sites; the hub publishes them\n * through this context so a drawer four components down — the one that has\n * to ask which site a new record is captured by — reaches them without four\n * components threading a prop they never read. `useCrmScope` reads it too:\n * a surface handed `hostId: null` learns from here which org it is under,\n * which is what lets every existing `useCrmScope({ hostId, org })` call\n * serve the org level unchanged.\n */\nexport interface CrmOrgMount extends ConsolePluginOrgMount {\n /**\n * The site the next create comes from: the reader's last pick this\n * session, or the org's only site when it has exactly one, else `null`.\n * In an org that keeps each site's records apart a record must come from\n * a site — its `visibleTo` and the consent group its profile lives on\n * follow from it — so a create holds until one is picked; in an org whose\n * records belong to every site, `null` is an answer: a record from no site.\n */\n createHostId: string | null\n /** Picks the site the next create comes from; `null` picks none. */\n setCreateHostId: (hostId: string | null) => void\n /** How a site reads on screen — its name, or its id when the list cannot name it. */\n siteName: (hostId: string) => string\n /**\n * The subdomain a console URL under `/hosts/[host]` takes, or `null` for\n * a site the list did not answer for — named, never linked.\n */\n siteSubdomain: (hostId: string) => string | null\n /**\n * The site's own CRM hub — `${hostsPath}/${subdomain}/crm` — or `null`\n * for a site whose subdomain the list could not answer, which is named\n * and not linked.\n */\n siteHubHref: (hostId: string) => string | null\n}\n\nconst CrmOrgMountContext = createContext<CrmOrgMount | null>(null)\n\n/**\n * The session's memory of the picked site, per org. Session storage rather\n * than local: the pick is a working convenience for one sitting, and a\n * site remembered across weeks would stamp a record onto a site the reader\n * had forgotten choosing.\n */\nconst pickStorageKey = (orgId: string) => `aglyn.crm.createSite.${orgId}`\n\nfunction readRememberedPick(orgId: string): string | null {\n try {\n return window.sessionStorage.getItem(pickStorageKey(orgId))\n } catch {\n return null\n }\n}\n\nfunction writeRememberedPick(orgId: string, hostId: string | null): void {\n try {\n if (hostId) window.sessionStorage.setItem(pickStorageKey(orgId), hostId)\n else window.sessionStorage.removeItem(pickStorageKey(orgId))\n } catch {\n // A browser that refuses storage still gets the pick for this page.\n }\n}\n\n/**\n * Publishes the org-level mount to every surface beneath it.\n *\n * Mounted by the hub page ONLY when the shell handed it an `orgMount`; under\n * a site there is no provider and `useCrmOrgMount` answers `null`, which is\n * how a surface tells the two levels apart without a second prop.\n */\nexport function CrmOrgMountProvider(props: {\n mount: ConsolePluginOrgMount\n children: ReactNode\n}) {\n const { mount, children } = props\n const { orgId, orgSlug, hosts, hostsReady, hostsPath } = mount\n const [picked, setPicked] = useState<string | null>(null)\n // Read after mount rather than in the initializer, so the server and the\n // first client paint agree: the pick only ever affects a drawer somebody\n // has since opened.\n useEffect(() => {\n setPicked(readRememberedPick(orgId))\n }, [orgId])\n\n const byId = useMemo(() => {\n const index = new Map<string, ConsolePluginOrgHost>()\n for (const host of hosts) index.set(host.id, host)\n return index\n }, [hosts])\n\n /*\n * A remembered site the list no longer carries is forgotten rather than\n * stamped — the reader may have lost the site, or the site may be gone.\n * An org with exactly one site needs no picker at all; nothing else is\n * picked silently, because a wrong guess files a person under a brand\n * that never met them.\n */\n const createHostId = useMemo(() => {\n if (picked && byId.has(picked)) return picked\n if (hostsReady && hosts.length === 1) return hosts[0].id\n return null\n }, [picked, byId, hostsReady, hosts])\n\n const setCreateHostId = useCallback(\n (hostId: string | null) => {\n setPicked(hostId)\n writeRememberedPick(orgId, hostId)\n },\n [orgId],\n )\n const siteName = useCallback(\n (hostId: string) => byId.get(hostId)?.name || hostId,\n [byId],\n )\n const siteSubdomain = useCallback(\n (hostId: string) => byId.get(hostId)?.subdomain ?? null,\n [byId],\n )\n const siteHubHref = useCallback(\n (hostId: string) => {\n const subdomain = byId.get(hostId)?.subdomain\n return subdomain ? `${hostsPath}/${encodeURIComponent(subdomain)}/crm` : null\n },\n [byId, hostsPath],\n )\n\n const value = useMemo<CrmOrgMount>(\n () => ({\n orgId,\n orgSlug,\n hosts,\n hostsReady,\n hostsPath,\n createHostId,\n setCreateHostId,\n siteName,\n siteSubdomain,\n siteHubHref,\n }),\n [\n orgId,\n orgSlug,\n hosts,\n hostsReady,\n hostsPath,\n createHostId,\n setCreateHostId,\n siteName,\n siteSubdomain,\n siteHubHref,\n ],\n )\n return (\n <CrmOrgMountContext.Provider value={value}>\n {children}\n </CrmOrgMountContext.Provider>\n )\n}\nCrmOrgMountProvider.displayName = 'CrmOrgMountProvider'\n\n/** The org-level mount, or `null` under a site. */\nexport function useCrmOrgMount(): CrmOrgMount | null {\n return useContext(CrmOrgMountContext)\n}\n\n/**\n * `Card` beneath the org-level mount the SHELL hands it as a prop (AGL-2636).\n *\n * The hub page mounts the provider itself, because the shell hands the hub\n * page an `orgMount` and the page owns everything under it. A widget on the\n * org's `orgDashboard` slot is dropped into a console page the CRM does not\n * own — the org's sites list — and the console never imports a plugin, so\n * the page cannot mount this plugin's provider around the slot. The slot\n * hands each widget the same `orgMount` instead, and this puts the provider\n * where the hub page puts it: around the surface, one level down. `Card`\n * then reads `useCrmOrgMount()` and `useCrmScope({ hostId: null })` exactly\n * as it does under the hub, and stays one component for both mounts.\n *\n * Handed no mount — a slot that names a site, or a shell older than the org\n * zone — the card renders as it is, and `useCrmOrgMount` answers `null` the\n * way it does under a site.\n */\nexport function withCrmOrgMount<P extends object>(\n Card: ComponentType<P>,\n): ComponentType<P & { orgMount?: ConsolePluginOrgMount }> {\n function CrmOrgMounted(props: P & { orgMount?: ConsolePluginOrgMount }) {\n const { orgMount, ...rest } = props\n const card = <Card {...(rest as P)} />\n return orgMount ? (\n <CrmOrgMountProvider mount={orgMount}>{card}</CrmOrgMountProvider>\n ) : (\n card\n )\n }\n CrmOrgMounted.displayName = `CrmOrgMounted(${\n Card.displayName ?? (Card as { name?: string }).name ?? 'Card'\n })`\n return CrmOrgMounted\n}\n\n/**\n * THE SITE A CREATE OPENED FROM A RECORD DEFAULTS TO (AGL-2630).\n *\n * A task, an activity or a deal filed from a contact's page belongs, nine\n * times in ten, with the site that captured the contact — not with the\n * site the reader last picked in a list's drawer. So a record page wraps\n * its cards in this, naming its record's own capturing site, and every\n * create under it defaults its Site picker there. A pick the reader makes\n * INSIDE such a create is held for that page alone; the session's pick,\n * which a create opened from a list keeps defaulting to, is untouched.\n *\n * Nothing under a site, nothing for a record no site has captured, and\n * nothing for a site the mount's list does not carry — the reader may not\n * have it: the children render as they are and the session's pick stands.\n */\nexport function CrmCreateSiteDefault(props: {\n hostId: string | null | undefined\n children: ReactNode\n}) {\n const { hostId, children } = props\n const mount = useContext(CrmOrgMountContext)\n const [picked, setPicked] = useState<string | null>(null)\n // Another record on the same page starts from its own site again.\n useEffect(() => {\n setPicked(null)\n }, [hostId])\n const value = useMemo<CrmOrgMount | null>(() => {\n if (!mount || !hostId) return mount\n if (!mount.hosts.some((host) => host.id === hostId)) return mount\n return {\n ...mount,\n createHostId: picked ?? hostId,\n setCreateHostId: setPicked,\n }\n }, [mount, hostId, picked])\n return (\n <CrmOrgMountContext.Provider value={value}>\n {children}\n </CrmOrgMountContext.Provider>\n )\n}\nCrmCreateSiteDefault.displayName = 'CrmCreateSiteDefault'\n\nexport default useCrmOrgMount\n"],"names":["createContext","useCallback","useContext","useEffect","useMemo","useState","CrmOrgMountContext","pickStorageKey","orgId","readRememberedPick","window","sessionStorage","getItem","writeRememberedPick","hostId","setItem","removeItem","CrmOrgMountProvider","props","mount","children","orgSlug","hosts","hostsReady","hostsPath","picked","setPicked","byId","index","Map","host","set","id","createHostId","has","length","setCreateHostId","siteName","get","name","siteSubdomain","subdomain","siteHubHref","encodeURIComponent","value","Provider","displayName","useCrmOrgMount","withCrmOrgMount","Card","CrmOrgMounted","orgMount","rest","card","CrmCreateSiteDefault","some"],"mappings":"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"}
@@ -47,16 +47,28 @@ export interface CrmScope {
47
47
  */
48
48
  visibleTo: readonly ScopeToken[] | null;
49
49
  /**
50
- * The site a record this viewer CREATES is captured by: the mounted site,
51
- * or at the org level the site the reader picked (`null` until they
52
- * have). A create must hold until this is known — the tokens below are
53
- * empty without it, and a record stamped with none is visible to nobody.
50
+ * The site a record this viewer CREATES came from: the mounted site, or at
51
+ * the org level the site the reader picked — `null` when they picked none.
54
52
  */
55
53
  createHostId: string | null;
56
54
  /** The consent group of {@link createHostId} — what a create records consent for. */
57
55
  createGroup: ConsentGroup | null;
58
- /** What a record this viewer CREATES is stamped with; empty until {@link createHostId} is known. */
56
+ /**
57
+ * What a record this viewer CREATES is stamped with. With a site, what
58
+ * `crmScopeTokens` says for it; with none, the whole org when the org's
59
+ * records belong to every site, and EMPTY when the org keeps its sites
60
+ * apart (or its document has not loaded) — there a record must come from
61
+ * a site, and a create holds until one is picked, because a record stamped
62
+ * with no tokens is visible to nobody.
63
+ */
59
64
  createTokens: readonly ScopeToken[];
65
+ /**
66
+ * Whether a create must name a site: true when the org keeps each site's
67
+ * records apart, where the site decides who may see the record. False when
68
+ * every record belongs to the whole org, where a site is only the source it
69
+ * came from and may be left empty.
70
+ */
71
+ createNeedsSite: boolean;
60
72
  }
61
73
  /**
62
74
  * The scope facts every CRM surface needs, resolved once (AGL-2597, one