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

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 (169) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/activity-list.d.ts +36 -5
  3. package/src/lib/components/activity-list.js +159 -137
  4. package/src/lib/components/activity-list.js.map +1 -1
  5. package/src/lib/components/activity-timeline.d.ts +84 -0
  6. package/src/lib/components/activity-timeline.js +224 -0
  7. package/src/lib/components/activity-timeline.js.map +1 -0
  8. package/src/lib/components/companies-section.d.ts +6 -6
  9. package/src/lib/components/companies-section.js +54 -84
  10. package/src/lib/components/companies-section.js.map +1 -1
  11. package/src/lib/components/company-edit-drawer.js +17 -4
  12. package/src/lib/components/company-edit-drawer.js.map +1 -1
  13. package/src/lib/components/company-picker.js +6 -2
  14. package/src/lib/components/company-picker.js.map +1 -1
  15. package/src/lib/components/company-properties-card.js +53 -36
  16. package/src/lib/components/company-properties-card.js.map +1 -1
  17. package/src/lib/components/contact-associations-card.d.ts +3 -2
  18. package/src/lib/components/contact-associations-card.js +12 -12
  19. package/src/lib/components/contact-associations-card.js.map +1 -1
  20. package/src/lib/components/contact-detail-page.d.ts +4 -3
  21. package/src/lib/components/contact-detail-page.js +18 -12
  22. package/src/lib/components/contact-detail-page.js.map +1 -1
  23. package/src/lib/components/contact-list-columns.d.ts +3 -5
  24. package/src/lib/components/contact-list-columns.js +6 -8
  25. package/src/lib/components/contact-list-columns.js.map +1 -1
  26. package/src/lib/components/contact-properties-card.d.ts +21 -4
  27. package/src/lib/components/contact-properties-card.js +129 -29
  28. package/src/lib/components/contact-properties-card.js.map +1 -1
  29. package/src/lib/components/contact-timeline-card.d.ts +12 -6
  30. package/src/lib/components/contact-timeline-card.js +132 -174
  31. package/src/lib/components/contact-timeline-card.js.map +1 -1
  32. package/src/lib/components/contacts-bulk-bar.js +10 -1
  33. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  34. package/src/lib/components/contacts-section.js +282 -331
  35. package/src/lib/components/contacts-section.js.map +1 -1
  36. package/src/lib/components/crm-email-state-chip.js +4 -1
  37. package/src/lib/components/crm-email-state-chip.js.map +1 -1
  38. package/src/lib/components/crm-next-activity-column.d.ts +4 -2
  39. package/src/lib/components/crm-next-activity-column.js +6 -3
  40. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  41. package/src/lib/components/deal-edit-drawer.js +8 -4
  42. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  43. package/src/lib/components/deal-products-card.js +15 -13
  44. package/src/lib/components/deal-products-card.js.map +1 -1
  45. package/src/lib/components/deal-properties-card.js +7 -0
  46. package/src/lib/components/deal-properties-card.js.map +1 -1
  47. package/src/lib/components/deal-stage-card.js +31 -29
  48. package/src/lib/components/deal-stage-card.js.map +1 -1
  49. package/src/lib/components/deals-section.d.ts +2 -2
  50. package/src/lib/components/deals-section.js +67 -97
  51. package/src/lib/components/deals-section.js.map +1 -1
  52. package/src/lib/components/fields-section.d.ts +4 -0
  53. package/src/lib/components/fields-section.js +368 -154
  54. package/src/lib/components/fields-section.js.map +1 -1
  55. package/src/lib/components/form-contact-fields-card.js +24 -20
  56. package/src/lib/components/form-contact-fields-card.js.map +1 -1
  57. package/src/lib/components/lead-campaigns-card.d.ts +1 -1
  58. package/src/lib/components/lead-campaigns-card.js +43 -38
  59. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  60. package/src/lib/components/lead-properties-card.d.ts +14 -6
  61. package/src/lib/components/lead-properties-card.js +323 -311
  62. package/src/lib/components/lead-properties-card.js.map +1 -1
  63. package/src/lib/components/lead-surfaces-note.js +13 -2
  64. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  65. package/src/lib/components/leads-bulk-bar.js +16 -3
  66. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  67. package/src/lib/components/leads-section.d.ts +2 -1
  68. package/src/lib/components/leads-section.js +110 -148
  69. package/src/lib/components/leads-section.js.map +1 -1
  70. package/src/lib/components/linked-deals-card.js +12 -10
  71. package/src/lib/components/linked-deals-card.js.map +1 -1
  72. package/src/lib/components/recent-activity-feed.d.ts +12 -8
  73. package/src/lib/components/recent-activity-feed.js +54 -29
  74. package/src/lib/components/recent-activity-feed.js.map +1 -1
  75. package/src/lib/components/record-activity-card.d.ts +4 -2
  76. package/src/lib/components/record-activity-card.js +29 -11
  77. package/src/lib/components/record-activity-card.js.map +1 -1
  78. package/src/lib/components/reports/activity-card.js +18 -12
  79. package/src/lib/components/reports/activity-card.js.map +1 -1
  80. package/src/lib/components/reports/lead-funnel-card.js +47 -37
  81. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  82. package/src/lib/components/reports/pipeline-card.js +22 -16
  83. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  84. package/src/lib/components/reports/report-export.d.ts +4 -11
  85. package/src/lib/components/reports/report-export.js +16 -33
  86. package/src/lib/components/reports/report-export.js.map +1 -1
  87. package/src/lib/components/reports/source-conversion-card.js +18 -12
  88. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  89. package/src/lib/components/reports/tasks-card.js +35 -25
  90. package/src/lib/components/reports/tasks-card.js.map +1 -1
  91. package/src/lib/components/tasks-section.js +151 -144
  92. package/src/lib/components/tasks-section.js.map +1 -1
  93. package/src/lib/constants/company-filters.d.ts +13 -0
  94. package/src/lib/constants/company-filters.js +47 -17
  95. package/src/lib/constants/company-filters.js.map +1 -1
  96. package/src/lib/constants/contact-filters.d.ts +61 -10
  97. package/src/lib/constants/contact-filters.js +373 -141
  98. package/src/lib/constants/contact-filters.js.map +1 -1
  99. package/src/lib/constants/deal-filters.d.ts +44 -0
  100. package/src/lib/constants/deal-filters.js +78 -0
  101. package/src/lib/constants/deal-filters.js.map +1 -0
  102. package/src/lib/constants/field-list-query.d.ts +23 -0
  103. package/src/lib/constants/field-list-query.js +80 -0
  104. package/src/lib/constants/field-list-query.js.map +1 -0
  105. package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
  106. package/src/lib/hooks/use-crm-list-query.js +182 -0
  107. package/src/lib/hooks/use-crm-list-query.js.map +1 -0
  108. package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
  109. package/src/lib/hooks/use-crm-tasks.js +44 -42
  110. package/src/lib/hooks/use-crm-tasks.js.map +1 -1
  111. package/src/lib/hooks/use-deals.d.ts +2 -34
  112. package/src/lib/hooks/use-deals.js +3 -62
  113. package/src/lib/hooks/use-deals.js.map +1 -1
  114. package/src/lib/model/crm-list-query.d.ts +91 -0
  115. package/src/lib/model/crm-list-query.js +182 -0
  116. package/src/lib/model/crm-list-query.js.map +1 -0
  117. package/src/lib/model/lead-filters.d.ts +50 -44
  118. package/src/lib/model/lead-filters.js +194 -88
  119. package/src/lib/model/lead-filters.js.map +1 -1
  120. package/src/lib/model/list-fields-api.d.ts +26 -0
  121. package/src/lib/model/list-fields-api.js +47 -0
  122. package/src/lib/model/list-fields-api.js.map +1 -0
  123. package/src/lib/model/list-fields.d.ts +47 -0
  124. package/src/lib/model/list-fields.js +39 -0
  125. package/src/lib/model/list-fields.js.map +1 -0
  126. package/src/lib/model/task-views.d.ts +15 -10
  127. package/src/lib/model/task-views.js +68 -9
  128. package/src/lib/model/task-views.js.map +1 -1
  129. package/src/lib/server/capture-contact.js +7 -5
  130. package/src/lib/server/capture-contact.js.map +1 -1
  131. package/src/lib/server/companies-import.js +3 -3
  132. package/src/lib/server/companies-import.js.map +1 -1
  133. package/src/lib/server/company-delete.js +3 -1
  134. package/src/lib/server/company-delete.js.map +1 -1
  135. package/src/lib/server/consent-group-participant.js +4 -0
  136. package/src/lib/server/consent-group-participant.js.map +1 -1
  137. package/src/lib/server/contact-update.js +14 -8
  138. package/src/lib/server/contact-update.js.map +1 -1
  139. package/src/lib/server/contacts-import.js +4 -2
  140. package/src/lib/server/contacts-import.js.map +1 -1
  141. package/src/lib/server/deals-import.js +4 -2
  142. package/src/lib/server/deals-import.js.map +1 -1
  143. package/src/lib/server/erase-person.js +2 -1
  144. package/src/lib/server/erase-person.js.map +1 -1
  145. package/src/lib/server/lead-create.js +4 -1
  146. package/src/lib/server/lead-create.js.map +1 -1
  147. package/src/lib/server/lead-source-values.js +9 -5
  148. package/src/lib/server/lead-source-values.js.map +1 -1
  149. package/src/lib/server/leads-import.js +4 -1
  150. package/src/lib/server/leads-import.js.map +1 -1
  151. package/src/lib/server/list-fields-routes.d.ts +29 -0
  152. package/src/lib/server/list-fields-routes.js +85 -0
  153. package/src/lib/server/list-fields-routes.js.map +1 -0
  154. package/src/lib/server/record-email-state.js +5 -2
  155. package/src/lib/server/record-email-state.js.map +1 -1
  156. package/src/lib/server/record-timeline.js +3 -2
  157. package/src/lib/server/record-timeline.js.map +1 -1
  158. package/src/lib/server/task-routes.js +6 -3
  159. package/src/lib/server/task-routes.js.map +1 -1
  160. package/src/lib/server/tasks-import.js +5 -2
  161. package/src/lib/server/tasks-import.js.map +1 -1
  162. package/src/lib/server.js +10 -1
  163. package/src/lib/server.js.map +1 -1
  164. package/src/lib/components/contact-custom-fields-card.d.ts +0 -39
  165. package/src/lib/components/contact-custom-fields-card.js +0 -204
  166. package/src/lib/components/contact-custom-fields-card.js.map +0 -1
  167. package/src/lib/hooks/use-org-leads.d.ts +0 -50
  168. package/src/lib/hooks/use-org-leads.js +0 -104
  169. package/src/lib/hooks/use-org-leads.js.map +0 -1
@@ -166,21 +166,50 @@ import { useCrmApi } from "./use-crm-api.js";
166
166
  {
167
167
  label: 'Notes',
168
168
  value: company.notes
169
- },
170
- ...fields.active.map((definition)=>{
171
- var _company_custom;
172
- const text = formatContactCustomValue(definition, (_company_custom = company.custom) == null ? void 0 : _company_custom[definition.key]);
173
- return {
174
- label: definition.label || definition.key,
175
- value: text && definition.type === 'url' ? /*#__PURE__*/ _jsx(Link, {
176
- href: text,
177
- target: "_blank",
178
- rel: "noreferrer noopener",
179
- children: text
180
- }) : text
181
- };
182
- })
169
+ }
183
170
  ];
171
+ // The org's own company fields (AGL-2661), after the built-in ones under
172
+ // "More fields", as a record's Details keep them; none defined, none drawn.
173
+ const moreRows = fields.active.map((definition)=>{
174
+ var _company_custom;
175
+ const text = formatContactCustomValue(definition, (_company_custom = company.custom) == null ? void 0 : _company_custom[definition.key]);
176
+ return {
177
+ label: definition.label || definition.key,
178
+ value: text && definition.type === 'url' ? /*#__PURE__*/ _jsx(Link, {
179
+ href: text,
180
+ target: "_blank",
181
+ rel: "noreferrer noopener",
182
+ children: text
183
+ }) : text
184
+ };
185
+ });
186
+ const factRows = (list)=>list.map((row)=>/*#__PURE__*/ _jsxs(Stack, {
187
+ direction: "row",
188
+ spacing: 2,
189
+ sx: {
190
+ alignItems: 'baseline'
191
+ },
192
+ children: [
193
+ /*#__PURE__*/ _jsx(Typography, {
194
+ variant: "caption",
195
+ color: "text.secondary",
196
+ sx: {
197
+ minWidth: 96,
198
+ flexShrink: 0
199
+ },
200
+ children: row.label
201
+ }),
202
+ /*#__PURE__*/ _jsx(Typography, {
203
+ variant: "body2",
204
+ color: row.value ? 'text.primary' : 'text.secondary',
205
+ sx: {
206
+ whiteSpace: 'pre-wrap',
207
+ overflowWrap: 'anywhere'
208
+ },
209
+ children: row.value || '—'
210
+ })
211
+ ]
212
+ }, row.label));
184
213
  return /*#__PURE__*/ _jsxs(CrmRecordHeader, {
185
214
  kind: "Company",
186
215
  title: String(company.name || company.$id),
@@ -240,35 +269,23 @@ import { useCrmApi } from "./use-crm-api.js";
240
269
  ]
241
270
  }),
242
271
  children: [
243
- /*#__PURE__*/ _jsx(Stack, {
272
+ /*#__PURE__*/ _jsxs(Stack, {
244
273
  spacing: 1,
245
- children: rows.map((row)=>/*#__PURE__*/ _jsxs(Stack, {
246
- direction: "row",
247
- spacing: 2,
248
- sx: {
249
- alignItems: 'baseline'
250
- },
274
+ children: [
275
+ factRows(rows),
276
+ moreRows.length ? /*#__PURE__*/ _jsxs(_Fragment, {
251
277
  children: [
252
278
  /*#__PURE__*/ _jsx(Typography, {
253
- variant: "caption",
254
- color: "text.secondary",
279
+ variant: "subtitle2",
255
280
  sx: {
256
- minWidth: 96,
257
- flexShrink: 0
281
+ pt: 1
258
282
  },
259
- children: row.label
283
+ children: 'More fields'
260
284
  }),
261
- /*#__PURE__*/ _jsx(Typography, {
262
- variant: "body2",
263
- color: row.value ? 'text.primary' : 'text.secondary',
264
- sx: {
265
- whiteSpace: 'pre-wrap',
266
- overflowWrap: 'anywhere'
267
- },
268
- children: row.value || '—'
269
- })
285
+ factRows(moreRows)
270
286
  ]
271
- }, row.label))
287
+ }) : null
288
+ ]
272
289
  }),
273
290
  editing ? /*#__PURE__*/ _jsx(CompanyEditDrawer, {
274
291
  open: true,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-properties-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n type AglynPostalAddress,\n type CrmCompany,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPencilOutline } from '@aglyn/shared-data-mdi'\nimport { MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Button, Link, Stack, Typography } from '@mui/material'\nimport { type ReactNode, useCallback, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport type { CrmScope } from '../hooks/use-crm-scope'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { COMPANY_DETACH_LIMIT } from '../model/companies'\nimport {\n companyDeleteFailureMessage,\n deleteCompanyThroughRoute,\n} from '../model/company-delete'\nimport type { CrmRoutes } from '../model/crm-routes'\nimport CompanyEditDrawer from './company-edit-drawer'\nimport { formatContactCustomValue } from './contact-custom-columns'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { useCrmApi } from './use-crm-api'\n\nexport interface CompanyPropertiesCardProps {\n company: Partial<CrmCompany> & { $id: string }\n /** Whether the document the card holds is server-confirmed. */\n seed: { fromCache: boolean; unreadable: boolean }\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling>\n crmScope: CrmScope\n members: OrgMemberOptions\n routes: CrmRoutes\n /** Called once the record is gone, so the page can leave it. */\n onDeleted: () => void\n}\n\n/** A postal address on one line, or nothing. */\nfunction formatAddress(address: AglynPostalAddress | null | undefined): string {\n if (!address) return ''\n return [\n address.line1,\n address.line2,\n [address.city, address.state].filter(Boolean).join(', '),\n address.postalCode,\n address.country,\n ]\n .map((part) => String(part ?? '').trim())\n .filter(Boolean)\n .join(' · ')\n}\n\n/**\n * WHAT A COMPANY IS: its fields, read off the one document the page holds,\n * with the edit and the delete beside them (AGL-2597).\n *\n * ## Edit is the create drawer\n *\n * The same form the list creates with, opened on the record. One form means\n * one set of refusals — a domain that is not a domain is refused the same\n * way on day one and on a rename — and one place to add a field.\n *\n * ## Delete is a DETACH first\n *\n * `crm/company-delete` — the route the bulk bar calls per row as well —\n * unlinks the contacts that name this company before removing it, bounded\n * at {@link COMPANY_DETACH_LIMIT} per pass and honest past it: the company\n * stands, and the person is told more remain. The unlink clears each\n * holder's facet that named the company, which is the server's to write\n * (AGL-2804). What this card adds is the confirm, the sentence for each\n * outcome, and leaving the page once the record is gone.\n */\nexport function CompanyPropertiesCard(props: CompanyPropertiesCardProps) {\n const {\n company,\n seed,\n hostId,\n org,\n crmScope,\n members,\n routes,\n onDeleted,\n } = props\n const { scope } = crmScope\n const callCrm = useCrmApi(hostId)\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n // The feed the act is logged in, decided by the level it was performed\n // at (AGL-2738): this site's under a site, the organization's at the org\n // hub, where a client-direct append to the company's own site would face\n // a gate the record write never had to pass.\n const logActivity = useCrmActivityLogger(hostId)\n const [editing, setEditing] = useState(false)\n const [deleting, setDeleting] = useState(false)\n // The org's company fields (AGL-2661): one row each under the fixed\n // ones, read by type the way the list's columns read them.\n const fields = useContactFieldDefinitions(crmScope.orgId, 'company')\n\n const handleDelete = useCallback(async () => {\n if (!scope || deleting) return\n const name = String(company.name ?? company.$id)\n const accepted = await confirm({\n title: 'Delete this company?',\n description:\n `\"${name}\" is removed from Companies and unlinked from every ` +\n 'contact at it. The contacts themselves are kept, and so is ' +\n 'anything else filed against the company.',\n confirmationText: 'Delete company',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating\n // on the resolved value alone would make this always return.\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setDeleting(true)\n try {\n const outcome = await deleteCompanyThroughRoute(callCrm, company.$id)\n if (!outcome.deleted) {\n enqueueSnackbar(\n `${COMPANY_DETACH_LIMIT.toLocaleString()} contacts were unlinked ` +\n `from \"${name}\" and more remain. Delete again to continue.`,\n { variant: 'warning' },\n )\n return\n }\n // Setup → Activity shows CRM work (AGL-2622): the company is org\n // data, but the act happened in this site's console and belongs in\n // its feed.\n logActivity('Deleted company', { type: 'company', id: company.$id, name })\n enqueueSnackbar(\n outcome.detached\n ? `Company deleted and unlinked from ${outcome.detached.toLocaleString()} ` +\n `contact${outcome.detached === 1 ? '' : 's'}`\n : 'Company deleted',\n { variant: 'success', persist: false },\n )\n onDeleted()\n } catch (error) {\n console.error(error)\n enqueueSnackbar(companyDeleteFailureMessage(error), {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setDeleting(false)\n }\n }, [scope, deleting, company, confirm, callCrm, logActivity, enqueueSnackbar, onDeleted])\n\n const address = formatAddress(company.address)\n // The domain, the industry and the owner read on the header — the\n // subtitle and the chip row — so the rows list what is left.\n const rows: Array<{ label: string; value: ReactNode }> = [\n {\n label: 'Website',\n value: company.website ? (\n <Link href={company.website} target=\"_blank\" rel=\"noreferrer noopener\">\n {company.website}\n </Link>\n ) : null,\n },\n // Callable in one tap (AGL-2661); plain text for a value no dialer takes.\n { label: 'Phone', value: <CrmPhoneLink phone={company.phone} /> },\n { label: 'Address', value: address },\n { label: 'Tags', value: (company.tags ?? []).join(', ') },\n { label: 'Notes', value: company.notes },\n ...fields.active.map((definition) => {\n const text = formatContactCustomValue(definition, company.custom?.[definition.key])\n return {\n label: definition.label || definition.key,\n value:\n text && definition.type === 'url' ? (\n <Link href={text} target=\"_blank\" rel=\"noreferrer noopener\">\n {text}\n </Link>\n ) : (\n text\n ),\n }\n }),\n ]\n\n return (\n <CrmRecordHeader\n kind=\"Company\"\n title={String(company.name || company.$id)}\n subtitle={company.domain}\n help={pluginDocsHelp('companies', { anchor: '#a-companys-page' })}\n backHref={routes.section('companies')}\n backLabel=\"Back to companies\"\n actions={\n <>\n <CrmCallButton\n hostId={hostId}\n org={org}\n link={{ companyId: company.$id }}\n phone={company.phone}\n />\n <Button\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPencilOutline.path} size={0.8} />}\n onClick={() => setEditing(true)}\n >\n {'Edit'}\n </Button>\n </>\n }\n menuItems={[\n {\n key: 'delete',\n label: deleting ? 'Deleting…' : 'Delete company',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !scope || deleting,\n disabledReason: deleting ? 'The company is being deleted' : 'The organization has not loaded',\n onClick: () => void handleDelete(),\n },\n ]}\n chips={\n <>\n <CrmRecordChip label=\"Industry\" value={company.industry} />\n <CrmRecordChip\n label=\"Owner\"\n value={\n company.ownerUid\n ? members.ready\n ? members.labelFor(company.ownerUid)\n : '…'\n : undefined\n }\n />\n </>\n }\n >\n <Stack spacing={1}>\n {rows.map((row) => (\n <Stack\n key={row.label}\n direction=\"row\"\n spacing={2}\n sx={{ alignItems: 'baseline' }}\n >\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ minWidth: 96, flexShrink: 0 }}\n >\n {row.label}\n </Typography>\n <Typography\n variant=\"body2\"\n color={row.value ? 'text.primary' : 'text.secondary'}\n sx={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}\n >\n {row.value || '—'}\n </Typography>\n </Stack>\n ))}\n </Stack>\n {editing ? (\n <CompanyEditDrawer\n open\n onClose={() => setEditing(false)}\n hostId={hostId}\n org={org}\n company={company}\n seed={seed}\n members={members}\n onSaved={() => setEditing(false)}\n />\n ) : null}\n </CrmRecordHeader>\n )\n}\nCompanyPropertiesCard.displayName = 'CompanyPropertiesCard'\n\nexport default CompanyPropertiesCard\n"],"names":["pluginDocsHelp","mdiDeleteOutline","mdiPencilOutline","MdiIcon","useConfirmationContext","useSnackbar","Button","Link","Stack","Typography","useCallback","useState","useContactFieldDefinitions","useCrmActivityLogger","COMPANY_DETACH_LIMIT","companyDeleteFailureMessage","deleteCompanyThroughRoute","CompanyEditDrawer","formatContactCustomValue","CrmCallButton","CrmPhoneLink","CrmRecordChip","CrmRecordHeader","useCrmApi","formatAddress","address","line1","line2","city","state","filter","Boolean","join","postalCode","country","map","part","String","trim","CompanyPropertiesCard","props","company","seed","hostId","org","crmScope","members","routes","onDeleted","scope","callCrm","confirm","enqueueSnackbar","logActivity","editing","setEditing","deleting","setDeleting","fields","orgId","handleDelete","name","$id","accepted","title","description","confirmationText","confirmationButtonProps","color","then","catch","outcome","deleted","toLocaleString","variant","type","id","detached","persist","error","console","allowDuplicate","rows","label","value","website","href","target","rel","phone","tags","notes","active","definition","text","custom","key","kind","subtitle","domain","help","anchor","backHref","section","backLabel","actions","link","companyId","size","startIcon","path","onClick","menuItems","icon","destructive","disabled","disabledReason","chips","industry","ownerUid","ready","labelFor","undefined","spacing","row","direction","sx","alignItems","minWidth","flexShrink","whiteSpace","overflowWrap","open","onClose","onSaved","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-properties-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n type AglynPostalAddress,\n type CrmCompany,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPencilOutline } from '@aglyn/shared-data-mdi'\nimport { MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Button, Link, Stack, Typography } from '@mui/material'\nimport { type ReactNode, useCallback, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport type { CrmScope } from '../hooks/use-crm-scope'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { COMPANY_DETACH_LIMIT } from '../model/companies'\nimport {\n companyDeleteFailureMessage,\n deleteCompanyThroughRoute,\n} from '../model/company-delete'\nimport type { CrmRoutes } from '../model/crm-routes'\nimport CompanyEditDrawer from './company-edit-drawer'\nimport { formatContactCustomValue } from './contact-custom-columns'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { useCrmApi } from './use-crm-api'\n\nexport interface CompanyPropertiesCardProps {\n company: Partial<CrmCompany> & { $id: string }\n /** Whether the document the card holds is server-confirmed. */\n seed: { fromCache: boolean; unreadable: boolean }\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling>\n crmScope: CrmScope\n members: OrgMemberOptions\n routes: CrmRoutes\n /** Called once the record is gone, so the page can leave it. */\n onDeleted: () => void\n}\n\n/** A postal address on one line, or nothing. */\nfunction formatAddress(address: AglynPostalAddress | null | undefined): string {\n if (!address) return ''\n return [\n address.line1,\n address.line2,\n [address.city, address.state].filter(Boolean).join(', '),\n address.postalCode,\n address.country,\n ]\n .map((part) => String(part ?? '').trim())\n .filter(Boolean)\n .join(' · ')\n}\n\n/**\n * WHAT A COMPANY IS: its fields, read off the one document the page holds,\n * with the edit and the delete beside them (AGL-2597).\n *\n * ## Edit is the create drawer\n *\n * The same form the list creates with, opened on the record. One form means\n * one set of refusals — a domain that is not a domain is refused the same\n * way on day one and on a rename — and one place to add a field.\n *\n * ## Delete is a DETACH first\n *\n * `crm/company-delete` — the route the bulk bar calls per row as well —\n * unlinks the contacts that name this company before removing it, bounded\n * at {@link COMPANY_DETACH_LIMIT} per pass and honest past it: the company\n * stands, and the person is told more remain. The unlink clears each\n * holder's facet that named the company, which is the server's to write\n * (AGL-2804). What this card adds is the confirm, the sentence for each\n * outcome, and leaving the page once the record is gone.\n */\nexport function CompanyPropertiesCard(props: CompanyPropertiesCardProps) {\n const {\n company,\n seed,\n hostId,\n org,\n crmScope,\n members,\n routes,\n onDeleted,\n } = props\n const { scope } = crmScope\n const callCrm = useCrmApi(hostId)\n const { confirm } = useConfirmationContext()\n const { enqueueSnackbar } = useSnackbar()\n // The feed the act is logged in, decided by the level it was performed\n // at (AGL-2738): this site's under a site, the organization's at the org\n // hub, where a client-direct append to the company's own site would face\n // a gate the record write never had to pass.\n const logActivity = useCrmActivityLogger(hostId)\n const [editing, setEditing] = useState(false)\n const [deleting, setDeleting] = useState(false)\n // The org's company fields (AGL-2661): one row each under the fixed\n // ones, read by type the way the list's columns read them.\n const fields = useContactFieldDefinitions(crmScope.orgId, 'company')\n\n const handleDelete = useCallback(async () => {\n if (!scope || deleting) return\n const name = String(company.name ?? company.$id)\n const accepted = await confirm({\n title: 'Delete this company?',\n description:\n `\"${name}\" is removed from Companies and unlinked from every ` +\n 'contact at it. The contacts themselves are kept, and so is ' +\n 'anything else filed against the company.',\n confirmationText: 'Delete company',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating\n // on the resolved value alone would make this always return.\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setDeleting(true)\n try {\n const outcome = await deleteCompanyThroughRoute(callCrm, company.$id)\n if (!outcome.deleted) {\n enqueueSnackbar(\n `${COMPANY_DETACH_LIMIT.toLocaleString()} contacts were unlinked ` +\n `from \"${name}\" and more remain. Delete again to continue.`,\n { variant: 'warning' },\n )\n return\n }\n // Setup → Activity shows CRM work (AGL-2622): the company is org\n // data, but the act happened in this site's console and belongs in\n // its feed.\n logActivity('Deleted company', { type: 'company', id: company.$id, name })\n enqueueSnackbar(\n outcome.detached\n ? `Company deleted and unlinked from ${outcome.detached.toLocaleString()} ` +\n `contact${outcome.detached === 1 ? '' : 's'}`\n : 'Company deleted',\n { variant: 'success', persist: false },\n )\n onDeleted()\n } catch (error) {\n console.error(error)\n enqueueSnackbar(companyDeleteFailureMessage(error), {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setDeleting(false)\n }\n }, [scope, deleting, company, confirm, callCrm, logActivity, enqueueSnackbar, onDeleted])\n\n const address = formatAddress(company.address)\n // The domain, the industry and the owner read on the header — the\n // subtitle and the chip row — so the rows list what is left.\n const rows: Array<{ label: string; value: ReactNode }> = [\n {\n label: 'Website',\n value: company.website ? (\n <Link href={company.website} target=\"_blank\" rel=\"noreferrer noopener\">\n {company.website}\n </Link>\n ) : null,\n },\n // Callable in one tap (AGL-2661); plain text for a value no dialer takes.\n { label: 'Phone', value: <CrmPhoneLink phone={company.phone} /> },\n { label: 'Address', value: address },\n { label: 'Tags', value: (company.tags ?? []).join(', ') },\n { label: 'Notes', value: company.notes },\n ]\n // The org's own company fields (AGL-2661), after the built-in ones under\n // \"More fields\", as a record's Details keep them; none defined, none drawn.\n const moreRows: Array<{ label: string; value: ReactNode }> = fields.active.map(\n (definition) => {\n const text = formatContactCustomValue(definition, company.custom?.[definition.key])\n return {\n label: definition.label || definition.key,\n value:\n text && definition.type === 'url' ? (\n <Link href={text} target=\"_blank\" rel=\"noreferrer noopener\">\n {text}\n </Link>\n ) : (\n text\n ),\n }\n },\n )\n const factRows = (list: Array<{ label: string; value: ReactNode }>) =>\n list.map((row) => (\n <Stack\n key={row.label}\n direction=\"row\"\n spacing={2}\n sx={{ alignItems: 'baseline' }}\n >\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ minWidth: 96, flexShrink: 0 }}\n >\n {row.label}\n </Typography>\n <Typography\n variant=\"body2\"\n color={row.value ? 'text.primary' : 'text.secondary'}\n sx={{ whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }}\n >\n {row.value || '—'}\n </Typography>\n </Stack>\n ))\n\n return (\n <CrmRecordHeader\n kind=\"Company\"\n title={String(company.name || company.$id)}\n subtitle={company.domain}\n help={pluginDocsHelp('companies', { anchor: '#a-companys-page' })}\n backHref={routes.section('companies')}\n backLabel=\"Back to companies\"\n actions={\n <>\n <CrmCallButton\n hostId={hostId}\n org={org}\n link={{ companyId: company.$id }}\n phone={company.phone}\n />\n <Button\n size=\"small\"\n color=\"primary\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPencilOutline.path} size={0.8} />}\n onClick={() => setEditing(true)}\n >\n {'Edit'}\n </Button>\n </>\n }\n menuItems={[\n {\n key: 'delete',\n label: deleting ? 'Deleting…' : 'Delete company',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !scope || deleting,\n disabledReason: deleting ? 'The company is being deleted' : 'The organization has not loaded',\n onClick: () => void handleDelete(),\n },\n ]}\n chips={\n <>\n <CrmRecordChip label=\"Industry\" value={company.industry} />\n <CrmRecordChip\n label=\"Owner\"\n value={\n company.ownerUid\n ? members.ready\n ? members.labelFor(company.ownerUid)\n : '…'\n : undefined\n }\n />\n </>\n }\n >\n <Stack spacing={1}>\n {factRows(rows)}\n {moreRows.length ? (\n <>\n <Typography variant=\"subtitle2\" sx={{ pt: 1 }}>\n {'More fields'}\n </Typography>\n {factRows(moreRows)}\n </>\n ) : null}\n </Stack>\n {editing ? (\n <CompanyEditDrawer\n open\n onClose={() => setEditing(false)}\n hostId={hostId}\n org={org}\n company={company}\n seed={seed}\n members={members}\n onSaved={() => setEditing(false)}\n />\n ) : null}\n </CrmRecordHeader>\n )\n}\nCompanyPropertiesCard.displayName = 'CompanyPropertiesCard'\n\nexport default CompanyPropertiesCard\n"],"names":["pluginDocsHelp","mdiDeleteOutline","mdiPencilOutline","MdiIcon","useConfirmationContext","useSnackbar","Button","Link","Stack","Typography","useCallback","useState","useContactFieldDefinitions","useCrmActivityLogger","COMPANY_DETACH_LIMIT","companyDeleteFailureMessage","deleteCompanyThroughRoute","CompanyEditDrawer","formatContactCustomValue","CrmCallButton","CrmPhoneLink","CrmRecordChip","CrmRecordHeader","useCrmApi","formatAddress","address","line1","line2","city","state","filter","Boolean","join","postalCode","country","map","part","String","trim","CompanyPropertiesCard","props","company","seed","hostId","org","crmScope","members","routes","onDeleted","scope","callCrm","confirm","enqueueSnackbar","logActivity","editing","setEditing","deleting","setDeleting","fields","orgId","handleDelete","name","$id","accepted","title","description","confirmationText","confirmationButtonProps","color","then","catch","outcome","deleted","toLocaleString","variant","type","id","detached","persist","error","console","allowDuplicate","rows","label","value","website","href","target","rel","phone","tags","notes","moreRows","active","definition","text","custom","key","factRows","list","row","direction","spacing","sx","alignItems","minWidth","flexShrink","whiteSpace","overflowWrap","kind","subtitle","domain","help","anchor","backHref","section","backLabel","actions","link","companyId","size","startIcon","path","onClick","menuItems","icon","destructive","disabled","disabledReason","chips","industry","ownerUid","ready","labelFor","undefined","length","pt","open","onClose","onSaved","displayName"],"mappings":"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"}
@@ -45,8 +45,9 @@ export interface ContactAssociationsCardProps {
45
45
  * lists and each email's own picker, and the helper says so where a
46
46
  * reader would otherwise assume the opposite.
47
47
  *
48
- * The filing is the one editable thing here and has its own Save, through
49
- * the same stale-seed guard the properties card uses: a campaign membership
48
+ * The filing is the one editable thing here and has its own **Save filing**
49
+ * in the card header, through the same stale-seed guard the properties card
50
+ * uses: a campaign membership
50
51
  * written over a cached read could revert somebody else's filing. The save
51
52
  * goes to `crm/contact-update` (AGL-2804), because the filing is this
52
53
  * holder's facet and a facet is the server's to write; like every field that
@@ -54,8 +54,9 @@ import { useCrmCampaigns } from "../hooks/use-crm-campaigns.js";
54
54
  * lists and each email's own picker, and the helper says so where a
55
55
  * reader would otherwise assume the opposite.
56
56
  *
57
- * The filing is the one editable thing here and has its own Save, through
58
- * the same stale-seed guard the properties card uses: a campaign membership
57
+ * The filing is the one editable thing here and has its own **Save filing**
58
+ * in the card header, through the same stale-seed guard the properties card
59
+ * uses: a campaign membership
59
60
  * written over a cached read could revert somebody else's filing. The save
60
61
  * goes to `crm/contact-update` (AGL-2804), because the filing is this
61
62
  * holder's facet and a facet is the server's to write; like every field that
@@ -181,6 +182,15 @@ import { useCrmCampaigns } from "../hooks/use-crm-campaigns.js";
181
182
  }),
182
183
  contentGutterX: true,
183
184
  contentGutterY: true,
185
+ HeaderProps: {
186
+ action: /*#__PURE__*/ _jsx(Button, {
187
+ size: "small",
188
+ variant: "outlined",
189
+ disabled: saving || siteCampaigns.ready && !siteCampaigns.options.length,
190
+ onClick: ()=>void handleSaveFiling(),
191
+ children: saving ? 'Saving…' : 'Save filing'
192
+ })
193
+ },
184
194
  children: /*#__PURE__*/ _jsxs(Stack, {
185
195
  spacing: 2,
186
196
  children: [
@@ -296,16 +306,6 @@ import { useCrmCampaigns } from "../hooks/use-crm-campaigns.js";
296
306
  helperText: "Your own filing. It never adds anyone to a send — a campaign mails its lists.",
297
307
  empty: siteCampaigns.ready && !siteCampaigns.options.length,
298
308
  emptyText: hostId ? 'This site has no campaigns yet.' : 'There are no campaigns yet.'
299
- }),
300
- /*#__PURE__*/ _jsx(Stack, {
301
- direction: "row",
302
- children: /*#__PURE__*/ _jsx(Button, {
303
- size: "small",
304
- variant: "outlined",
305
- disabled: saving || siteCampaigns.ready && !siteCampaigns.options.length,
306
- onClick: ()=>void handleSaveFiling(),
307
- children: saving ? 'Saving…' : 'Save filing'
308
- })
309
309
  })
310
310
  ]
311
311
  })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-associations-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n CONTACT_SOURCE_LABELS,\n type ConsentGroup,\n type ContactSource,\n MARKETING_BASIS_LABELS,\n readMarketingBasis,\n} from '@aglyn/aglyn'\nimport { personKeyInBrowser } from '../model/person-key-web'\n/*\n * The component path and NOT the marketing barrel: that barrel is the entry\n * point the tenant's loader imports to activate the plugin's SITE half, so a\n * console card named there ships to every published page.\n */\nimport { CrmRecordAttributionZone } from './crm-attribution-zone'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Divider, Stack, Typography } from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport type { ContactRecord } from '../model/contact-record'\nimport { crmRoutes } from '../model/crm-routes'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmCampaigns } from '../hooks/use-crm-campaigns'\n\nexport interface ContactAssociationsCardProps {\n /**\n * The site whose lead, campaigns and attribution are shown — the mounted\n * site, or at the organization level the holder's own; `null` for a row\n * no site has captured, when none of the three has an address.\n */\n hostId: string | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /** The document as read, for the consent fields the projection leaves out. */\n row: Record<string, unknown>\n consentGroup: ConsentGroup\n /** The listener's verdict on the row, for the stale-seed guard. */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The hub's own path, for the link to this person's lead. */\n basePath: string\n /**\n * The site's Bookings page narrowed to this person's address (AGL-2660),\n * or nothing where the site has no booking door — the page decides, and\n * the row is simply not drawn.\n */\n bookingsHref?: string | null\n}\n\n/**\n * WHERE THIS PERSON CAME FROM AND WHAT THEY ARE FILED UNDER (AGL-2596).\n *\n * Four facts that were in the v1 drawer and are not properties of the\n * person so much as of the RELATIONSHIP:\n *\n * - The SOURCES say which mechanism created the record — a form, a\n * checkout, an import.\n * - The ATTRIBUTION says which campaign led to it, recorded from the link\n * they followed and never editable. One keyed read, paid on opening the\n * page rather than per row of the list.\n * - The CONSENT is the basis this controller holds for marketing email:\n * opted in, opted out, or no record — three states that never collapse\n * into two, because \"no record\" is the commonest and is not a refusal.\n * - The FILING is which campaigns the team has PUT them in, a working set\n * kept by hand. It adds nobody to a send: a campaign's audience is its\n * lists and each email's own picker, and the helper says so where a\n * reader would otherwise assume the opposite.\n *\n * The filing is the one editable thing here and has its own Save, through\n * the same stale-seed guard the properties card uses: a campaign membership\n * written over a cached read could revert somebody else's filing. The save\n * goes to `crm/contact-update` (AGL-2804), because the filing is this\n * holder's facet and a facet is the server's to write; like every field that\n * route writes, it is refused to a plan without the CRM (AGL-2851).\n *\n * ## The lead (AGL-2612)\n *\n * A LEAD is the sales record a lead surface — a sign-up, a booking, a form\n * with lead routing — files beside the contact, under the capturing site at\n * `hosts/{hostId}/leads/{personKey}`. The lead's page links the contact\n * once converted; nothing linked back, so the two halves of one person\n * read as unrelated. One `getDoc` by the key the capture derives — the\n * address, normalized and hashed, computed here on WebCrypto — is what\n * answers \"does this site hold a lead for this person\", and it is a read\n * this page pays because opening the record IS the ask. Absence is a fact\n * too: a contact captured by an order or an import has no lead, and the\n * chip is simply not there.\n */\nexport function ContactAssociationsCard(props: ContactAssociationsCardProps) {\n const { hostId, record, row, consentGroup, seed, basePath, bookingsHref } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n const routes = crmRoutes(basePath)\n\n const [leadKey, setLeadKey] = useState<string | null>(null)\n const email = record.email\n useEffect(() => {\n let active = true\n setLeadKey(null)\n if (!hostId || !orgId) return undefined\n void (async () => {\n const key = await personKeyInBrowser(email)\n if (!key || !active) return\n const snapshot = await getDoc(\n doc(firestore, 'orgs', orgId, 'leads', key),\n ).catch(() => null)\n if (active && snapshot?.exists()) setLeadKey(key)\n })().catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, hostId, email])\n\n const [campaigns, setCampaigns] = useState<string[]>(record.campaignIds)\n const [saving, setSaving] = useState(false)\n const recordId = record.$id\n useEffect(() => {\n setCampaigns(record.campaignIds)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n /*\n * The campaigns, read because this page IS the ask: one picker on one\n * record, and nothing in the list ever paid for the campaign list. Under\n * a site, the ones placed on it; at the organization level, every one.\n */\n const siteCampaigns = useCrmCampaigns({ hostId, orgId }, { enabled: true })\n\n const consent = useMemo(\n () => readMarketingBasis(row, consentGroup),\n [row, consentGroup],\n )\n\n const handleSaveFiling = useCallback(async () => {\n setSaving(true)\n try {\n const verdict = await writeGuardedBySeed(\n {\n subject: 'contact',\n unreadable: seed.status === 'error',\n fromCache: seed.fromCache,\n },\n // An empty selection is sent as an empty list rather than left out,\n // so \"filed under no campaign\" has one shape here and in the pass\n // that detaches a deleted campaign.\n () =>\n contactUpdate.updateOne(record.$id, {\n campaignIds: Aglyn.campaignMembershipValue(campaigns),\n }),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n } finally {\n setSaving(false)\n }\n }, [campaigns, contactUpdate, enqueueSnackbar, record.$id, seed.fromCache, seed.status])\n\n const sources = Object.keys(record.sources ?? {}) as ContactSource[]\n const consentDetail = [\n consent.basisAtMs\n ? `recorded ${new Date(consent.basisAtMs).toLocaleDateString()}`\n : null,\n consent.assertedBy === 'operator' ? 'entered by your team' : null,\n consent.otherGrant === 'other-host'\n ? 'opted in to another site in this workspace'\n : null,\n ].filter(Boolean)\n\n return (\n <CardDisplay\n header={'Relationship'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#what-each-site-keeps-to-itself' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Sources'}</Typography>\n {sources.length || leadKey ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip\n key={source}\n label={CONTACT_SOURCE_LABELS[source] ?? source}\n size=\"small\"\n />\n ))}\n {/*\n This site's lead for the person, when it holds one — a link,\n in the shape the lead's page uses to point back here.\n */}\n {leadKey ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.lead(leadKey)}\n size=\"small\"\n variant=\"outlined\"\n sx={{ py: 0, minHeight: 24, lineHeight: 1.5 }}\n >\n {'Lead on this site'}\n </Button>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No capture recorded.'}\n </Typography>\n )}\n </Stack>\n {/*\n The person's bookings on this site (AGL-2660): every booking\n taken with their address, past ones too, on the Bookings page —\n the appointments are that page's records, and this card only\n points at them.\n */}\n {bookingsHref ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Bookings'}</Typography>\n <Typography variant=\"body2\">\n <AppLink href={bookingsHref}>{'Bookings on this site'}</AppLink>\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Every booking made with this address, upcoming and past.'}\n </Typography>\n </Stack>\n ) : null}\n {hostId ? (\n <Stack spacing={0.5}>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"contact\"\n recordId={record.$id}\n />\n {/* Attribution is the campaign that brought the person; the\n picker below is the team's own filing (AGL-3274). */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Filing is separate — see Filed under campaigns.'}\n </Typography>\n </Stack>\n ) : null}\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Marketing email'}</Typography>\n <Typography variant=\"body2\">\n {MARKETING_BASIS_LABELS[consent.basis]}\n {consentDetail.length ? ` · ${consentDetail.join(' · ')}` : ''}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {consent.basis === 'unrecorded'\n ? 'Nothing was recorded either way — not a refusal, and not permission.'\n : `The basis this ${\n consentGroup.declared ? 'group' : 'site'\n } holds. Other sites in the workspace keep their own.`}\n </Typography>\n </Stack>\n <Divider />\n <CampaignPicker\n options={siteCampaigns.options}\n value={campaigns}\n onChange={setCampaigns}\n label=\"Filed under campaigns\"\n helperText=\"Your own filing. It never adds anyone to a send — a campaign mails its lists.\"\n empty={siteCampaigns.ready && !siteCampaigns.options.length}\n emptyText={hostId ? 'This site has no campaigns yet.' : 'There are no campaigns yet.'}\n />\n <Stack direction=\"row\">\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || (siteCampaigns.ready && !siteCampaigns.options.length)}\n onClick={() => void handleSaveFiling()}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n </Stack>\n </Stack>\n </CardDisplay>\n )\n}\nContactAssociationsCard.displayName = 'ContactAssociationsCard'\n\nexport default ContactAssociationsCard\n"],"names":["Aglyn","CONTACT_SOURCE_LABELS","MARKETING_BASIS_LABELS","readMarketingBasis","personKeyInBrowser","CrmRecordAttributionZone","CampaignPicker","AppLink","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Chip","Divider","Stack","Typography","doc","getDoc","useCallback","useEffect","useMemo","useState","useContactUpdate","crmRoutes","useCrmScope","useCrmCampaigns","ContactAssociationsCard","props","record","hostId","row","consentGroup","seed","basePath","bookingsHref","orgId","firestore","enqueueSnackbar","contactUpdate","routes","leadKey","setLeadKey","email","active","undefined","key","snapshot","catch","exists","campaigns","setCampaigns","campaignIds","saving","setSaving","recordId","$id","siteCampaigns","enabled","consent","handleSaveFiling","verdict","subject","unreadable","status","fromCache","updateOne","campaignMembershipValue","ok","message","variant","persist","error","console","Error","allowDuplicate","sources","Object","keys","consentDetail","basisAtMs","Date","toLocaleDateString","assertedBy","otherGrant","filter","Boolean","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","spacing","length","direction","sx","flexWrap","rowGap","map","source","label","size","component","componentVariant","nativeButton","href","lead","py","minHeight","lineHeight","color","recordKind","basis","join","declared","options","value","onChange","helperText","empty","ready","emptyText","disabled","onClick","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-associations-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n CONTACT_SOURCE_LABELS,\n type ConsentGroup,\n type ContactSource,\n MARKETING_BASIS_LABELS,\n readMarketingBasis,\n} from '@aglyn/aglyn'\nimport { personKeyInBrowser } from '../model/person-key-web'\n/*\n * The component path and NOT the marketing barrel: that barrel is the entry\n * point the tenant's loader imports to activate the plugin's SITE half, so a\n * console card named there ships to every published page.\n */\nimport { CrmRecordAttributionZone } from './crm-attribution-zone'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Divider, Stack, Typography } from '@mui/material'\nimport { doc, getDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport type { ContactRecord } from '../model/contact-record'\nimport { crmRoutes } from '../model/crm-routes'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmCampaigns } from '../hooks/use-crm-campaigns'\n\nexport interface ContactAssociationsCardProps {\n /**\n * The site whose lead, campaigns and attribution are shown — the mounted\n * site, or at the organization level the holder's own; `null` for a row\n * no site has captured, when none of the three has an address.\n */\n hostId: string | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /** The document as read, for the consent fields the projection leaves out. */\n row: Record<string, unknown>\n consentGroup: ConsentGroup\n /** The listener's verdict on the row, for the stale-seed guard. */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The hub's own path, for the link to this person's lead. */\n basePath: string\n /**\n * The site's Bookings page narrowed to this person's address (AGL-2660),\n * or nothing where the site has no booking door — the page decides, and\n * the row is simply not drawn.\n */\n bookingsHref?: string | null\n}\n\n/**\n * WHERE THIS PERSON CAME FROM AND WHAT THEY ARE FILED UNDER (AGL-2596).\n *\n * Four facts that were in the v1 drawer and are not properties of the\n * person so much as of the RELATIONSHIP:\n *\n * - The SOURCES say which mechanism created the record — a form, a\n * checkout, an import.\n * - The ATTRIBUTION says which campaign led to it, recorded from the link\n * they followed and never editable. One keyed read, paid on opening the\n * page rather than per row of the list.\n * - The CONSENT is the basis this controller holds for marketing email:\n * opted in, opted out, or no record — three states that never collapse\n * into two, because \"no record\" is the commonest and is not a refusal.\n * - The FILING is which campaigns the team has PUT them in, a working set\n * kept by hand. It adds nobody to a send: a campaign's audience is its\n * lists and each email's own picker, and the helper says so where a\n * reader would otherwise assume the opposite.\n *\n * The filing is the one editable thing here and has its own **Save filing**\n * in the card header, through the same stale-seed guard the properties card\n * uses: a campaign membership\n * written over a cached read could revert somebody else's filing. The save\n * goes to `crm/contact-update` (AGL-2804), because the filing is this\n * holder's facet and a facet is the server's to write; like every field that\n * route writes, it is refused to a plan without the CRM (AGL-2851).\n *\n * ## The lead (AGL-2612)\n *\n * A LEAD is the sales record a lead surface — a sign-up, a booking, a form\n * with lead routing — files beside the contact, under the capturing site at\n * `hosts/{hostId}/leads/{personKey}`. The lead's page links the contact\n * once converted; nothing linked back, so the two halves of one person\n * read as unrelated. One `getDoc` by the key the capture derives — the\n * address, normalized and hashed, computed here on WebCrypto — is what\n * answers \"does this site hold a lead for this person\", and it is a read\n * this page pays because opening the record IS the ask. Absence is a fact\n * too: a contact captured by an order or an import has no lead, and the\n * chip is simply not there.\n */\nexport function ContactAssociationsCard(props: ContactAssociationsCardProps) {\n const { hostId, record, row, consentGroup, seed, basePath, bookingsHref } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n const routes = crmRoutes(basePath)\n\n const [leadKey, setLeadKey] = useState<string | null>(null)\n const email = record.email\n useEffect(() => {\n let active = true\n setLeadKey(null)\n if (!hostId || !orgId) return undefined\n void (async () => {\n const key = await personKeyInBrowser(email)\n if (!key || !active) return\n const snapshot = await getDoc(\n doc(firestore, 'orgs', orgId, 'leads', key),\n ).catch(() => null)\n if (active && snapshot?.exists()) setLeadKey(key)\n })().catch(() => undefined)\n return () => {\n active = false\n }\n }, [firestore, hostId, email])\n\n const [campaigns, setCampaigns] = useState<string[]>(record.campaignIds)\n const [saving, setSaving] = useState(false)\n const recordId = record.$id\n useEffect(() => {\n setCampaigns(record.campaignIds)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n /*\n * The campaigns, read because this page IS the ask: one picker on one\n * record, and nothing in the list ever paid for the campaign list. Under\n * a site, the ones placed on it; at the organization level, every one.\n */\n const siteCampaigns = useCrmCampaigns({ hostId, orgId }, { enabled: true })\n\n const consent = useMemo(\n () => readMarketingBasis(row, consentGroup),\n [row, consentGroup],\n )\n\n const handleSaveFiling = useCallback(async () => {\n setSaving(true)\n try {\n const verdict = await writeGuardedBySeed(\n {\n subject: 'contact',\n unreadable: seed.status === 'error',\n fromCache: seed.fromCache,\n },\n // An empty selection is sent as an empty list rather than left out,\n // so \"filed under no campaign\" has one shape here and in the pass\n // that detaches a deleted campaign.\n () =>\n contactUpdate.updateOne(record.$id, {\n campaignIds: Aglyn.campaignMembershipValue(campaigns),\n }),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n } finally {\n setSaving(false)\n }\n }, [campaigns, contactUpdate, enqueueSnackbar, record.$id, seed.fromCache, seed.status])\n\n const sources = Object.keys(record.sources ?? {}) as ContactSource[]\n const consentDetail = [\n consent.basisAtMs\n ? `recorded ${new Date(consent.basisAtMs).toLocaleDateString()}`\n : null,\n consent.assertedBy === 'operator' ? 'entered by your team' : null,\n consent.otherGrant === 'other-host'\n ? 'opted in to another site in this workspace'\n : null,\n ].filter(Boolean)\n\n return (\n <CardDisplay\n header={'Relationship'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#what-each-site-keeps-to-itself' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || (siteCampaigns.ready && !siteCampaigns.options.length)}\n onClick={() => void handleSaveFiling()}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Sources'}</Typography>\n {sources.length || leadKey ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip\n key={source}\n label={CONTACT_SOURCE_LABELS[source] ?? source}\n size=\"small\"\n />\n ))}\n {/*\n This site's lead for the person, when it holds one — a link,\n in the shape the lead's page uses to point back here.\n */}\n {leadKey ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.lead(leadKey)}\n size=\"small\"\n variant=\"outlined\"\n sx={{ py: 0, minHeight: 24, lineHeight: 1.5 }}\n >\n {'Lead on this site'}\n </Button>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No capture recorded.'}\n </Typography>\n )}\n </Stack>\n {/*\n The person's bookings on this site (AGL-2660): every booking\n taken with their address, past ones too, on the Bookings page —\n the appointments are that page's records, and this card only\n points at them.\n */}\n {bookingsHref ? (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Bookings'}</Typography>\n <Typography variant=\"body2\">\n <AppLink href={bookingsHref}>{'Bookings on this site'}</AppLink>\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Every booking made with this address, upcoming and past.'}\n </Typography>\n </Stack>\n ) : null}\n {hostId ? (\n <Stack spacing={0.5}>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"contact\"\n recordId={record.$id}\n />\n {/* Attribution is the campaign that brought the person; the\n picker below is the team's own filing (AGL-3274). */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Filing is separate — see Filed under campaigns.'}\n </Typography>\n </Stack>\n ) : null}\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\">{'Marketing email'}</Typography>\n <Typography variant=\"body2\">\n {MARKETING_BASIS_LABELS[consent.basis]}\n {consentDetail.length ? ` · ${consentDetail.join(' · ')}` : ''}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {consent.basis === 'unrecorded'\n ? 'Nothing was recorded either way — not a refusal, and not permission.'\n : `The basis this ${\n consentGroup.declared ? 'group' : 'site'\n } holds. Other sites in the workspace keep their own.`}\n </Typography>\n </Stack>\n <Divider />\n <CampaignPicker\n options={siteCampaigns.options}\n value={campaigns}\n onChange={setCampaigns}\n label=\"Filed under campaigns\"\n helperText=\"Your own filing. It never adds anyone to a send — a campaign mails its lists.\"\n empty={siteCampaigns.ready && !siteCampaigns.options.length}\n emptyText={hostId ? 'This site has no campaigns yet.' : 'There are no campaigns yet.'}\n />\n </Stack>\n </CardDisplay>\n )\n}\nContactAssociationsCard.displayName = 'ContactAssociationsCard'\n\nexport default ContactAssociationsCard\n"],"names":["Aglyn","CONTACT_SOURCE_LABELS","MARKETING_BASIS_LABELS","readMarketingBasis","personKeyInBrowser","CrmRecordAttributionZone","CampaignPicker","AppLink","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Chip","Divider","Stack","Typography","doc","getDoc","useCallback","useEffect","useMemo","useState","useContactUpdate","crmRoutes","useCrmScope","useCrmCampaigns","ContactAssociationsCard","props","record","hostId","row","consentGroup","seed","basePath","bookingsHref","orgId","firestore","enqueueSnackbar","contactUpdate","routes","leadKey","setLeadKey","email","active","undefined","key","snapshot","catch","exists","campaigns","setCampaigns","campaignIds","saving","setSaving","recordId","$id","siteCampaigns","enabled","consent","handleSaveFiling","verdict","subject","unreadable","status","fromCache","updateOne","campaignMembershipValue","ok","message","variant","persist","error","console","Error","allowDuplicate","sources","Object","keys","consentDetail","basisAtMs","Date","toLocaleDateString","assertedBy","otherGrant","filter","Boolean","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","disabled","ready","options","length","onClick","spacing","direction","sx","flexWrap","rowGap","map","source","label","component","componentVariant","nativeButton","href","lead","py","minHeight","lineHeight","color","recordKind","basis","join","declared","value","onChange","helperText","empty","emptyText","displayName"],"mappings":"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"}
@@ -25,9 +25,10 @@ import { type CrmDetailPageProps } from '../model/crm-routes';
25
25
  * ## The cards
26
26
  *
27
27
  * Each in its own file, so a card can grow without this page growing:
28
- * properties (the editable profile), the relationship (sources, attribution,
29
- * consent, campaign filing) and the timeline. Other CRM sections add a card
30
- * here by inserting one import and one line.
28
+ * properties (the editable profile, with the org's custom fields under it),
29
+ * the relationship (sources, attribution, consent, campaign filing) and the
30
+ * timeline. Other CRM sections add a card here by inserting one import and
31
+ * one line.
31
32
  *
32
33
  * ## Delete is a detach
33
34
  *
@@ -21,7 +21,9 @@ import { CONTACT_LIFECYCLE_STAGE_LABELS, pluginDocsHelp } from "@aglyn/aglyn";
21
21
  import { mdiDeleteOutline, mdiMerge } from "@aglyn/shared-data-mdi";
22
22
  import { AppLink, MdiIcon, useConfirmationContext } from "@aglyn/shared-ui-jsx";
23
23
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
24
- import { useFirestore, useFirestoreDoc } from "@aglyn/tenant-feature-instance";
24
+ import { useFirestore, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
25
+ import { restampCrmListFields } from "../model/list-fields-api.js";
26
+ import { crmTaskCallScope } from "../model/task-routes.js";
25
27
  import { Stack, Tooltip, Typography } from "@mui/material";
26
28
  import { arrayRemove, deleteDoc, deleteField, doc, updateDoc } from "firebase/firestore";
27
29
  import { useParams, useRouter } from "next/navigation";
@@ -32,7 +34,6 @@ import { useCrmScope } from "../hooks/use-crm-scope.js";
32
34
  import { contactPrimaryGroup, contactRecordFromDoc } from "../model/contact-record.js";
33
35
  import { crmRoutes } from "../model/crm-routes.js";
34
36
  import ContactAssociationsCard from "./contact-associations-card.js";
35
- import ContactCustomFieldsCard from "./contact-custom-fields-card.js";
36
37
  import ContactDuplicatesCard from "./contact-duplicates-card.js";
37
38
  import ContactKnownByCard from "./contact-known-by-card.js";
38
39
  import ContactMergeDialog, { useContactMergeDialog } from "./contact-merge-dialog.js";
@@ -82,9 +83,10 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
82
83
  * ## The cards
83
84
  *
84
85
  * Each in its own file, so a card can grow without this page growing:
85
- * properties (the editable profile), the relationship (sources, attribution,
86
- * consent, campaign filing) and the timeline. Other CRM sections add a card
87
- * here by inserting one import and one line.
86
+ * properties (the editable profile, with the org's custom fields under it),
87
+ * the relationship (sources, attribution, consent, campaign filing) and the
88
+ * timeline. Other CRM sections add a card here by inserting one import and
89
+ * one line.
88
90
  *
89
91
  * ## Delete is a detach
90
92
  *
@@ -199,6 +201,7 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
199
201
  emailsHub,
200
202
  hostId
201
203
  ]);
204
+ const { data: user } = useUser();
202
205
  const handleRemove = useCallback(async ()=>{
203
206
  var _ref;
204
207
  if (!row || !scope) return;
@@ -225,6 +228,12 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
225
228
  capturedByHostIds: arrayRemove(...plan.removeHostIds),
226
229
  updatedAt: new Date()
227
230
  }));
231
+ // The facet and the scope just let go of are what the Contacts list
232
+ // filters and searches by; the rules keep a client from restamping
233
+ // them, so the route does (AGL-3321).
234
+ await restampCrmListFields(user, crmTaskCallScope(hostId, orgId), 'contacts', [
235
+ id
236
+ ]);
228
237
  }
229
238
  // `siteLabel` and not "this site": at the organization level the line
230
239
  // lands in a feed that spans every site, so it has to name the one.
@@ -257,7 +266,10 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
257
266
  routes,
258
267
  row,
259
268
  scope,
260
- siteLabel
269
+ siteLabel,
270
+ user,
271
+ hostId,
272
+ orgId
261
273
  ]);
262
274
  const overflowItems = [
263
275
  /*
@@ -456,12 +468,6 @@ const contactDocsHelp = pluginDocsHelp('contacts', {
456
468
  basePath: basePath,
457
469
  bookingsHref: bookingsHref
458
470
  }),
459
- suiteIncluded ? /*#__PURE__*/ _jsx(ContactCustomFieldsCard, {
460
- hostId: hostId,
461
- org: org,
462
- contactId: id,
463
- basePath: basePath
464
- }) : null,
465
471
  /*#__PURE__*/ _jsx(ContactTimelineCard, {
466
472
  hostId: hostId,
467
473
  org: org,