@aglyn/plugins-crm 1.0.0-beta.218 → 1.0.0-beta.219

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 (249) hide show
  1. package/package.json +14 -13
  2. package/src/index.d.ts +1 -0
  3. package/src/index.js.map +1 -1
  4. package/src/lib/components/companies-bulk-bar.js +12 -4
  5. package/src/lib/components/companies-bulk-bar.js.map +1 -1
  6. package/src/lib/components/company-detail-page.js +11 -0
  7. package/src/lib/components/company-detail-page.js.map +1 -1
  8. package/src/lib/components/company-edit-drawer.js +12 -2
  9. package/src/lib/components/company-edit-drawer.js.map +1 -1
  10. package/src/lib/components/company-import-drawer.d.ts +1 -1
  11. package/src/lib/components/company-picker.js +10 -3
  12. package/src/lib/components/company-picker.js.map +1 -1
  13. package/src/lib/components/contact-detail-page.d.ts +4 -4
  14. package/src/lib/components/contact-detail-page.js +50 -53
  15. package/src/lib/components/contact-detail-page.js.map +1 -1
  16. package/src/lib/components/contact-list-columns.d.ts +3 -0
  17. package/src/lib/components/contact-list-columns.js +21 -5
  18. package/src/lib/components/contact-list-columns.js.map +1 -1
  19. package/src/lib/components/contact-merge-dialog.js +2 -1
  20. package/src/lib/components/contact-merge-dialog.js.map +1 -1
  21. package/src/lib/components/contact-timeline-card.js +4 -3
  22. package/src/lib/components/contact-timeline-card.js.map +1 -1
  23. package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
  24. package/src/lib/components/contacts-bulk-bar.js +24 -51
  25. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  26. package/src/lib/components/contacts-section.js +9 -3
  27. package/src/lib/components/contacts-section.js.map +1 -1
  28. package/src/lib/components/crm-export-all-button.d.ts +1 -1
  29. package/src/lib/components/crm-export-all-button.js.map +1 -1
  30. package/src/lib/components/crm-send-email-dialog.js +9 -6
  31. package/src/lib/components/crm-send-email-dialog.js.map +1 -1
  32. package/src/lib/components/csv-import-drawer.js +2 -2
  33. package/src/lib/components/csv-import-drawer.js.map +1 -1
  34. package/src/lib/components/deal-detail-page.js +18 -2
  35. package/src/lib/components/deal-detail-page.js.map +1 -1
  36. package/src/lib/components/deal-edit-drawer.js +10 -0
  37. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  38. package/src/lib/components/deals-bulk-bar.js +13 -5
  39. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  40. package/src/lib/components/lead-campaigns-card.js +6 -0
  41. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  42. package/src/lib/components/lead-detail-page.js +25 -5
  43. package/src/lib/components/lead-detail-page.js.map +1 -1
  44. package/src/lib/components/lead-properties-card.js +6 -0
  45. package/src/lib/components/lead-properties-card.js.map +1 -1
  46. package/src/lib/components/lead-surfaces-note.js +4 -5
  47. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  48. package/src/lib/components/lead-unqualify-dialog.js +6 -0
  49. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  50. package/src/lib/components/leads-bulk-bar.js +28 -3
  51. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  52. package/src/lib/components/leads-section.js +36 -7
  53. package/src/lib/components/leads-section.js.map +1 -1
  54. package/src/lib/components/org-lead-surfaces-note.js +6 -2
  55. package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
  56. package/src/lib/components/recipes-card.js +9 -4
  57. package/src/lib/components/recipes-card.js.map +1 -1
  58. package/src/lib/components/record-sharing-card.d.ts +57 -0
  59. package/src/lib/components/record-sharing-card.js +244 -0
  60. package/src/lib/components/record-sharing-card.js.map +1 -0
  61. package/src/lib/components/settings-section.js +7 -0
  62. package/src/lib/components/settings-section.js.map +1 -1
  63. package/src/lib/components/share-records-dialog.d.ts +28 -0
  64. package/src/lib/components/share-records-dialog.js +184 -0
  65. package/src/lib/components/share-records-dialog.js.map +1 -0
  66. package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
  67. package/src/lib/components/sharing-rule-drawer.js +374 -0
  68. package/src/lib/components/sharing-rule-drawer.js.map +1 -0
  69. package/src/lib/components/sharing-rules-card.d.ts +24 -0
  70. package/src/lib/components/sharing-rules-card.js +287 -0
  71. package/src/lib/components/sharing-rules-card.js.map +1 -0
  72. package/src/lib/components/use-crm-api.d.ts +1 -1
  73. package/src/lib/components/use-crm-api.js.map +1 -1
  74. package/src/lib/components/use-site-route-context.d.ts +25 -0
  75. package/src/lib/components/use-site-route-context.js +58 -0
  76. package/src/lib/components/use-site-route-context.js.map +1 -0
  77. package/src/lib/constants/api-routes.d.ts +31 -0
  78. package/src/lib/constants/api-routes.js +27 -1
  79. package/src/lib/constants/api-routes.js.map +1 -1
  80. package/src/lib/declarations.console-server.d.ts +37 -0
  81. package/src/lib/declarations.console-server.js +103 -0
  82. package/src/lib/declarations.console-server.js.map +1 -0
  83. package/src/lib/declarations.server.d.ts +6 -0
  84. package/src/lib/declarations.server.js +33 -0
  85. package/src/lib/declarations.server.js.map +1 -1
  86. package/src/lib/hooks/use-contact-remove.d.ts +35 -0
  87. package/src/lib/hooks/use-contact-remove.js +77 -0
  88. package/src/lib/hooks/use-contact-remove.js.map +1 -0
  89. package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
  90. package/src/lib/hooks/use-crm-sharing.js +109 -0
  91. package/src/lib/hooks/use-crm-sharing.js.map +1 -0
  92. package/src/lib/model/companies-csv.d.ts +1 -1
  93. package/src/lib/model/companies-csv.js +2 -2
  94. package/src/lib/model/companies-csv.js.map +1 -1
  95. package/src/lib/model/contact-holder.d.ts +68 -0
  96. package/src/lib/model/contact-holder.js +129 -0
  97. package/src/lib/model/contact-holder.js.map +1 -0
  98. package/src/lib/model/contact-interaction-links.d.ts +46 -0
  99. package/src/lib/model/contact-interaction-links.js +65 -0
  100. package/src/lib/model/contact-interaction-links.js.map +1 -0
  101. package/src/lib/model/contact-merge.d.ts +136 -0
  102. package/src/lib/model/contact-merge.js +412 -0
  103. package/src/lib/model/contact-merge.js.map +1 -0
  104. package/src/lib/model/contact-record.d.ts +7 -1
  105. package/src/lib/model/contact-record.js +6 -5
  106. package/src/lib/model/contact-record.js.map +1 -1
  107. package/src/lib/model/contact-remove.d.ts +48 -0
  108. package/src/lib/model/contact-remove.js +22 -0
  109. package/src/lib/model/contact-remove.js.map +1 -0
  110. package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
  111. package/src/lib/model/contacts-bulk-writes.js +2 -82
  112. package/src/lib/model/contacts-bulk-writes.js.map +1 -1
  113. package/src/lib/model/contacts-csv.d.ts +2 -2
  114. package/src/lib/model/contacts-csv.js +2 -2
  115. package/src/lib/model/contacts-csv.js.map +1 -1
  116. package/src/lib/model/crm-csv.d.ts +239 -0
  117. package/src/lib/model/crm-csv.js +467 -0
  118. package/src/lib/model/crm-csv.js.map +1 -0
  119. package/src/lib/model/crm-digest.d.ts +185 -0
  120. package/src/lib/model/crm-digest.js +272 -0
  121. package/src/lib/model/crm-digest.js.map +1 -0
  122. package/src/lib/model/crm-import.d.ts +7 -1
  123. package/src/lib/model/crm-import.js +2 -1
  124. package/src/lib/model/crm-import.js.map +1 -1
  125. package/src/lib/model/crm-record-routes.d.ts +5 -0
  126. package/src/lib/model/crm-record-routes.js +7 -0
  127. package/src/lib/model/crm-record-routes.js.map +1 -1
  128. package/src/lib/model/crm-routes.d.ts +6 -5
  129. package/src/lib/model/crm-routes.js +7 -7
  130. package/src/lib/model/crm-routes.js.map +1 -1
  131. package/src/lib/model/crm-search-sources.d.ts +15 -0
  132. package/src/lib/model/crm-search-sources.js +162 -0
  133. package/src/lib/model/crm-search-sources.js.map +1 -0
  134. package/src/lib/model/crm-sharing-api.d.ts +32 -0
  135. package/src/lib/model/crm-sharing-api.js +59 -0
  136. package/src/lib/model/crm-sharing-api.js.map +1 -0
  137. package/src/lib/model/crm-sharing.d.ts +333 -0
  138. package/src/lib/model/crm-sharing.js +603 -0
  139. package/src/lib/model/crm-sharing.js.map +1 -0
  140. package/src/lib/model/deals-csv.d.ts +1 -1
  141. package/src/lib/model/deals-csv.js +4 -5
  142. package/src/lib/model/deals-csv.js.map +1 -1
  143. package/src/lib/model/lead-filters.d.ts +1 -1
  144. package/src/lib/model/leads-csv.d.ts +1 -1
  145. package/src/lib/model/leads-csv.js +1 -1
  146. package/src/lib/model/leads-csv.js.map +1 -1
  147. package/src/lib/model/tasks-csv.d.ts +1 -1
  148. package/src/lib/model/tasks-csv.js +1 -1
  149. package/src/lib/model/tasks-csv.js.map +1 -1
  150. package/src/lib/plan-entitlements.d.ts +75 -0
  151. package/src/lib/plan-entitlements.js +56 -0
  152. package/src/lib/plan-entitlements.js.map +1 -0
  153. package/src/lib/plugin.js +3 -0
  154. package/src/lib/plugin.js.map +1 -1
  155. package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
  156. package/src/lib/server/api-v1/contacts-merge.js +120 -0
  157. package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
  158. package/src/lib/server/api-v1/contacts.d.ts +18 -0
  159. package/src/lib/server/api-v1/contacts.js +1076 -0
  160. package/src/lib/server/api-v1/contacts.js.map +1 -0
  161. package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
  162. package/src/lib/server/api-v1/crm-activities.js +269 -0
  163. package/src/lib/server/api-v1/crm-activities.js.map +1 -0
  164. package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
  165. package/src/lib/server/api-v1/crm-companies.js +391 -0
  166. package/src/lib/server/api-v1/crm-companies.js.map +1 -0
  167. package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
  168. package/src/lib/server/api-v1/crm-deals.js +631 -0
  169. package/src/lib/server/api-v1/crm-deals.js.map +1 -0
  170. package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
  171. package/src/lib/server/api-v1/crm-email-templates.js +336 -0
  172. package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
  173. package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
  174. package/src/lib/server/api-v1/crm-leads.js +832 -0
  175. package/src/lib/server/api-v1/crm-leads.js.map +1 -0
  176. package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
  177. package/src/lib/server/api-v1/crm-pipelines.js +146 -0
  178. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
  179. package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
  180. package/src/lib/server/api-v1/crm-shared.js +436 -0
  181. package/src/lib/server/api-v1/crm-shared.js.map +1 -0
  182. package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
  183. package/src/lib/server/api-v1/crm-tasks.js +398 -0
  184. package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
  185. package/src/lib/server/api-v1/openapi.d.ts +29 -0
  186. package/src/lib/server/api-v1/openapi.js +985 -0
  187. package/src/lib/server/api-v1/openapi.js.map +1 -0
  188. package/src/lib/server/api-v1/usage.d.ts +26 -0
  189. package/src/lib/server/api-v1/usage.js +73 -0
  190. package/src/lib/server/api-v1/usage.js.map +1 -0
  191. package/src/lib/server/consent-group-participant.js +1 -1
  192. package/src/lib/server/consent-group-participant.js.map +1 -1
  193. package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
  194. package/src/lib/server/contact-lifecycle-floor.js +91 -0
  195. package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
  196. package/src/lib/server/contact-remove.d.ts +75 -0
  197. package/src/lib/server/contact-remove.js +244 -0
  198. package/src/lib/server/contact-remove.js.map +1 -0
  199. package/src/lib/server/contact-update.js +2 -1
  200. package/src/lib/server/contact-update.js.map +1 -1
  201. package/src/lib/server/contacts-import.d.ts +47 -11
  202. package/src/lib/server/contacts-import.js +171 -71
  203. package/src/lib/server/contacts-import.js.map +1 -1
  204. package/src/lib/server/contacts-merge.d.ts +1 -1
  205. package/src/lib/server/contacts-merge.js +3 -2
  206. package/src/lib/server/contacts-merge.js.map +1 -1
  207. package/src/lib/server/convert-host-lead.d.ts +189 -0
  208. package/src/lib/server/convert-host-lead.js +518 -0
  209. package/src/lib/server/convert-host-lead.js.map +1 -0
  210. package/src/lib/server/crm-sharing.d.ts +108 -0
  211. package/src/lib/server/crm-sharing.js +790 -0
  212. package/src/lib/server/crm-sharing.js.map +1 -0
  213. package/src/lib/server/daily-digest-route.d.ts +107 -0
  214. package/src/lib/server/daily-digest-route.js +588 -0
  215. package/src/lib/server/daily-digest-route.js.map +1 -0
  216. package/src/lib/server/export-route.d.ts +60 -0
  217. package/src/lib/server/export-route.js +447 -0
  218. package/src/lib/server/export-route.js.map +1 -0
  219. package/src/lib/server/inbound-route.d.ts +74 -0
  220. package/src/lib/server/inbound-route.js +257 -0
  221. package/src/lib/server/inbound-route.js.map +1 -0
  222. package/src/lib/server/lead-convert.d.ts +1 -1
  223. package/src/lib/server/lead-convert.js +2 -2
  224. package/src/lib/server/lead-convert.js.map +1 -1
  225. package/src/lib/server/merge-contacts.d.ts +55 -0
  226. package/src/lib/server/merge-contacts.js +274 -0
  227. package/src/lib/server/merge-contacts.js.map +1 -0
  228. package/src/lib/server/pipeline-record-index.d.ts +36 -0
  229. package/src/lib/server/pipeline-record-index.js +87 -0
  230. package/src/lib/server/pipeline-record-index.js.map +1 -0
  231. package/src/lib/server/record-facts.js +2 -1
  232. package/src/lib/server/record-facts.js.map +1 -1
  233. package/src/lib/server/task-reminders-route.d.ts +82 -0
  234. package/src/lib/server/task-reminders-route.js +497 -0
  235. package/src/lib/server/task-reminders-route.js.map +1 -0
  236. package/src/lib/server-deal-stage.js +2 -1
  237. package/src/lib/server-deal-stage.js.map +1 -1
  238. package/src/lib/server.d.ts +5 -2
  239. package/src/lib/server.js +47 -2
  240. package/src/lib/server.js.map +1 -1
  241. package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
  242. package/src/lib/testing/stand-in-site-record-routes.js +55 -0
  243. package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
  244. package/src/lib/usage-axes.d.ts +35 -0
  245. package/src/lib/usage-axes.js +68 -0
  246. package/src/lib/usage-axes.js.map +1 -0
  247. package/src/lib/components/use-emails-hub-path.d.ts +0 -24
  248. package/src/lib/components/use-emails-hub-path.js +0 -59
  249. package/src/lib/components/use-emails-hub-path.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-crm-sharing.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 { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { type CrmSharingObject, type CrmSharingSite, readCrmSharingRules } from '../model/crm-sharing'\nimport { callCrmSharing, followUpCrmSharing } from '../model/crm-sharing-api'\nimport { crmTaskCallScope } from '../model/task-routes'\nimport { useCrmOrgMount } from './use-crm-org-mount'\nimport type { CrmOrgDoc } from './use-crm-scope'\n\nexport interface CrmSharingSites {\n sites: CrmSharingSite[]\n ready: boolean\n /** How a site reads: its name, or a neutral phrase for one this reader cannot name. */\n siteName: (hostId: string) => string\n}\n\n/**\n * The org's sites, for a sharing surface (AGL-3336).\n *\n * At the organization level the hub already holds them (`useCrmOrgMount`).\n * Under a site a member cannot list the org's other sites, so a MANAGER's\n * surface asks `crm/sharing` for them — the one reader a share is offered\n * to — and every other reader names them as \"another site\".\n */\nexport function useCrmSharingSites(input: {\n hostId: string | null\n orgId: string | null\n enabled: boolean\n}): CrmSharingSites {\n const { hostId, orgId, enabled } = input\n const mount = useCrmOrgMount()\n const { data: user } = useUser()\n const [fetched, setFetched] = useState<{ key: string; sites: CrmSharingSite[] } | null>(null)\n const scope = crmTaskCallScope(hostId, orgId)\n const key = scope ? JSON.stringify(scope) : ''\n const fromMount = mount && !hostId\n useEffect(() => {\n if (fromMount || !enabled || !scope || !user) return\n let cancelled = false\n callCrmSharing(user, scope, 'sites')\n .then((answer) => {\n if (!cancelled) setFetched({ key, sites: answer.sites ?? [] })\n })\n .catch(() => {\n if (!cancelled) setFetched({ key, sites: [] })\n })\n return () => {\n cancelled = true\n }\n // `scope` is keyed by `key`; the object itself is rebuilt every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [fromMount, enabled, key, user])\n const sites = useMemo<CrmSharingSite[]>(\n () =>\n fromMount\n ? mount.hosts.map((host) => ({ id: host.id, name: host.name }))\n : fetched?.key === key\n ? fetched.sites\n : [],\n [fromMount, mount, fetched, key],\n )\n const ready = fromMount ? mount.hostsReady : fetched?.key === key || !enabled\n const siteName = useCallback(\n (id: string) => sites.find((site) => site.id === id)?.name ?? (id === hostId ? 'This site' : 'another site'),\n [sites, hostId],\n )\n return { sites, ready, siteName }\n}\n\n/** The org's sharing rules, off the org document the shell already passed. */\nexport function useCrmSharingRules(org: CrmOrgDoc) {\n return useMemo(\n () => readCrmSharingRules((org ?? null) as Record<string, unknown> | null),\n [org],\n )\n}\n\n\n/**\n * The follow-up a CLIENT-DIRECT write to a lead, a company or a deal owes\n * the org's sharing rules (AGL-3336): a server write is followed by the\n * core's record-written seam, a browser's by this. Fire-and-forget — the\n * save has landed whether or not a rule then shares the record.\n */\nexport function useCrmSharingFollowUp(\n hostId: string | null | undefined,\n orgId: string | null | undefined,\n): (object: CrmSharingObject, ids: readonly string[]) => void {\n const { data: user } = useUser()\n return useCallback(\n (object: CrmSharingObject, ids: readonly string[]) => {\n void followUpCrmSharing(user, crmTaskCallScope(hostId, orgId), object, ids)\n },\n [user, hostId, orgId],\n )\n}\n"],"names":["useUser","useCallback","useEffect","useMemo","useState","readCrmSharingRules","callCrmSharing","followUpCrmSharing","crmTaskCallScope","useCrmOrgMount","useCrmSharingSites","input","hostId","orgId","enabled","mount","data","user","fetched","setFetched","scope","key","JSON","stringify","fromMount","cancelled","then","answer","sites","catch","hosts","map","host","id","name","ready","hostsReady","siteName","find","site","useCrmSharingRules","org","useCrmSharingFollowUp","object","ids"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,OAAO,QAAQ,iCAAgC;AACxD,SAASC,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACjE,SAAqDC,mBAAmB,QAAQ,0BAAsB;AACtG,SAASC,cAAc,EAAEC,kBAAkB,QAAQ,8BAA0B;AAC7E,SAASC,gBAAgB,QAAQ,0BAAsB;AACvD,SAASC,cAAc,QAAQ,yBAAqB;AAUpD;;;;;;;CAOC,GACD,OAAO,SAASC,mBAAmBC,KAIlC;IACC,MAAM,EAAEC,MAAM,EAAEC,KAAK,EAAEC,OAAO,EAAE,GAAGH;IACnC,MAAMI,QAAQN;IACd,MAAM,EAAEO,MAAMC,IAAI,EAAE,GAAGjB;IACvB,MAAM,CAACkB,SAASC,WAAW,GAAGf,SAA0D;IACxF,MAAMgB,QAAQZ,iBAAiBI,QAAQC;IACvC,MAAMQ,MAAMD,QAAQE,KAAKC,SAAS,CAACH,SAAS;IAC5C,MAAMI,YAAYT,SAAS,CAACH;IAC5BV,UAAU;QACR,IAAIsB,aAAa,CAACV,WAAW,CAACM,SAAS,CAACH,MAAM;QAC9C,IAAIQ,YAAY;QAChBnB,eAAeW,MAAMG,OAAO,SACzBM,IAAI,CAAC,CAACC;gBACoCA;YAAzC,IAAI,CAACF,WAAWN,WAAW;gBAAEE;gBAAKO,KAAK,GAAED,gBAAAA,OAAOC,KAAK,YAAZD,gBAAgB,EAAE;YAAC;QAC9D,GACCE,KAAK,CAAC;YACL,IAAI,CAACJ,WAAWN,WAAW;gBAAEE;gBAAKO,OAAO,EAAE;YAAC;QAC9C;QACF,OAAO;YACLH,YAAY;QACd;IACA,wEAAwE;IACxE,uDAAuD;IACzD,GAAG;QAACD;QAAWV;QAASO;QAAKJ;KAAK;IAClC,MAAMW,QAAQzB,QACZ,IACEqB,YACIT,MAAMe,KAAK,CAACC,GAAG,CAAC,CAACC,OAAU,CAAA;gBAAEC,IAAID,KAAKC,EAAE;gBAAEC,MAAMF,KAAKE,IAAI;YAAC,CAAA,KAC1DhB,CAAAA,2BAAAA,QAASG,GAAG,MAAKA,MACfH,QAAQU,KAAK,GACb,EAAE,EACV;QAACJ;QAAWT;QAAOG;QAASG;KAAI;IAElC,MAAMc,QAAQX,YAAYT,MAAMqB,UAAU,GAAGlB,CAAAA,2BAAAA,QAASG,GAAG,MAAKA,OAAO,CAACP;IACtE,MAAMuB,WAAWpC,YACf,CAACgC;;YAAeL;wBAAAA,cAAAA,MAAMU,IAAI,CAAC,CAACC,OAASA,KAAKN,EAAE,KAAKA,wBAAjCL,YAAsCM,IAAI,mBAAKD,OAAOrB,SAAS,cAAc;OAC7F;QAACgB;QAAOhB;KAAO;IAEjB,OAAO;QAAEgB;QAAOO;QAAOE;IAAS;AAClC;AAEA,4EAA4E,GAC5E,OAAO,SAASG,mBAAmBC,GAAc;IAC/C,OAAOtC,QACL,IAAME,oBAAqBoC,cAAAA,MAAO,OAClC;QAACA;KAAI;AAET;AAGA;;;;;CAKC,GACD,OAAO,SAASC,sBACd9B,MAAiC,EACjCC,KAAgC;IAEhC,MAAM,EAAEG,MAAMC,IAAI,EAAE,GAAGjB;IACvB,OAAOC,YACL,CAAC0C,QAA0BC;QACzB,KAAKrC,mBAAmBU,MAAMT,iBAAiBI,QAAQC,QAAQ8B,QAAQC;IACzE,GACA;QAAC3B;QAAML;QAAQC;KAAM;AAEzB"}
@@ -28,7 +28,7 @@
28
28
  * because it is what a merchant reads the file for, and maps to "Do not
29
29
  * import" on the way back.
30
30
  */
31
- export { COMPANY_CSV_COLUMNS, companiesCsv, type CompanyCsvOptions, type CompanyCsvRow, } from '@aglyn/aglyn';
31
+ export { COMPANY_CSV_COLUMNS, companiesCsv, type CompanyCsvOptions, type CompanyCsvRow, } from './crm-csv';
32
32
  /**
33
33
  * The file the Import drawer hands out to start from: the export's header
34
34
  * and nothing under it.
@@ -26,8 +26,8 @@
26
26
  * column the import has no field for, the contacts count, is written last
27
27
  * because it is what a merchant reads the file for, and maps to "Do not
28
28
  * import" on the way back.
29
- */ /* The writer moved to `@aglyn/aglyn` under AGL-2662 — see `deals-csv.ts`. */ export { COMPANY_CSV_COLUMNS, companiesCsv } from "@aglyn/aglyn";
30
- import { companiesCsv } from "@aglyn/aglyn";
29
+ */ /* The writer is `crm-csv.ts`, shared with the server's export — see `deals-csv.ts`. */ export { COMPANY_CSV_COLUMNS, companiesCsv } from "./crm-csv.js";
30
+ import { companiesCsv } from "./crm-csv.js";
31
31
  /**
32
32
  * The file the Import drawer hands out to start from: the export's header
33
33
  * and nothing under it.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/companies-csv.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\n/**\n * The companies CSV — one file from the table's Export button and the bulk\n * bar's (AGL-2621).\n *\n * The same rule the contacts file follows: the header is the companies\n * IMPORT's own vocabulary — the first alias of every field\n * `guessCompanyImportMapping` reads — so an export re-imports without a\n * hand mapping, and the template the Import drawer hands out IS this header\n * over no rows. The owner is written by ADDRESS, because the import resolves\n * an owner by email and a uid is nothing a spreadsheet can read. The one\n * column the import has no field for, the contacts count, is written last\n * because it is what a merchant reads the file for, and maps to \"Do not\n * import\" on the way back.\n */\n\n/* The writer moved to `@aglyn/aglyn` under AGL-2662 — see `deals-csv.ts`. */\nexport {\n COMPANY_CSV_COLUMNS,\n companiesCsv,\n type CompanyCsvOptions,\n type CompanyCsvRow,\n} from '@aglyn/aglyn'\n\nimport { companiesCsv } from '@aglyn/aglyn'\n\n/**\n * The file the Import drawer hands out to start from: the export's header\n * and nothing under it.\n */\nexport function companyImportTemplateCsv(): string {\n return companiesCsv([])\n}\n"],"names":["COMPANY_CSV_COLUMNS","companiesCsv","companyImportTemplateCsv"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;CAaC,GAED,2EAA2E,GAC3E,SACEA,mBAAmB,EACnBC,YAAY,QAGP,eAAc;AAErB,SAASA,YAAY,QAAQ,eAAc;AAE3C;;;CAGC,GACD,OAAO,SAASC;IACd,OAAOD,aAAa,EAAE;AACxB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/companies-csv.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\n/**\n * The companies CSV — one file from the table's Export button and the bulk\n * bar's (AGL-2621).\n *\n * The same rule the contacts file follows: the header is the companies\n * IMPORT's own vocabulary — the first alias of every field\n * `guessCompanyImportMapping` reads — so an export re-imports without a\n * hand mapping, and the template the Import drawer hands out IS this header\n * over no rows. The owner is written by ADDRESS, because the import resolves\n * an owner by email and a uid is nothing a spreadsheet can read. The one\n * column the import has no field for, the contacts count, is written last\n * because it is what a merchant reads the file for, and maps to \"Do not\n * import\" on the way back.\n */\n\n/* The writer is `crm-csv.ts`, shared with the server's export — see `deals-csv.ts`. */\nexport {\n COMPANY_CSV_COLUMNS,\n companiesCsv,\n type CompanyCsvOptions,\n type CompanyCsvRow,\n} from './crm-csv'\n\nimport { companiesCsv } from './crm-csv'\n\n/**\n * The file the Import drawer hands out to start from: the export's header\n * and nothing under it.\n */\nexport function companyImportTemplateCsv(): string {\n return companiesCsv([])\n}\n"],"names":["COMPANY_CSV_COLUMNS","companiesCsv","companyImportTemplateCsv"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;CAaC,GAED,qFAAqF,GACrF,SACEA,mBAAmB,EACnBC,YAAY,QAGP,eAAW;AAElB,SAASA,YAAY,QAAQ,eAAW;AAExC;;;CAGC,GACD,OAAO,SAASC;IACd,OAAOD,aAAa,EAAE;AACxB"}
@@ -0,0 +1,68 @@
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
+ */
17
+ import { type ConsentGroup } from '@aglyn/aglyn/app-utils/consent-groups';
18
+ /**
19
+ * The holder of nothing: the group a contact is flattened through when no
20
+ * site has captured it and no facet exists — a row written by a path that
21
+ * predates attribution, read at the org level (AGL-2630).
22
+ *
23
+ * An empty `groupId` reads an empty facet (`readContactFacet` answers the
24
+ * empty facet for a key the map lacks) and REFUSES a write
25
+ * (`contactFacetPath` throws on an empty holder), which is the right pair:
26
+ * the row still shows its shared identity, and nothing edits a facet
27
+ * nobody holds. Exported so a surface can test for it rather than for an
28
+ * empty string.
29
+ */
30
+ export declare const NO_HOLDER_GROUP: ConsentGroup;
31
+ /**
32
+ * The group a LEAD is read and acted on through (AGL-3278).
33
+ *
34
+ * The same question {@link contactPrimaryGroup} answers, with a shorter
35
+ * answer: a lead is one flat document — no facets, one profile, one
36
+ * status — so there is nothing to choose between and the holder is simply
37
+ * the site that captured the person first. What it is FOR is the same: at
38
+ * the organization level the record page has no viewing site, and it needs
39
+ * one to read the consent basis against, to file an activity under, to
40
+ * offer a booking on and to convert as.
41
+ *
42
+ * A lead no site has captured — a row imported before attribution — reads
43
+ * through {@link NO_HOLDER_GROUP}, whose empty `hostId` is how a surface
44
+ * asks "is there a site here at all" without testing for an empty string.
45
+ */
46
+ export declare function leadPrimaryGroup(lead: Record<string, unknown> | null | undefined, org: Record<string, unknown> | null | undefined): ConsentGroup;
47
+ /**
48
+ * The group a CROSS-HOLDER reader flattens one contact through (AGL-2630).
49
+ *
50
+ * Under a site there is one answer for every row — the site's own group,
51
+ * `consentGroupForHost` — and the reader never asks this. At the
52
+ * organization level there is no viewing site, and a contact captured by
53
+ * three sites has up to three facets; the org-wide member reading them all
54
+ * needs ONE to show the row's profile, owner and stage under, and to write
55
+ * an edit back to. This picks the person's PRIMARY holder: the first
56
+ * capturing site, in capture order, whose group actually holds a facet;
57
+ * then any facet at all, resolved back to its declared group or to the
58
+ * site whose id it is keyed by; then the first capturing site even with no
59
+ * facet yet (the facet is written on the first edit). A row nobody captured
60
+ * and nobody wrote a facet for reads through {@link NO_HOLDER_GROUP}.
61
+ *
62
+ * Capture order rather than sorted order, deliberately: `capturedByHostIds`
63
+ * is maintained by `arrayUnion`, so its first entry is the site that met
64
+ * the person first, and that site's profile is the one a person reading
65
+ * across brands would expect to see. `contactCaptureHostIds` sorts the same
66
+ * array for RENDERING, where a stable order matters more than a first.
67
+ */
68
+ export declare function contactPrimaryGroup(row: Record<string, unknown> | null | undefined, org: Record<string, unknown> | null | undefined): ConsentGroup;
@@ -0,0 +1,129 @@
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
+ */ /**
17
+ * WHICH HOLDER A CROSS-SITE READER SEES A CONTACT THROUGH (AGL-2630).
18
+ *
19
+ * A contact document carries one facet per holder, and every field but the
20
+ * shared identity lives in one of them. A surface standing on a site reads
21
+ * that site's group; a surface standing at the organization — the org-level
22
+ * Contacts list, and the whole-collection export that writes the same file
23
+ * — has no site to read as and needs one answer per row.
24
+ *
25
+ * In the library rather than in the CRM plugin because both of those
26
+ * readers need it and one of them is an app: an app may not import a
27
+ * feature plugin (AGL-2662).
28
+ */ import { CONTACT_FACETS_FIELD } from "@aglyn/aglyn/app-utils/contacts";
29
+ import { CAPTURED_BY_HOST_FIELD } from "@aglyn/aglyn/app-utils/marketing-consent";
30
+ import { consentGroupForHost, consentGroupsAwaitConfirmation, readConsentGroups, soloConsentGroup } from "@aglyn/aglyn/app-utils/consent-groups";
31
+ /**
32
+ * The holder of nothing: the group a contact is flattened through when no
33
+ * site has captured it and no facet exists — a row written by a path that
34
+ * predates attribution, read at the org level (AGL-2630).
35
+ *
36
+ * An empty `groupId` reads an empty facet (`readContactFacet` answers the
37
+ * empty facet for a key the map lacks) and REFUSES a write
38
+ * (`contactFacetPath` throws on an empty holder), which is the right pair:
39
+ * the row still shows its shared identity, and nothing edits a facet
40
+ * nobody holds. Exported so a surface can test for it rather than for an
41
+ * empty string.
42
+ */ export const NO_HOLDER_GROUP = Object.freeze({
43
+ hostId: '',
44
+ groupId: '',
45
+ name: null,
46
+ hostIds: [],
47
+ declared: false,
48
+ awaitsConfirmation: false
49
+ });
50
+ /**
51
+ * The sites that captured this person, in CAPTURE order — the order
52
+ * `arrayUnion` left, so the first entry is the site that met them first.
53
+ *
54
+ * `contactCaptureHostIds` sorts the same array for RENDERING, where a
55
+ * stable order matters more than a first. Both readers below want the
56
+ * first, so neither may use the sorted one.
57
+ */ function captureOrder(record) {
58
+ const raw = (record != null ? record : {})[CAPTURED_BY_HOST_FIELD];
59
+ if (!Array.isArray(raw)) return [];
60
+ return raw.map((id)=>String(id != null ? id : '').trim()).filter(Boolean);
61
+ }
62
+ /**
63
+ * The group a LEAD is read and acted on through (AGL-3278).
64
+ *
65
+ * The same question {@link contactPrimaryGroup} answers, with a shorter
66
+ * answer: a lead is one flat document — no facets, one profile, one
67
+ * status — so there is nothing to choose between and the holder is simply
68
+ * the site that captured the person first. What it is FOR is the same: at
69
+ * the organization level the record page has no viewing site, and it needs
70
+ * one to read the consent basis against, to file an activity under, to
71
+ * offer a booking on and to convert as.
72
+ *
73
+ * A lead no site has captured — a row imported before attribution — reads
74
+ * through {@link NO_HOLDER_GROUP}, whose empty `hostId` is how a surface
75
+ * asks "is there a site here at all" without testing for an empty string.
76
+ */ export function leadPrimaryGroup(lead, org) {
77
+ const captured = captureOrder(lead);
78
+ return captured.length ? consentGroupForHost(org, captured[0]) : NO_HOLDER_GROUP;
79
+ }
80
+ /**
81
+ * The group a CROSS-HOLDER reader flattens one contact through (AGL-2630).
82
+ *
83
+ * Under a site there is one answer for every row — the site's own group,
84
+ * `consentGroupForHost` — and the reader never asks this. At the
85
+ * organization level there is no viewing site, and a contact captured by
86
+ * three sites has up to three facets; the org-wide member reading them all
87
+ * needs ONE to show the row's profile, owner and stage under, and to write
88
+ * an edit back to. This picks the person's PRIMARY holder: the first
89
+ * capturing site, in capture order, whose group actually holds a facet;
90
+ * then any facet at all, resolved back to its declared group or to the
91
+ * site whose id it is keyed by; then the first capturing site even with no
92
+ * facet yet (the facet is written on the first edit). A row nobody captured
93
+ * and nobody wrote a facet for reads through {@link NO_HOLDER_GROUP}.
94
+ *
95
+ * Capture order rather than sorted order, deliberately: `capturedByHostIds`
96
+ * is maintained by `arrayUnion`, so its first entry is the site that met
97
+ * the person first, and that site's profile is the one a person reading
98
+ * across brands would expect to see. `contactCaptureHostIds` sorts the same
99
+ * array for RENDERING, where a stable order matters more than a first.
100
+ */ export function contactPrimaryGroup(row, org) {
101
+ const record = row != null ? row : {};
102
+ const facets = record[CONTACT_FACETS_FIELD];
103
+ const facetKeys = facets && typeof facets === 'object' && !Array.isArray(facets) ? Object.keys(facets) : [];
104
+ const captured = captureOrder(record);
105
+ for (const hostId of captured){
106
+ const group = consentGroupForHost(org, hostId);
107
+ if (facetKeys.includes(group.groupId)) return group;
108
+ }
109
+ for (const key of facetKeys){
110
+ const declared = readConsentGroups(org)[key];
111
+ if (declared) {
112
+ return {
113
+ hostId: declared.hostIds[0],
114
+ groupId: key,
115
+ name: declared.name,
116
+ hostIds: [
117
+ ...declared.hostIds
118
+ ],
119
+ declared: true,
120
+ awaitsConfirmation: consentGroupsAwaitConfirmation(org)
121
+ };
122
+ }
123
+ return soloConsentGroup(key);
124
+ }
125
+ if (captured.length) return consentGroupForHost(org, captured[0]);
126
+ return NO_HOLDER_GROUP;
127
+ }
128
+
129
+ //# sourceMappingURL=contact-holder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-holder.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\n/**\n * WHICH HOLDER A CROSS-SITE READER SEES A CONTACT THROUGH (AGL-2630).\n *\n * A contact document carries one facet per holder, and every field but the\n * shared identity lives in one of them. A surface standing on a site reads\n * that site's group; a surface standing at the organization — the org-level\n * Contacts list, and the whole-collection export that writes the same file\n * — has no site to read as and needs one answer per row.\n *\n * In the library rather than in the CRM plugin because both of those\n * readers need it and one of them is an app: an app may not import a\n * feature plugin (AGL-2662).\n */\n\nimport { CONTACT_FACETS_FIELD } from '@aglyn/aglyn/app-utils/contacts'\nimport { CAPTURED_BY_HOST_FIELD } from '@aglyn/aglyn/app-utils/marketing-consent'\nimport {\n type ConsentGroup,\n consentGroupForHost,\n consentGroupsAwaitConfirmation,\n readConsentGroups,\n soloConsentGroup,\n} from '@aglyn/aglyn/app-utils/consent-groups'\n\n/**\n * The holder of nothing: the group a contact is flattened through when no\n * site has captured it and no facet exists — a row written by a path that\n * predates attribution, read at the org level (AGL-2630).\n *\n * An empty `groupId` reads an empty facet (`readContactFacet` answers the\n * empty facet for a key the map lacks) and REFUSES a write\n * (`contactFacetPath` throws on an empty holder), which is the right pair:\n * the row still shows its shared identity, and nothing edits a facet\n * nobody holds. Exported so a surface can test for it rather than for an\n * empty string.\n */\nexport const NO_HOLDER_GROUP: ConsentGroup = Object.freeze({\n hostId: '',\n groupId: '',\n name: null,\n hostIds: [],\n declared: false,\n awaitsConfirmation: false,\n}) as ConsentGroup\n\n/**\n * The sites that captured this person, in CAPTURE order — the order\n * `arrayUnion` left, so the first entry is the site that met them first.\n *\n * `contactCaptureHostIds` sorts the same array for RENDERING, where a\n * stable order matters more than a first. Both readers below want the\n * first, so neither may use the sorted one.\n */\nfunction captureOrder(\n record: Record<string, unknown> | null | undefined,\n): string[] {\n const raw = (record ?? {})[CAPTURED_BY_HOST_FIELD]\n if (!Array.isArray(raw)) return []\n return raw.map((id) => String(id ?? '').trim()).filter(Boolean)\n}\n\n/**\n * The group a LEAD is read and acted on through (AGL-3278).\n *\n * The same question {@link contactPrimaryGroup} answers, with a shorter\n * answer: a lead is one flat document — no facets, one profile, one\n * status — so there is nothing to choose between and the holder is simply\n * the site that captured the person first. What it is FOR is the same: at\n * the organization level the record page has no viewing site, and it needs\n * one to read the consent basis against, to file an activity under, to\n * offer a booking on and to convert as.\n *\n * A lead no site has captured — a row imported before attribution — reads\n * through {@link NO_HOLDER_GROUP}, whose empty `hostId` is how a surface\n * asks \"is there a site here at all\" without testing for an empty string.\n */\nexport function leadPrimaryGroup(\n lead: Record<string, unknown> | null | undefined,\n org: Record<string, unknown> | null | undefined,\n): ConsentGroup {\n const captured = captureOrder(lead)\n return captured.length ? consentGroupForHost(org, captured[0]) : NO_HOLDER_GROUP\n}\n\n/**\n * The group a CROSS-HOLDER reader flattens one contact through (AGL-2630).\n *\n * Under a site there is one answer for every row — the site's own group,\n * `consentGroupForHost` — and the reader never asks this. At the\n * organization level there is no viewing site, and a contact captured by\n * three sites has up to three facets; the org-wide member reading them all\n * needs ONE to show the row's profile, owner and stage under, and to write\n * an edit back to. This picks the person's PRIMARY holder: the first\n * capturing site, in capture order, whose group actually holds a facet;\n * then any facet at all, resolved back to its declared group or to the\n * site whose id it is keyed by; then the first capturing site even with no\n * facet yet (the facet is written on the first edit). A row nobody captured\n * and nobody wrote a facet for reads through {@link NO_HOLDER_GROUP}.\n *\n * Capture order rather than sorted order, deliberately: `capturedByHostIds`\n * is maintained by `arrayUnion`, so its first entry is the site that met\n * the person first, and that site's profile is the one a person reading\n * across brands would expect to see. `contactCaptureHostIds` sorts the same\n * array for RENDERING, where a stable order matters more than a first.\n */\nexport function contactPrimaryGroup(\n row: Record<string, unknown> | null | undefined,\n org: Record<string, unknown> | null | undefined,\n): ConsentGroup {\n const record = row ?? {}\n const facets = record[CONTACT_FACETS_FIELD]\n const facetKeys =\n facets && typeof facets === 'object' && !Array.isArray(facets)\n ? Object.keys(facets as Record<string, unknown>)\n : []\n const captured = captureOrder(record)\n for (const hostId of captured) {\n const group = consentGroupForHost(org, hostId)\n if (facetKeys.includes(group.groupId)) return group\n }\n for (const key of facetKeys) {\n const declared = readConsentGroups(org)[key]\n if (declared) {\n return {\n hostId: declared.hostIds[0],\n groupId: key,\n name: declared.name,\n hostIds: [...declared.hostIds],\n declared: true,\n awaitsConfirmation: consentGroupsAwaitConfirmation(org),\n }\n }\n return soloConsentGroup(key)\n }\n if (captured.length) return consentGroupForHost(org, captured[0])\n return NO_HOLDER_GROUP\n}"],"names":["CONTACT_FACETS_FIELD","CAPTURED_BY_HOST_FIELD","consentGroupForHost","consentGroupsAwaitConfirmation","readConsentGroups","soloConsentGroup","NO_HOLDER_GROUP","Object","freeze","hostId","groupId","name","hostIds","declared","awaitsConfirmation","captureOrder","record","raw","Array","isArray","map","id","String","trim","filter","Boolean","leadPrimaryGroup","lead","org","captured","length","contactPrimaryGroup","row","facets","facetKeys","keys","group","includes","key"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;CAYC,GAED,SAASA,oBAAoB,QAAQ,kCAAiC;AACtE,SAASC,sBAAsB,QAAQ,2CAA0C;AACjF,SAEEC,mBAAmB,EACnBC,8BAA8B,EAC9BC,iBAAiB,EACjBC,gBAAgB,QACX,wCAAuC;AAE9C;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,kBAAgCC,OAAOC,MAAM,CAAC;IACzDC,QAAQ;IACRC,SAAS;IACTC,MAAM;IACNC,SAAS,EAAE;IACXC,UAAU;IACVC,oBAAoB;AACtB,GAAkB;AAElB;;;;;;;CAOC,GACD,SAASC,aACPC,MAAkD;IAElD,MAAMC,MAAM,CAACD,iBAAAA,SAAU,CAAC,EAAE,CAACf,uBAAuB;IAClD,IAAI,CAACiB,MAAMC,OAAO,CAACF,MAAM,OAAO,EAAE;IAClC,OAAOA,IAAIG,GAAG,CAAC,CAACC,KAAOC,OAAOD,aAAAA,KAAM,IAAIE,IAAI,IAAIC,MAAM,CAACC;AACzD;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,iBACdC,IAAgD,EAChDC,GAA+C;IAE/C,MAAMC,WAAWd,aAAaY;IAC9B,OAAOE,SAASC,MAAM,GAAG5B,oBAAoB0B,KAAKC,QAAQ,CAAC,EAAE,IAAIvB;AACnE;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASyB,oBACdC,GAA+C,EAC/CJ,GAA+C;IAE/C,MAAMZ,SAASgB,cAAAA,MAAO,CAAC;IACvB,MAAMC,SAASjB,MAAM,CAAChB,qBAAqB;IAC3C,MAAMkC,YACJD,UAAU,OAAOA,WAAW,YAAY,CAACf,MAAMC,OAAO,CAACc,UACnD1B,OAAO4B,IAAI,CAACF,UACZ,EAAE;IACR,MAAMJ,WAAWd,aAAaC;IAC9B,KAAK,MAAMP,UAAUoB,SAAU;QAC7B,MAAMO,QAAQlC,oBAAoB0B,KAAKnB;QACvC,IAAIyB,UAAUG,QAAQ,CAACD,MAAM1B,OAAO,GAAG,OAAO0B;IAChD;IACA,KAAK,MAAME,OAAOJ,UAAW;QAC3B,MAAMrB,WAAWT,kBAAkBwB,IAAI,CAACU,IAAI;QAC5C,IAAIzB,UAAU;YACZ,OAAO;gBACLJ,QAAQI,SAASD,OAAO,CAAC,EAAE;gBAC3BF,SAAS4B;gBACT3B,MAAME,SAASF,IAAI;gBACnBC,SAAS;uBAAIC,SAASD,OAAO;iBAAC;gBAC9BC,UAAU;gBACVC,oBAAoBX,+BAA+ByB;YACrD;QACF;QACA,OAAOvB,iBAAiBiC;IAC1B;IACA,IAAIT,SAASC,MAAM,EAAE,OAAO5B,oBAAoB0B,KAAKC,QAAQ,CAAC,EAAE;IAChE,OAAOvB;AACT"}
@@ -0,0 +1,46 @@
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
+ */
17
+ import type { ContactInteraction, ContactSource } from '@aglyn/aglyn';
18
+ import { type PluginRecordRouteContext } from '@aglyn/aglyn/plugin-manager/plugin-record-routes';
19
+ /**
20
+ * Where a contact's captured history points back at (AGL-2622): the record
21
+ * each capture door left, where the console reads it.
22
+ *
23
+ * The submission, the order and the booking are other plugins' records, so
24
+ * their addresses are the ones those plugins publish on the record-route seam
25
+ * (`formSubmission`, `order`, `booking` — AGL-3080), and an entry whose owner
26
+ * is not loaded carries no link. A member sign-up lands on the platform's own
27
+ * Users page.
28
+ */
29
+ /**
30
+ * What the link on a captured timeline entry says. Only the doors that
31
+ * leave a record the console can open are named; a newsletter opt-in, an
32
+ * API create, an import or a by-hand add leave nothing to open, so they
33
+ * carry no link and no label.
34
+ */
35
+ export declare const INTERACTION_LINK_LABELS: Partial<Record<ContactSource, string>>;
36
+ /**
37
+ * The console page a captured interaction points back at, or `null` when
38
+ * the door left nothing to open, or its owner publishes no address here.
39
+ *
40
+ * A submission and an order are addressed by the `refId` the capture door
41
+ * stamped — `formSubmissions/{id}`, `orders/{id}` — and an interaction that
42
+ * predates the stamp lands on nothing rather than on a list that would read
43
+ * as "the record is gone". A booking and a member sign-up land on their
44
+ * pages, which list them; neither page addresses one row.
45
+ */
46
+ export declare function contactInteractionHref(interaction: Pick<ContactInteraction, 'type' | 'refId'>, context: PluginRecordRouteContext): string | null;
@@ -0,0 +1,65 @@
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
+ */ import { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
17
+ import { pluginRecordHref, pluginRecordListHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
18
+ /**
19
+ * Where a contact's captured history points back at (AGL-2622): the record
20
+ * each capture door left, where the console reads it.
21
+ *
22
+ * The submission, the order and the booking are other plugins' records, so
23
+ * their addresses are the ones those plugins publish on the record-route seam
24
+ * (`formSubmission`, `order`, `booking` — AGL-3080), and an entry whose owner
25
+ * is not loaded carries no link. A member sign-up lands on the platform's own
26
+ * Users page.
27
+ */ /**
28
+ * What the link on a captured timeline entry says. Only the doors that
29
+ * leave a record the console can open are named; a newsletter opt-in, an
30
+ * API create, an import or a by-hand add leave nothing to open, so they
31
+ * carry no link and no label.
32
+ */ export const INTERACTION_LINK_LABELS = {
33
+ form: 'Open submission',
34
+ order: 'Open order',
35
+ booking: 'Open bookings',
36
+ member: 'Open site users'
37
+ };
38
+ /**
39
+ * The console page a captured interaction points back at, or `null` when
40
+ * the door left nothing to open, or its owner publishes no address here.
41
+ *
42
+ * A submission and an order are addressed by the `refId` the capture door
43
+ * stamped — `formSubmissions/{id}`, `orders/{id}` — and an interaction that
44
+ * predates the stamp lands on nothing rather than on a list that would read
45
+ * as "the record is gone". A booking and a member sign-up land on their
46
+ * pages, which list them; neither page addresses one row.
47
+ */ export function contactInteractionHref(interaction, context) {
48
+ switch(interaction.type){
49
+ case 'form':
50
+ return interaction.refId ? pluginRecordHref('formSubmission', context, interaction.refId) : null;
51
+ case 'order':
52
+ return interaction.refId ? pluginRecordHref('order', context, interaction.refId) : null;
53
+ case 'booking':
54
+ return pluginRecordListHref('booking', context);
55
+ case 'member':
56
+ return context.host ? buildRoute(Route.HOST_USERS, {
57
+ orgSlug: context.orgSlug,
58
+ host: context.host
59
+ }) : null;
60
+ default:
61
+ return null;
62
+ }
63
+ }
64
+
65
+ //# sourceMappingURL=contact-interaction-links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-interaction-links.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\nimport type { ContactInteraction, ContactSource } from '@aglyn/aglyn'\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport {\n pluginRecordHref,\n pluginRecordListHref,\n type PluginRecordRouteContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\n\n/**\n * Where a contact's captured history points back at (AGL-2622): the record\n * each capture door left, where the console reads it.\n *\n * The submission, the order and the booking are other plugins' records, so\n * their addresses are the ones those plugins publish on the record-route seam\n * (`formSubmission`, `order`, `booking` — AGL-3080), and an entry whose owner\n * is not loaded carries no link. A member sign-up lands on the platform's own\n * Users page.\n */\n\n/**\n * What the link on a captured timeline entry says. Only the doors that\n * leave a record the console can open are named; a newsletter opt-in, an\n * API create, an import or a by-hand add leave nothing to open, so they\n * carry no link and no label.\n */\nexport const INTERACTION_LINK_LABELS: Partial<Record<ContactSource, string>> = {\n form: 'Open submission',\n order: 'Open order',\n booking: 'Open bookings',\n member: 'Open site users',\n}\n\n/**\n * The console page a captured interaction points back at, or `null` when\n * the door left nothing to open, or its owner publishes no address here.\n *\n * A submission and an order are addressed by the `refId` the capture door\n * stamped — `formSubmissions/{id}`, `orders/{id}` — and an interaction that\n * predates the stamp lands on nothing rather than on a list that would read\n * as \"the record is gone\". A booking and a member sign-up land on their\n * pages, which list them; neither page addresses one row.\n */\nexport function contactInteractionHref(\n interaction: Pick<ContactInteraction, 'type' | 'refId'>,\n context: PluginRecordRouteContext,\n): string | null {\n switch (interaction.type) {\n case 'form':\n return interaction.refId ? pluginRecordHref('formSubmission', context, interaction.refId) : null\n case 'order':\n return interaction.refId ? pluginRecordHref('order', context, interaction.refId) : null\n case 'booking':\n return pluginRecordListHref('booking', context)\n case 'member':\n return context.host\n ? buildRoute(Route.HOST_USERS, { orgSlug: context.orgSlug, host: context.host })\n : null\n default:\n return null\n }\n}\n"],"names":["buildRoute","Route","pluginRecordHref","pluginRecordListHref","INTERACTION_LINK_LABELS","form","order","booking","member","contactInteractionHref","interaction","context","type","refId","host","HOST_USERS","orgSlug"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AACzE,SACEC,gBAAgB,EAChBC,oBAAoB,QAEf,mDAAkD;AAEzD;;;;;;;;;CASC,GAED;;;;;CAKC,GACD,OAAO,MAAMC,0BAAkE;IAC7EC,MAAM;IACNC,OAAO;IACPC,SAAS;IACTC,QAAQ;AACV,EAAC;AAED;;;;;;;;;CASC,GACD,OAAO,SAASC,uBACdC,WAAuD,EACvDC,OAAiC;IAEjC,OAAQD,YAAYE,IAAI;QACtB,KAAK;YACH,OAAOF,YAAYG,KAAK,GAAGX,iBAAiB,kBAAkBS,SAASD,YAAYG,KAAK,IAAI;QAC9F,KAAK;YACH,OAAOH,YAAYG,KAAK,GAAGX,iBAAiB,SAASS,SAASD,YAAYG,KAAK,IAAI;QACrF,KAAK;YACH,OAAOV,qBAAqB,WAAWQ;QACzC,KAAK;YACH,OAAOA,QAAQG,IAAI,GACfd,WAAWC,MAAMc,UAAU,EAAE;gBAAEC,SAASL,QAAQK,OAAO;gBAAEF,MAAMH,QAAQG,IAAI;YAAC,KAC5E;QACN;YACE,OAAO;IACX;AACF"}
@@ -0,0 +1,136 @@
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
+ */
17
+ /**
18
+ * MERGING TWO CONTACT DOCUMENTS INTO ONE (AGL-2625) — the pure half.
19
+ *
20
+ * Contacts are keyed by email, so exact duplicates cannot exist; one human
21
+ * with `jane@acme.com` and `jane@gmail.com` is two documents, with tags,
22
+ * deals, tasks and a timeline split between them. A merge folds the second
23
+ * into the first: the SURVIVOR keeps its address as the identity, the MERGED
24
+ * record's address becomes an alternate on it, and every field is combined
25
+ * by one rule the dialog can show before the write happens.
26
+ *
27
+ * ## The rule
28
+ *
29
+ * - A scalar — a name, a phone, a title, a stage, an owner — is the
30
+ * survivor's where the survivor has one, and fills from the merged record
31
+ * where the survivor is empty. Nothing the survivor holds is overwritten.
32
+ * - A list — tags, campaign filings, the timeline, the sites that captured
33
+ * the person, the forms, the companies — is the UNION, deduplicated and
34
+ * bounded where the write paths bound it.
35
+ * - A figure that counts something — orders, lifetime value, refunds — is
36
+ * the SUM: two records of one person's purchases are one person's
37
+ * purchases. A "first" is the earlier, a "last" is the later.
38
+ * - Notes are appended, survivor first: a note is a list of sentences, and
39
+ * dropping the merged record's would be the one silent loss in a merge
40
+ * that is otherwise lossless.
41
+ * - A recorded REFUSAL of marketing mail on either record stands on the
42
+ * survivor. The merge asserts the two are one person, and that person
43
+ * said no.
44
+ *
45
+ * Applied per holder: each group's facet on the merged record is folded
46
+ * into the same group's facet on the survivor, and a group only the merged
47
+ * record had comes across whole. No holder's records reach another holder's
48
+ * facet, which is the guarantee the facet shape exists for.
49
+ *
50
+ * ## Why this is a plan and not a write
51
+ *
52
+ * The console shows the two records side by side and says which value wins
53
+ * before anything is committed; the server performs the write. Both read
54
+ * this function, so the preview cannot show one outcome and the write land
55
+ * another. The plan is plain values — no sentinels — so it can be rendered,
56
+ * and it is shaped for a merge-`set`, which deep-merges the facet map one
57
+ * holder at a time and replaces an array whole: exactly the two behaviors
58
+ * the rule above needs.
59
+ */
60
+ import { type ContactInteraction } from '@aglyn/aglyn/app-utils/contacts';
61
+ export interface ContactMergePlan {
62
+ /**
63
+ * What the survivor becomes, as a merge-`set` payload: every derived
64
+ * field with its full value, and only the groups the merged record held
65
+ * under `facets`. Plain values throughout — the writer adds its own
66
+ * `updatedAt`.
67
+ */
68
+ survivor: Record<string, unknown>;
69
+ /** Every address the survivor answers to afterwards, primary first. */
70
+ emails: string[];
71
+ /** The merged record's addresses — what its leads and index entries are keyed by. */
72
+ mergedEmails: string[];
73
+ /**
74
+ * The companies that lose a contact: those BOTH records were filed under,
75
+ * which counted two people and now count one. A company only one record
76
+ * named keeps its count — the survivor still names it.
77
+ */
78
+ companyCounts: Array<{
79
+ companyId: string;
80
+ delta: -1;
81
+ }>;
82
+ /** The union of both records' scope tokens — who may read the survivor. */
83
+ visibleTo: string[];
84
+ }
85
+ type Doc = Record<string, unknown>;
86
+ /**
87
+ * Both timelines as one: deduplicated by event, newest first, and capped
88
+ * the way every capture caps it — so a merge of two full timelines keeps
89
+ * the fifty most recent visits rather than growing the document past what
90
+ * a capture may write.
91
+ */
92
+ export declare function mergeInteractions(survivor: readonly ContactInteraction[] | undefined, merged: readonly ContactInteraction[] | undefined): ContactInteraction[];
93
+ /**
94
+ * One holder's facet after the merge.
95
+ *
96
+ * `survivor` is that holder's facet on the surviving record — possibly
97
+ * absent, when only the merged record was held by this group — and `merged`
98
+ * the same holder's facet on the record being folded in.
99
+ */
100
+ export declare function mergeContactFacet(survivor: Doc | undefined, merged: Doc | undefined): Doc;
101
+ /**
102
+ * The plan for folding `merged` into `survivor`.
103
+ *
104
+ * Both are the stored documents as read. The survivor's `email` is the
105
+ * identity that stands; the merged record's becomes an alternate.
106
+ */
107
+ export declare function planContactMerge(survivor: Doc, merged: Doc): ContactMergePlan;
108
+ /** Where a previewed value comes from. */
109
+ export type ContactMergeSource = 'survivor' | 'merged' | 'both' | 'none';
110
+ /** One line of the side-by-side the dialog shows. */
111
+ export interface ContactMergePreviewRow {
112
+ key: string;
113
+ label: string;
114
+ /** The surviving record's value, as text. */
115
+ survivor: string;
116
+ /** The merged record's value, as text. */
117
+ merged: string;
118
+ /** What the survivor carries afterwards. */
119
+ result: string;
120
+ from: ContactMergeSource;
121
+ }
122
+ export interface ContactMergePreviewOptions {
123
+ /** The owner's display name for a uid; the uid itself when absent. */
124
+ memberName?: (uid: string) => string;
125
+ }
126
+ /**
127
+ * The merge as the viewing group would see it, field by field.
128
+ *
129
+ * Reads through ONE group's facet, the way the record page does, so a
130
+ * holder previewing a merge sees its own values and never another holder's.
131
+ * Every row carries the survivor's value, the merged record's, and the
132
+ * result the plan would write, so the dialog is a rendering and not a second
133
+ * opinion.
134
+ */
135
+ export declare function contactMergePreview(survivor: Doc, merged: Doc, groupId: string, options?: ContactMergePreviewOptions): ContactMergePreviewRow[];
136
+ export {};