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

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 (346) hide show
  1. package/README.md +2 -0
  2. package/package.json +14 -14
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/components/companies-bulk-bar.js +12 -4
  6. package/src/lib/components/companies-bulk-bar.js.map +1 -1
  7. package/src/lib/components/company-detail-page.js +11 -0
  8. package/src/lib/components/company-detail-page.js.map +1 -1
  9. package/src/lib/components/company-edit-drawer.js +12 -2
  10. package/src/lib/components/company-edit-drawer.js.map +1 -1
  11. package/src/lib/components/company-import-drawer.d.ts +1 -1
  12. package/src/lib/components/company-picker.js +10 -3
  13. package/src/lib/components/company-picker.js.map +1 -1
  14. package/src/lib/components/contact-associations-card.js +4 -3
  15. package/src/lib/components/contact-associations-card.js.map +1 -1
  16. package/src/lib/components/contact-detail-page.d.ts +4 -4
  17. package/src/lib/components/contact-detail-page.js +50 -53
  18. package/src/lib/components/contact-detail-page.js.map +1 -1
  19. package/src/lib/components/contact-list-columns.d.ts +3 -0
  20. package/src/lib/components/contact-list-columns.js +21 -5
  21. package/src/lib/components/contact-list-columns.js.map +1 -1
  22. package/src/lib/components/contact-merge-dialog.js +2 -1
  23. package/src/lib/components/contact-merge-dialog.js.map +1 -1
  24. package/src/lib/components/contact-timeline-card.js +4 -3
  25. package/src/lib/components/contact-timeline-card.js.map +1 -1
  26. package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
  27. package/src/lib/components/contacts-bulk-bar.js +24 -51
  28. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  29. package/src/lib/components/contacts-section.js +9 -3
  30. package/src/lib/components/contacts-section.js.map +1 -1
  31. package/src/lib/components/crm-booking-zone.d.ts +8 -2
  32. package/src/lib/components/crm-booking-zone.js.map +1 -1
  33. package/src/lib/components/crm-export-all-button.d.ts +1 -1
  34. package/src/lib/components/crm-export-all-button.js.map +1 -1
  35. package/src/lib/components/crm-glance-card.js +2 -1
  36. package/src/lib/components/crm-glance-card.js.map +1 -1
  37. package/src/lib/components/crm-send-email-dialog.js +9 -6
  38. package/src/lib/components/crm-send-email-dialog.js.map +1 -1
  39. package/src/lib/components/csv-import-drawer.js +2 -2
  40. package/src/lib/components/csv-import-drawer.js.map +1 -1
  41. package/src/lib/components/deal-detail-page.js +18 -2
  42. package/src/lib/components/deal-detail-page.js.map +1 -1
  43. package/src/lib/components/deal-edit-drawer.js +10 -0
  44. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  45. package/src/lib/components/deal-products-card.js +27 -16
  46. package/src/lib/components/deal-products-card.js.map +1 -1
  47. package/src/lib/components/deals-bulk-bar.js +13 -5
  48. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  49. package/src/lib/components/lead-campaigns-card.d.ts +4 -4
  50. package/src/lib/components/lead-campaigns-card.js +15 -8
  51. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  52. package/src/lib/components/lead-detail-page.js +25 -5
  53. package/src/lib/components/lead-detail-page.js.map +1 -1
  54. package/src/lib/components/lead-properties-card.js +10 -0
  55. package/src/lib/components/lead-properties-card.js.map +1 -1
  56. package/src/lib/components/lead-status-chip.js +3 -1
  57. package/src/lib/components/lead-status-chip.js.map +1 -1
  58. package/src/lib/components/lead-surfaces-note.js +4 -5
  59. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  60. package/src/lib/components/lead-unqualify-dialog.js +6 -0
  61. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  62. package/src/lib/components/leads-bulk-bar.js +34 -11
  63. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  64. package/src/lib/components/leads-section.js +41 -8
  65. package/src/lib/components/leads-section.js.map +1 -1
  66. package/src/lib/components/new-lead-drawer.js +5 -4
  67. package/src/lib/components/new-lead-drawer.js.map +1 -1
  68. package/src/lib/components/org-lead-surfaces-note.js +6 -2
  69. package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
  70. package/src/lib/components/recipes-card.js +9 -4
  71. package/src/lib/components/recipes-card.js.map +1 -1
  72. package/src/lib/components/record-sharing-card.d.ts +57 -0
  73. package/src/lib/components/record-sharing-card.js +244 -0
  74. package/src/lib/components/record-sharing-card.js.map +1 -0
  75. package/src/lib/components/reports/activity-card.js +4 -3
  76. package/src/lib/components/reports/activity-card.js.map +1 -1
  77. package/src/lib/components/reports/closed-deals-card.js +4 -3
  78. package/src/lib/components/reports/closed-deals-card.js.map +1 -1
  79. package/src/lib/components/reports/contacts-mix-card.js +3 -2
  80. package/src/lib/components/reports/contacts-mix-card.js.map +1 -1
  81. package/src/lib/components/reports/contacts-trend-card.js +2 -1
  82. package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
  83. package/src/lib/components/reports/forecast-card.js +3 -2
  84. package/src/lib/components/reports/forecast-card.js.map +1 -1
  85. package/src/lib/components/reports/lead-funnel-card.js +8 -6
  86. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  87. package/src/lib/components/reports/pipeline-card.js +4 -3
  88. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  89. package/src/lib/components/reports/report-format.d.ts +1 -1
  90. package/src/lib/components/reports/report-format.js.map +1 -1
  91. package/src/lib/components/reports/report-scope.d.ts +1 -1
  92. package/src/lib/components/reports/report-scope.js.map +1 -1
  93. package/src/lib/components/reports/source-conversion-card.js +2 -1
  94. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  95. package/src/lib/components/reports/tasks-card.js +2 -1
  96. package/src/lib/components/reports/tasks-card.js.map +1 -1
  97. package/src/lib/components/reports/won-lost-by-owner-card.js +3 -2
  98. package/src/lib/components/reports/won-lost-by-owner-card.js.map +1 -1
  99. package/src/lib/components/reports-section.js +4 -4
  100. package/src/lib/components/reports-section.js.map +1 -1
  101. package/src/lib/components/settings-section.js +7 -0
  102. package/src/lib/components/settings-section.js.map +1 -1
  103. package/src/lib/components/share-records-dialog.d.ts +28 -0
  104. package/src/lib/components/share-records-dialog.js +184 -0
  105. package/src/lib/components/share-records-dialog.js.map +1 -0
  106. package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
  107. package/src/lib/components/sharing-rule-drawer.js +374 -0
  108. package/src/lib/components/sharing-rule-drawer.js.map +1 -0
  109. package/src/lib/components/sharing-rules-card.d.ts +24 -0
  110. package/src/lib/components/sharing-rules-card.js +287 -0
  111. package/src/lib/components/sharing-rules-card.js.map +1 -0
  112. package/src/lib/components/use-crm-api.d.ts +1 -1
  113. package/src/lib/components/use-crm-api.js.map +1 -1
  114. package/src/lib/components/use-site-route-context.d.ts +25 -0
  115. package/src/lib/components/use-site-route-context.js +58 -0
  116. package/src/lib/components/use-site-route-context.js.map +1 -0
  117. package/src/lib/constants/api-routes.d.ts +31 -0
  118. package/src/lib/constants/api-routes.js +27 -1
  119. package/src/lib/constants/api-routes.js.map +1 -1
  120. package/src/lib/constants/bundle-common.d.ts +2 -0
  121. package/src/lib/constants/bundle-common.js +1 -0
  122. package/src/lib/constants/bundle-common.js.map +1 -1
  123. package/src/lib/declarations.console-server.d.ts +41 -0
  124. package/src/lib/declarations.console-server.js +115 -0
  125. package/src/lib/declarations.console-server.js.map +1 -0
  126. package/src/lib/declarations.server.d.ts +12 -0
  127. package/src/lib/declarations.server.js +84 -1
  128. package/src/lib/declarations.server.js.map +1 -1
  129. package/src/lib/hooks/use-contact-remove.d.ts +35 -0
  130. package/src/lib/hooks/use-contact-remove.js +77 -0
  131. package/src/lib/hooks/use-contact-remove.js.map +1 -0
  132. package/src/lib/hooks/use-crm-campaigns.d.ts +2 -2
  133. package/src/lib/hooks/use-crm-campaigns.js +3 -3
  134. package/src/lib/hooks/use-crm-campaigns.js.map +1 -1
  135. package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
  136. package/src/lib/hooks/use-crm-sharing.js +109 -0
  137. package/src/lib/hooks/use-crm-sharing.js.map +1 -0
  138. package/src/lib/model/campaign-filing-activity.d.ts +1 -1
  139. package/src/lib/model/campaign-filing-activity.js +1 -1
  140. package/src/lib/model/campaign-filing-activity.js.map +1 -1
  141. package/src/lib/model/companies-csv.d.ts +1 -1
  142. package/src/lib/model/companies-csv.js +2 -2
  143. package/src/lib/model/companies-csv.js.map +1 -1
  144. package/src/lib/model/contact-holder.d.ts +68 -0
  145. package/src/lib/model/contact-holder.js +129 -0
  146. package/src/lib/model/contact-holder.js.map +1 -0
  147. package/src/lib/model/contact-interaction-links.d.ts +46 -0
  148. package/src/lib/model/contact-interaction-links.js +65 -0
  149. package/src/lib/model/contact-interaction-links.js.map +1 -0
  150. package/src/lib/model/contact-merge.d.ts +136 -0
  151. package/src/lib/model/contact-merge.js +412 -0
  152. package/src/lib/model/contact-merge.js.map +1 -0
  153. package/src/lib/model/contact-record.d.ts +7 -1
  154. package/src/lib/model/contact-record.js +7 -6
  155. package/src/lib/model/contact-record.js.map +1 -1
  156. package/src/lib/model/contact-remove.d.ts +48 -0
  157. package/src/lib/model/contact-remove.js +22 -0
  158. package/src/lib/model/contact-remove.js.map +1 -0
  159. package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
  160. package/src/lib/model/contacts-bulk-writes.js +2 -82
  161. package/src/lib/model/contacts-bulk-writes.js.map +1 -1
  162. package/src/lib/model/contacts-csv.d.ts +2 -2
  163. package/src/lib/model/contacts-csv.js +2 -2
  164. package/src/lib/model/contacts-csv.js.map +1 -1
  165. package/src/lib/model/crm-csv.d.ts +239 -0
  166. package/src/lib/model/crm-csv.js +467 -0
  167. package/src/lib/model/crm-csv.js.map +1 -0
  168. package/src/lib/model/crm-digest.d.ts +227 -0
  169. package/src/lib/model/crm-digest.js +336 -0
  170. package/src/lib/model/crm-digest.js.map +1 -0
  171. package/src/lib/model/crm-import.d.ts +7 -1
  172. package/src/lib/model/crm-import.js +2 -1
  173. package/src/lib/model/crm-import.js.map +1 -1
  174. package/src/lib/model/crm-inbound.d.ts +162 -0
  175. package/src/lib/model/crm-inbound.js +208 -0
  176. package/src/lib/model/crm-inbound.js.map +1 -0
  177. package/src/lib/model/crm-lead-import.d.ts +1 -1
  178. package/src/lib/model/crm-lead-import.js +4 -4
  179. package/src/lib/model/crm-lead-import.js.map +1 -1
  180. package/src/lib/model/crm-record-routes.d.ts +5 -0
  181. package/src/lib/model/crm-record-routes.js +7 -0
  182. package/src/lib/model/crm-record-routes.js.map +1 -1
  183. package/src/lib/model/crm-reports.d.ts +467 -0
  184. package/src/lib/model/crm-reports.js +667 -0
  185. package/src/lib/model/crm-reports.js.map +1 -0
  186. package/src/lib/model/crm-routes.d.ts +6 -5
  187. package/src/lib/model/crm-routes.js +7 -7
  188. package/src/lib/model/crm-routes.js.map +1 -1
  189. package/src/lib/model/crm-search-sources.d.ts +15 -0
  190. package/src/lib/model/crm-search-sources.js +162 -0
  191. package/src/lib/model/crm-search-sources.js.map +1 -0
  192. package/src/lib/model/crm-sharing-api.d.ts +32 -0
  193. package/src/lib/model/crm-sharing-api.js +59 -0
  194. package/src/lib/model/crm-sharing-api.js.map +1 -0
  195. package/src/lib/model/crm-sharing.d.ts +333 -0
  196. package/src/lib/model/crm-sharing.js +603 -0
  197. package/src/lib/model/crm-sharing.js.map +1 -0
  198. package/src/lib/model/deals-csv.d.ts +1 -1
  199. package/src/lib/model/deals-csv.js +4 -5
  200. package/src/lib/model/deals-csv.js.map +1 -1
  201. package/src/lib/model/lead-filters.d.ts +5 -4
  202. package/src/lib/model/lead-filters.js +7 -5
  203. package/src/lib/model/lead-filters.js.map +1 -1
  204. package/src/lib/model/leads-csv.d.ts +1 -1
  205. package/src/lib/model/leads-csv.js +1 -1
  206. package/src/lib/model/leads-csv.js.map +1 -1
  207. package/src/lib/model/tasks-csv.d.ts +1 -1
  208. package/src/lib/model/tasks-csv.js +1 -1
  209. package/src/lib/model/tasks-csv.js.map +1 -1
  210. package/src/lib/plan-entitlements.d.ts +75 -0
  211. package/src/lib/plan-entitlements.js +56 -0
  212. package/src/lib/plan-entitlements.js.map +1 -0
  213. package/src/lib/plugin.js +39 -0
  214. package/src/lib/plugin.js.map +1 -1
  215. package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
  216. package/src/lib/server/api-v1/contacts-merge.js +120 -0
  217. package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
  218. package/src/lib/server/api-v1/contacts.d.ts +18 -0
  219. package/src/lib/server/api-v1/contacts.js +1076 -0
  220. package/src/lib/server/api-v1/contacts.js.map +1 -0
  221. package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
  222. package/src/lib/server/api-v1/crm-activities.js +269 -0
  223. package/src/lib/server/api-v1/crm-activities.js.map +1 -0
  224. package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
  225. package/src/lib/server/api-v1/crm-companies.js +391 -0
  226. package/src/lib/server/api-v1/crm-companies.js.map +1 -0
  227. package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
  228. package/src/lib/server/api-v1/crm-deals.js +631 -0
  229. package/src/lib/server/api-v1/crm-deals.js.map +1 -0
  230. package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
  231. package/src/lib/server/api-v1/crm-email-templates.js +336 -0
  232. package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
  233. package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
  234. package/src/lib/server/api-v1/crm-leads.js +833 -0
  235. package/src/lib/server/api-v1/crm-leads.js.map +1 -0
  236. package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
  237. package/src/lib/server/api-v1/crm-pipelines.js +146 -0
  238. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
  239. package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
  240. package/src/lib/server/api-v1/crm-shared.js +436 -0
  241. package/src/lib/server/api-v1/crm-shared.js.map +1 -0
  242. package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
  243. package/src/lib/server/api-v1/crm-tasks.js +398 -0
  244. package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
  245. package/src/lib/server/api-v1/openapi.d.ts +29 -0
  246. package/src/lib/server/api-v1/openapi.js +985 -0
  247. package/src/lib/server/api-v1/openapi.js.map +1 -0
  248. package/src/lib/server/api-v1/usage.d.ts +26 -0
  249. package/src/lib/server/api-v1/usage.js +73 -0
  250. package/src/lib/server/api-v1/usage.js.map +1 -0
  251. package/src/lib/server/campaign-filing-activity.d.ts +1 -1
  252. package/src/lib/server/campaign-filing-activity.js +7 -11
  253. package/src/lib/server/campaign-filing-activity.js.map +1 -1
  254. package/src/lib/server/capture-contact.js +15 -4
  255. package/src/lib/server/capture-contact.js.map +1 -1
  256. package/src/lib/server/consent-group-participant.js +1 -1
  257. package/src/lib/server/consent-group-participant.js.map +1 -1
  258. package/src/lib/server/contact-email-engagement.d.ts +33 -0
  259. package/src/lib/server/contact-email-engagement.js +146 -0
  260. package/src/lib/server/contact-email-engagement.js.map +1 -0
  261. package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
  262. package/src/lib/server/contact-lifecycle-floor.js +91 -0
  263. package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
  264. package/src/lib/server/contact-remove.d.ts +75 -0
  265. package/src/lib/server/contact-remove.js +244 -0
  266. package/src/lib/server/contact-remove.js.map +1 -0
  267. package/src/lib/server/contact-update.js +4 -3
  268. package/src/lib/server/contact-update.js.map +1 -1
  269. package/src/lib/server/contacts-import.d.ts +47 -11
  270. package/src/lib/server/contacts-import.js +171 -71
  271. package/src/lib/server/contacts-import.js.map +1 -1
  272. package/src/lib/server/contacts-merge.d.ts +1 -1
  273. package/src/lib/server/contacts-merge.js +3 -2
  274. package/src/lib/server/contacts-merge.js.map +1 -1
  275. package/src/lib/server/container-detach.d.ts +31 -0
  276. package/src/lib/server/container-detach.js +115 -0
  277. package/src/lib/server/container-detach.js.map +1 -0
  278. package/src/lib/server/convert-host-lead.d.ts +189 -0
  279. package/src/lib/server/convert-host-lead.js +518 -0
  280. package/src/lib/server/convert-host-lead.js.map +1 -0
  281. package/src/lib/server/crm-inbound-email.d.ts +162 -0
  282. package/src/lib/server/crm-inbound-email.js +259 -0
  283. package/src/lib/server/crm-inbound-email.js.map +1 -0
  284. package/src/lib/server/crm-records-meter.d.ts +50 -0
  285. package/src/lib/server/crm-records-meter.js +126 -0
  286. package/src/lib/server/crm-records-meter.js.map +1 -0
  287. package/src/lib/server/crm-sharing.d.ts +108 -0
  288. package/src/lib/server/crm-sharing.js +790 -0
  289. package/src/lib/server/crm-sharing.js.map +1 -0
  290. package/src/lib/server/daily-digest-route.d.ts +101 -0
  291. package/src/lib/server/daily-digest-route.js +578 -0
  292. package/src/lib/server/daily-digest-route.js.map +1 -0
  293. package/src/lib/server/export-route.d.ts +60 -0
  294. package/src/lib/server/export-route.js +447 -0
  295. package/src/lib/server/export-route.js.map +1 -0
  296. package/src/lib/server/inbound-address.js +5 -3
  297. package/src/lib/server/inbound-address.js.map +1 -1
  298. package/src/lib/server/inbound-route.d.ts +74 -0
  299. package/src/lib/server/inbound-route.js +260 -0
  300. package/src/lib/server/inbound-route.js.map +1 -0
  301. package/src/lib/server/lead-campaign-carry.js +3 -3
  302. package/src/lib/server/lead-campaign-carry.js.map +1 -1
  303. package/src/lib/server/lead-convert.d.ts +1 -1
  304. package/src/lib/server/lead-convert.js +2 -2
  305. package/src/lib/server/lead-convert.js.map +1 -1
  306. package/src/lib/server/lead-create.d.ts +1 -1
  307. package/src/lib/server/lead-create.js +10 -15
  308. package/src/lib/server/lead-create.js.map +1 -1
  309. package/src/lib/server/lead-nurturing.d.ts +23 -0
  310. package/src/lib/server/lead-nurturing.js +82 -0
  311. package/src/lib/server/lead-nurturing.js.map +1 -0
  312. package/src/lib/server/leads-import.js +7 -7
  313. package/src/lib/server/leads-import.js.map +1 -1
  314. package/src/lib/server/merge-contacts.d.ts +55 -0
  315. package/src/lib/server/merge-contacts.js +274 -0
  316. package/src/lib/server/merge-contacts.js.map +1 -0
  317. package/src/lib/server/pipeline-record-index.d.ts +36 -0
  318. package/src/lib/server/pipeline-record-index.js +87 -0
  319. package/src/lib/server/pipeline-record-index.js.map +1 -0
  320. package/src/lib/server/record-email-state.js +23 -0
  321. package/src/lib/server/record-email-state.js.map +1 -1
  322. package/src/lib/server/record-facts.js +2 -1
  323. package/src/lib/server/record-facts.js.map +1 -1
  324. package/src/lib/server/record-timeline.d.ts +12 -0
  325. package/src/lib/server/record-timeline.js +127 -15
  326. package/src/lib/server/record-timeline.js.map +1 -1
  327. package/src/lib/server/task-reminders-route.d.ts +82 -0
  328. package/src/lib/server/task-reminders-route.js +499 -0
  329. package/src/lib/server/task-reminders-route.js.map +1 -0
  330. package/src/lib/server/task-routes.d.ts +2 -0
  331. package/src/lib/server/task-routes.js +22 -3
  332. package/src/lib/server/task-routes.js.map +1 -1
  333. package/src/lib/server-deal-stage.js +2 -1
  334. package/src/lib/server-deal-stage.js.map +1 -1
  335. package/src/lib/server.d.ts +5 -2
  336. package/src/lib/server.js +47 -2
  337. package/src/lib/server.js.map +1 -1
  338. package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
  339. package/src/lib/testing/stand-in-site-record-routes.js +55 -0
  340. package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
  341. package/src/lib/usage-axes.d.ts +35 -0
  342. package/src/lib/usage-axes.js +75 -0
  343. package/src/lib/usage-axes.js.map +1 -0
  344. package/src/lib/components/use-emails-hub-path.d.ts +0 -24
  345. package/src/lib/components/use-emails-hub-path.js +0 -59
  346. package/src/lib/components/use-emails-hub-path.js.map +0 -1
@@ -14,12 +14,12 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import * as Aglyn from "@aglyn/aglyn";
17
+ import { crmShareChipFor } from "./crm-sharing.js";
17
18
  /*
18
- * `NO_HOLDER_GROUP` and `contactPrimaryGroup` moved to `@aglyn/aglyn`
19
- * under AGL-2662 so the server's whole-collection export can flatten a
20
- * contact the way the organization-level list does — the console app may
21
- * not import this plugin. Re-exported here, so every caller is unchanged.
22
- */ export { contactPrimaryGroup, NO_HOLDER_GROUP } from "@aglyn/aglyn";
19
+ * Which holder a cross-site reader sees one contact through (AGL-2630) —
20
+ * `contact-holder.ts`, shared with the server's whole-collection export so
21
+ * the file and the organization-level list flatten a contact one way.
22
+ */ export { contactPrimaryGroup, NO_HOLDER_GROUP } from "./contact-holder.js";
23
23
  /** A document off the wire, flattened through one group's facet. */ export function contactRecordFromDoc(row, group) {
24
24
  var _row_$id, _facet_name, _facet_tags, _facet_notes, _facet_ltvCents, _facet_ordersCount, _facet_phone, _facet_jobTitle, _facet_leadSource, _facet_companyName, _facet_companyId, _facet_address, _facet_custom, _facet_mediaIds, _facet_ownerUid;
25
25
  const facet = Aglyn.readContactFacet(row, group.groupId);
@@ -37,7 +37,7 @@
37
37
  interactions: Aglyn.interactionsForGroup(facet.interactions, group.hostIds),
38
38
  tags: (_facet_tags = facet.tags) != null ? _facet_tags : [],
39
39
  notes: (_facet_notes = facet.notes) != null ? _facet_notes : '',
40
- campaignIds: Aglyn.readContactCampaignIds(row, group.groupId),
40
+ campaignIds: Aglyn.readContactContainerIds(row, group.groupId, 'campaign'),
41
41
  ltvCents: (_facet_ltvCents = facet.ltvCents) != null ? _facet_ltvCents : 0,
42
42
  ordersCount: (_facet_ordersCount = facet.ordersCount) != null ? _facet_ordersCount : 0,
43
43
  phone: (_facet_phone = facet.phone) != null ? _facet_phone : '',
@@ -54,6 +54,7 @@
54
54
  lastEmailEngagementAtMs: typeof facet.lastEmailEngagementAtMs === 'number' && Number.isFinite(facet.lastEmailEngagementAtMs) && facet.lastEmailEngagementAtMs > 0 ? facet.lastEmailEngagementAtMs : null,
55
55
  nextTaskAtMs: Aglyn.readNextTaskAtMs(row),
56
56
  emailState: Aglyn.readEmailState(row),
57
+ shareChip: crmShareChipFor(row, group.hostIds),
57
58
  createdAt: row['createdAt'],
58
59
  updatedAt: row['updatedAt']
59
60
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-record.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 * as Aglyn from '@aglyn/aglyn'\nimport type {\n AglynPostalAddress,\n ConsentGroup,\n ContactInteraction,\n ContactLifecycleStage,\n ContactSource,\n} from '@aglyn/aglyn'\n\n/**\n * One contact, as the VIEWING GROUP may see it (AGL-2596).\n *\n * A contact document is shared by every site in the org, and almost nothing\n * on it is legitimately shared: the notes, tags, timeline, profile and\n * commercial figures are the holder's own records and live in that holder's\n * facet. This is the row after the facet read — flat, so the list, the\n * record page and the export can render fields by name without any of them\n * reaching into `facets.{groupId}` for itself. One projection rather than a\n * facet read at every field is what keeps a surface from ever showing\n * another holder's records: there is no field on this shape that came from\n * the top of the document except the two that ARE shared, the address and\n * the canonical name.\n */\nexport interface ContactRecord {\n $id: string\n /**\n * The facet this row was flattened through — the viewing group under a\n * site, the person's primary holder at the org level (AGL-2630). A write\n * that edits the row goes back to the same facet, so a surface that has\n * only the flat record can still address the holder's own fields.\n */\n groupId: string\n /**\n * A site of that holder's group — the viewing site under a site, and at\n * the organization level the site the primary holder was resolved from —\n * which is where a stage move for this row is made and announced. `''` for\n * a row no site holds.\n */\n holderHostId: string\n /** Every site that has captured this person, sorted — the org-level \"Known by\". */\n capturedByHostIds: string[]\n /** The shared identity and the dedupe key. */\n email: string\n /** The other addresses a merge folded into this record (AGL-2625). */\n alternateEmails: string[]\n /** What THIS holder sees — their own override, or the canonical name. */\n name: string\n /** The canonical name: the identity of last resort, shared by every holder. */\n canonicalName: string\n /** This holder's own name for the person, when they have set one. */\n nameOverride: string\n sources: Partial<Record<ContactSource, true>>\n /** This holder's timeline, narrowed to the sites the group covers. */\n interactions: ContactInteraction[]\n tags: string[]\n notes: string\n campaignIds: string[]\n ltvCents: number\n ordersCount: number\n phone: string\n jobTitle: string\n /** This holder's lead source label (AGL-3298), `''` for none. */\n leadSource: string\n companyName: string\n companyId: string\n /**\n * What linking this person to a company has to know — this holder's link,\n * the shared mirror and the other holders' ids — so the properties card\n * and the bulk bar can plan a link from the row without the document. Not\n * a display field: the mirror is an index every reader of the document\n * already holds, and nothing renders it.\n */\n companyLink: Aglyn.ContactCompanyLinkState\n address: AglynPostalAddress | null\n /** This holder's custom field values, keyed by definition key (AGL-2601). */\n custom?: Record<string, Aglyn.ContactCustomValue>\n /** This holder's attached org-library files, by media id (AGL-2662). */\n mediaIds?: string[]\n ownerUid: string\n /** Empty when the holder has not placed the person in the funnel. */\n lifecycleStage: ContactLifecycleStage | ''\n /**\n * When the person last opened or clicked one of this holder's campaigns\n * (AGL-2616), or `null` when they never have since the stamp shipped.\n */\n lastEmailEngagementAtMs: number | null\n /**\n * When the earliest open task against this person is due (AGL-2661), or\n * `null` when nothing is scheduled. SHARED, not the holder's: a task is\n * filed against the contact document, and every holder reading the row\n * sees the same next step.\n */\n nextTaskAtMs: number | null\n /**\n * The last verdict on the address (AGL-3245) — bounced, blocked,\n * unsubscribed, do-not-contact — or `null` when nothing is known. SHARED\n * like the address it is about.\n */\n emailState: Aglyn.EmailState | null\n createdAt?: unknown\n updatedAt?: unknown\n}\n\n/*\n * `NO_HOLDER_GROUP` and `contactPrimaryGroup` moved to `@aglyn/aglyn`\n * under AGL-2662 so the server's whole-collection export can flatten a\n * contact the way the organization-level list does — the console app may\n * not import this plugin. Re-exported here, so every caller is unchanged.\n */\nexport { contactPrimaryGroup, NO_HOLDER_GROUP } from '@aglyn/aglyn'\n\n/** A document off the wire, flattened through one group's facet. */\nexport function contactRecordFromDoc(\n row: Record<string, any>,\n group: ConsentGroup,\n): ContactRecord {\n const facet = Aglyn.readContactFacet(row, group.groupId)\n return {\n $id: String(row['$id'] ?? ''),\n groupId: group.groupId,\n holderHostId: group.hostId,\n capturedByHostIds: Aglyn.contactCaptureHostIds(row),\n email: typeof row['email'] === 'string' ? row['email'] : '',\n alternateEmails: Array.isArray(row[Aglyn.CONTACT_ALTERNATE_EMAILS_FIELD])\n ? (row[Aglyn.CONTACT_ALTERNATE_EMAILS_FIELD] as unknown[]).filter(\n (email): email is string => typeof email === 'string',\n )\n : [],\n name: Aglyn.contactDisplayName(row, group.groupId),\n canonicalName: typeof row['name'] === 'string' ? row['name'] : '',\n nameOverride: facet.name ?? '',\n sources: facet.sources,\n interactions: Aglyn.interactionsForGroup(facet.interactions, group.hostIds),\n tags: facet.tags ?? [],\n notes: facet.notes ?? '',\n campaignIds: Aglyn.readContactCampaignIds(row, group.groupId),\n ltvCents: facet.ltvCents ?? 0,\n ordersCount: facet.ordersCount ?? 0,\n phone: facet.phone ?? '',\n jobTitle: facet.jobTitle ?? '',\n leadSource: facet.leadSource ?? '',\n companyName: facet.companyName ?? '',\n companyId: facet.companyId ?? '',\n companyLink: Aglyn.readContactCompanyLink(row, group.groupId),\n address: facet.address ?? null,\n custom: facet.custom ?? {},\n mediaIds: facet.mediaIds ?? [],\n ownerUid: facet.ownerUid ?? '',\n lifecycleStage: Aglyn.isContactLifecycleStage(facet.lifecycleStage)\n ? facet.lifecycleStage\n : '',\n lastEmailEngagementAtMs:\n typeof facet.lastEmailEngagementAtMs === 'number' &&\n Number.isFinite(facet.lastEmailEngagementAtMs) &&\n facet.lastEmailEngagementAtMs > 0\n ? facet.lastEmailEngagementAtMs\n : null,\n nextTaskAtMs: Aglyn.readNextTaskAtMs(row as { nextTaskAtMs?: unknown }),\n emailState: Aglyn.readEmailState(row),\n createdAt: row['createdAt'],\n updatedAt: row['updatedAt'],\n }\n}\n\n/**\n * Tags as typed — comma-separated — into the shape the facet stores:\n * lower-cased, trimmed, deduplicated and capped, so a tag typed on the\n * record page and one typed in the create drawer are the same tag to the\n * segment filter that matches on them.\n */\nexport function parseContactTags(input: string): string[] {\n return [\n ...new Set(\n input\n .split(',')\n .map((tag) => tag.trim().toLowerCase().slice(0, 40))\n .filter(Boolean)\n .slice(0, 20),\n ),\n ]\n}\n"],"names":["Aglyn","contactPrimaryGroup","NO_HOLDER_GROUP","contactRecordFromDoc","row","group","facet","readContactFacet","groupId","$id","String","holderHostId","hostId","capturedByHostIds","contactCaptureHostIds","email","alternateEmails","Array","isArray","CONTACT_ALTERNATE_EMAILS_FIELD","filter","name","contactDisplayName","canonicalName","nameOverride","sources","interactions","interactionsForGroup","hostIds","tags","notes","campaignIds","readContactCampaignIds","ltvCents","ordersCount","phone","jobTitle","leadSource","companyName","companyId","companyLink","readContactCompanyLink","address","custom","mediaIds","ownerUid","lifecycleStage","isContactLifecycleStage","lastEmailEngagementAtMs","Number","isFinite","nextTaskAtMs","readNextTaskAtMs","emailState","readEmailState","createdAt","updatedAt","parseContactTags","input","Set","split","map","tag","trim","toLowerCase","slice","Boolean"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-record.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 * as Aglyn from '@aglyn/aglyn'\nimport { type CrmShareChip, crmShareChipFor } from './crm-sharing'\nimport type {\n AglynPostalAddress,\n ConsentGroup,\n ContactInteraction,\n ContactLifecycleStage,\n ContactSource,\n} from '@aglyn/aglyn'\n\n/**\n * One contact, as the VIEWING GROUP may see it (AGL-2596).\n *\n * A contact document is shared by every site in the org, and almost nothing\n * on it is legitimately shared: the notes, tags, timeline, profile and\n * commercial figures are the holder's own records and live in that holder's\n * facet. This is the row after the facet read — flat, so the list, the\n * record page and the export can render fields by name without any of them\n * reaching into `facets.{groupId}` for itself. One projection rather than a\n * facet read at every field is what keeps a surface from ever showing\n * another holder's records: there is no field on this shape that came from\n * the top of the document except the two that ARE shared, the address and\n * the canonical name.\n */\nexport interface ContactRecord {\n $id: string\n /**\n * The facet this row was flattened through — the viewing group under a\n * site, the person's primary holder at the org level (AGL-2630). A write\n * that edits the row goes back to the same facet, so a surface that has\n * only the flat record can still address the holder's own fields.\n */\n groupId: string\n /**\n * A site of that holder's group — the viewing site under a site, and at\n * the organization level the site the primary holder was resolved from —\n * which is where a stage move for this row is made and announced. `''` for\n * a row no site holds.\n */\n holderHostId: string\n /** Every site that has captured this person, sorted — the org-level \"Known by\". */\n capturedByHostIds: string[]\n /** The shared identity and the dedupe key. */\n email: string\n /** The other addresses a merge folded into this record (AGL-2625). */\n alternateEmails: string[]\n /** What THIS holder sees — their own override, or the canonical name. */\n name: string\n /** The canonical name: the identity of last resort, shared by every holder. */\n canonicalName: string\n /** This holder's own name for the person, when they have set one. */\n nameOverride: string\n sources: Partial<Record<ContactSource, true>>\n /** This holder's timeline, narrowed to the sites the group covers. */\n interactions: ContactInteraction[]\n tags: string[]\n notes: string\n campaignIds: string[]\n ltvCents: number\n ordersCount: number\n phone: string\n jobTitle: string\n /** This holder's lead source label (AGL-3298), `''` for none. */\n leadSource: string\n companyName: string\n companyId: string\n /**\n * What linking this person to a company has to know — this holder's link,\n * the shared mirror and the other holders' ids — so the properties card\n * and the bulk bar can plan a link from the row without the document. Not\n * a display field: the mirror is an index every reader of the document\n * already holds, and nothing renders it.\n */\n companyLink: Aglyn.ContactCompanyLinkState\n address: AglynPostalAddress | null\n /** This holder's custom field values, keyed by definition key (AGL-2601). */\n custom?: Record<string, Aglyn.ContactCustomValue>\n /** This holder's attached org-library files, by media id (AGL-2662). */\n mediaIds?: string[]\n ownerUid: string\n /** Empty when the holder has not placed the person in the funnel. */\n lifecycleStage: ContactLifecycleStage | ''\n /**\n * When the person last opened or clicked one of this holder's campaigns\n * (AGL-2616), or `null` when they never have since the stamp shipped.\n */\n lastEmailEngagementAtMs: number | null\n /**\n * When the earliest open task against this person is due (AGL-2661), or\n * `null` when nothing is scheduled. SHARED, not the holder's: a task is\n * filed against the contact document, and every holder reading the row\n * sees the same next step.\n */\n nextTaskAtMs: number | null\n /**\n * The last verdict on the address (AGL-3245) — bounced, blocked,\n * unsubscribed, do-not-contact — or `null` when nothing is known. SHARED\n * like the address it is about.\n */\n emailState: Aglyn.EmailState | null\n /**\n * Why the viewing group sees this person when the answer is a share\n * (AGL-3336) — `null` when it holds them, and at the organization level.\n */\n shareChip?: CrmShareChip | null\n createdAt?: unknown\n updatedAt?: unknown\n}\n\n/*\n * Which holder a cross-site reader sees one contact through (AGL-2630) —\n * `contact-holder.ts`, shared with the server's whole-collection export so\n * the file and the organization-level list flatten a contact one way.\n */\nexport { contactPrimaryGroup, NO_HOLDER_GROUP } from './contact-holder'\n\n/** A document off the wire, flattened through one group's facet. */\nexport function contactRecordFromDoc(\n row: Record<string, any>,\n group: ConsentGroup,\n): ContactRecord {\n const facet = Aglyn.readContactFacet(row, group.groupId)\n return {\n $id: String(row['$id'] ?? ''),\n groupId: group.groupId,\n holderHostId: group.hostId,\n capturedByHostIds: Aglyn.contactCaptureHostIds(row),\n email: typeof row['email'] === 'string' ? row['email'] : '',\n alternateEmails: Array.isArray(row[Aglyn.CONTACT_ALTERNATE_EMAILS_FIELD])\n ? (row[Aglyn.CONTACT_ALTERNATE_EMAILS_FIELD] as unknown[]).filter(\n (email): email is string => typeof email === 'string',\n )\n : [],\n name: Aglyn.contactDisplayName(row, group.groupId),\n canonicalName: typeof row['name'] === 'string' ? row['name'] : '',\n nameOverride: facet.name ?? '',\n sources: facet.sources,\n interactions: Aglyn.interactionsForGroup(facet.interactions, group.hostIds),\n tags: facet.tags ?? [],\n notes: facet.notes ?? '',\n campaignIds: Aglyn.readContactContainerIds(row, group.groupId, 'campaign'),\n ltvCents: facet.ltvCents ?? 0,\n ordersCount: facet.ordersCount ?? 0,\n phone: facet.phone ?? '',\n jobTitle: facet.jobTitle ?? '',\n leadSource: facet.leadSource ?? '',\n companyName: facet.companyName ?? '',\n companyId: facet.companyId ?? '',\n companyLink: Aglyn.readContactCompanyLink(row, group.groupId),\n address: facet.address ?? null,\n custom: facet.custom ?? {},\n mediaIds: facet.mediaIds ?? [],\n ownerUid: facet.ownerUid ?? '',\n lifecycleStage: Aglyn.isContactLifecycleStage(facet.lifecycleStage)\n ? facet.lifecycleStage\n : '',\n lastEmailEngagementAtMs:\n typeof facet.lastEmailEngagementAtMs === 'number' &&\n Number.isFinite(facet.lastEmailEngagementAtMs) &&\n facet.lastEmailEngagementAtMs > 0\n ? facet.lastEmailEngagementAtMs\n : null,\n nextTaskAtMs: Aglyn.readNextTaskAtMs(row as { nextTaskAtMs?: unknown }),\n emailState: Aglyn.readEmailState(row),\n shareChip: crmShareChipFor(row, group.hostIds),\n createdAt: row['createdAt'],\n updatedAt: row['updatedAt'],\n }\n}\n\n/**\n * Tags as typed — comma-separated — into the shape the facet stores:\n * lower-cased, trimmed, deduplicated and capped, so a tag typed on the\n * record page and one typed in the create drawer are the same tag to the\n * segment filter that matches on them.\n */\nexport function parseContactTags(input: string): string[] {\n return [\n ...new Set(\n input\n .split(',')\n .map((tag) => tag.trim().toLowerCase().slice(0, 40))\n .filter(Boolean)\n .slice(0, 20),\n ),\n ]\n}\n"],"names":["Aglyn","crmShareChipFor","contactPrimaryGroup","NO_HOLDER_GROUP","contactRecordFromDoc","row","group","facet","readContactFacet","groupId","$id","String","holderHostId","hostId","capturedByHostIds","contactCaptureHostIds","email","alternateEmails","Array","isArray","CONTACT_ALTERNATE_EMAILS_FIELD","filter","name","contactDisplayName","canonicalName","nameOverride","sources","interactions","interactionsForGroup","hostIds","tags","notes","campaignIds","readContactContainerIds","ltvCents","ordersCount","phone","jobTitle","leadSource","companyName","companyId","companyLink","readContactCompanyLink","address","custom","mediaIds","ownerUid","lifecycleStage","isContactLifecycleStage","lastEmailEngagementAtMs","Number","isFinite","nextTaskAtMs","readNextTaskAtMs","emailState","readEmailState","shareChip","createdAt","updatedAt","parseContactTags","input","Set","split","map","tag","trim","toLowerCase","slice","Boolean"],"mappings":"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"}
@@ -0,0 +1,48 @@
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
+ * The contract of `crm/contact-remove` (AGL-3338), client-safe: what the
19
+ * record page's Delete and the contacts bar's Remove send, and what each
20
+ * contact is answered with.
21
+ */
22
+ /** The most contacts one request may name — the bar sends a selection in pieces this size. */
23
+ export declare const CRM_CONTACT_REMOVE_MAX = 200;
24
+ export interface ContactRemoveRequest {
25
+ /** The mounted site; at the organization level the record's own site, or nothing. */
26
+ hostId?: string | null;
27
+ /** The organization, which makes the call the route's organization variant. */
28
+ orgId?: string;
29
+ contactIds: string[];
30
+ }
31
+ /**
32
+ * One contact's answer: `detached` when other sites still hold the person
33
+ * and only this holder's half went, `deleted` when nobody else did and the
34
+ * document went with it — or refused, with a sentence written for the reader.
35
+ */
36
+ export type ContactRemoveOutcome = {
37
+ contactId: string;
38
+ ok: true;
39
+ removed: 'detached' | 'deleted';
40
+ } | {
41
+ contactId: string;
42
+ ok: false;
43
+ error: string;
44
+ };
45
+ export interface ContactRemoveResponse {
46
+ ok: true;
47
+ results: ContactRemoveOutcome[];
48
+ }
@@ -0,0 +1,22 @@
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
+ * The contract of `crm/contact-remove` (AGL-3338), client-safe: what the
18
+ * record page's Delete and the contacts bar's Remove send, and what each
19
+ * contact is answered with.
20
+ */ /** The most contacts one request may name — the bar sends a selection in pieces this size. */ export const CRM_CONTACT_REMOVE_MAX = 200;
21
+
22
+ //# sourceMappingURL=contact-remove.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contact-remove.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 contract of `crm/contact-remove` (AGL-3338), client-safe: what the\n * record page's Delete and the contacts bar's Remove send, and what each\n * contact is answered with.\n */\n\n/** The most contacts one request may name — the bar sends a selection in pieces this size. */\nexport const CRM_CONTACT_REMOVE_MAX = 200\n\nexport interface ContactRemoveRequest {\n /** The mounted site; at the organization level the record's own site, or nothing. */\n hostId?: string | null\n /** The organization, which makes the call the route's organization variant. */\n orgId?: string\n contactIds: string[]\n}\n\n/**\n * One contact's answer: `detached` when other sites still hold the person\n * and only this holder's half went, `deleted` when nobody else did and the\n * document went with it — or refused, with a sentence written for the reader.\n */\nexport type ContactRemoveOutcome =\n | { contactId: string; ok: true; removed: 'detached' | 'deleted' }\n | { contactId: string; ok: false; error: string }\n\nexport interface ContactRemoveResponse {\n ok: true\n results: ContactRemoveOutcome[]\n}\n"],"names":["CRM_CONTACT_REMOVE_MAX"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;CAIC,GAED,4FAA4F,GAC5F,OAAO,MAAMA,yBAAyB,IAAG"}
@@ -22,8 +22,7 @@
22
22
  * to an owner, file them under a company, move them along the funnel, let
23
23
  * them go. A contact's tags, owner and company are one holder's records in
24
24
  * that holder's facet, and a facet is the server's to write: the rules cannot
25
- * tell one field of it from another, so they leave a client nothing there
26
- * but letting a holder go. So every act but that one is a request to a route,
25
+ * tell one field of it from another. So every act is a request to a route,
27
26
  * and what this module decides is WHICH rows the request names — the bar
28
27
  * holds only the controls.
29
28
  *
@@ -37,23 +36,8 @@
37
36
  * "398 of 400" is a number nobody can act on; an address is. The route asks
38
37
  * the same questions of the stored document, so a row that changed since the
39
38
  * table read it is still refused by name rather than slipped past.
40
- *
41
- * ## Letting go is still written here
42
- *
43
- * Removing a contact from a site drops that holder's facet, consent entries,
44
- * capture attribution and scope tokens — the one change to a facet a holder
45
- * may make client-direct. Those patches are built here and applied in
46
- * batches with a per-row fallback, so a refused row is named rather than
47
- * failing the other 399 with it.
48
- *
49
- * Pure apart from the Firestore sentinels the detach carries: the runner takes
50
- * its writers as ports, so the bar wires `writeBatch`/`updateDoc`/`deleteDoc`
51
- * in and a spec wires a ledger in.
52
39
  */
53
40
  import { type ContactCompanyLinkState } from '@aglyn/aglyn';
54
- import { chunked } from './crm-bulk-writes';
55
- export declare const CONTACT_BULK_WRITE_CHUNK = 400;
56
- export { chunked };
57
41
  /** The record page's cap on a holder's tags — the same number, so the two agree. */
58
42
  export declare const CONTACT_TAGS_CAP = 20;
59
43
  /** As much of a table row as a bulk act reads. */
@@ -71,31 +55,16 @@ export interface ContactBulkRow {
71
55
  holderHostId?: string;
72
56
  /** THIS holder's tags, already read through the facet by the table. */
73
57
  tags?: string[];
74
- /** The holder tokens — what `planContactDetach` counts. */
58
+ /** The holder tokens the row is shared with. */
75
59
  visibleTo?: string[];
76
60
  /** The link state the company planner reads — see `ContactRecord.companyLink`. */
77
61
  companyLink?: ContactCompanyLinkState;
78
62
  }
79
- /** One client-direct write to one contact document, named by the address it is about. */
80
- export type ContactBulkWrite = {
81
- id: string;
82
- email: string;
83
- } & ({
84
- kind: 'update';
85
- data: Record<string, unknown>;
86
- } | {
87
- kind: 'delete';
88
- });
89
63
  /** A row an action deliberately left alone, and why. */
90
64
  export interface ContactBulkSkip {
91
65
  email: string;
92
66
  reason: string;
93
67
  }
94
- /** What a letting-go wants written, and what it declined to. */
95
- export interface ContactBulkPlan {
96
- writes: ContactBulkWrite[];
97
- skipped: ContactBulkSkip[];
98
- }
99
68
  /** The rows a request to a route names, and the rows it leaves out by name. */
100
69
  export interface ContactBulkSelection {
101
70
  rows: ContactBulkRow[];
@@ -133,41 +102,3 @@ export declare function planRemoveTag(rows: readonly ContactBulkRow[], tag: stri
133
102
  * request is not the place to guess.
134
103
  */
135
104
  export declare function planSetCompany(rows: readonly ContactBulkRow[], companyId: string | null): ContactBulkSelection;
136
- /**
137
- * Let every row go — the record page's DELETE IS A DETACH, per row.
138
- *
139
- * `planContactDetach` decides for each document whether this holder is the
140
- * last one (delete) or one of several (drop this group's facet, consent
141
- * entries, capture attribution and scope tokens). The `arrayRemove` and
142
- * `deleteField` sentinels are the ones the record page writes, so a row
143
- * detached here and a row detached there are the same document afterwards.
144
- *
145
- * ⛔ Not the erasure path: a privacy erasure removes the person everywhere
146
- * regardless of holders.
147
- */
148
- export declare function planDetach(rows: readonly ContactBulkRow[], group: {
149
- groupId: string;
150
- hostIds: readonly string[];
151
- }, nowMs: number): ContactBulkPlan;
152
- /** The writers the runner needs — Firestore's, or a spec's ledger. */
153
- export interface ContactBulkWriters {
154
- /** Apply every write atomically, or throw. */
155
- commitBatch: (writes: readonly ContactBulkWrite[]) => Promise<void>;
156
- /** Apply one write, or throw. */
157
- commitOne: (write: ContactBulkWrite) => Promise<void>;
158
- }
159
- export interface ContactBulkOutcome {
160
- /** Rows written. */
161
- done: number;
162
- /** Rows the store refused, by address, with the reason it gave. */
163
- refused: Array<{
164
- email: string;
165
- error: string;
166
- }>;
167
- }
168
- /**
169
- * The shared runner over contact writes — batched, with a per-row pass for
170
- * the chunk that failed (`crm-bulk-writes.ts`) — reporting by ADDRESS, which
171
- * is the name a contacts report lists a row under.
172
- */
173
- export declare function runContactBulkWrites(writers: ContactBulkWriters, writes: readonly ContactBulkWrite[], chunkSize?: number): Promise<ContactBulkOutcome>;
@@ -1,4 +1,3 @@
1
- import { _ as _extends } from "@swc/helpers/_/_extends";
2
1
  /**
3
2
  * @license
4
3
  * Copyright 2026 Aglyn LLC
@@ -22,8 +21,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
22
21
  * to an owner, file them under a company, move them along the funnel, let
23
22
  * them go. A contact's tags, owner and company are one holder's records in
24
23
  * that holder's facet, and a facet is the server's to write: the rules cannot
25
- * tell one field of it from another, so they leave a client nothing there
26
- * but letting a holder go. So every act but that one is a request to a route,
24
+ * tell one field of it from another. So every act is a request to a route,
27
25
  * and what this module decides is WHICH rows the request names — the bar
28
26
  * holds only the controls.
29
27
  *
@@ -37,27 +35,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
37
35
  * "398 of 400" is a number nobody can act on; an address is. The route asks
38
36
  * the same questions of the stored document, so a row that changed since the
39
37
  * table read it is still refused by name rather than slipped past.
40
- *
41
- * ## Letting go is still written here
42
- *
43
- * Removing a contact from a site drops that holder's facet, consent entries,
44
- * capture attribution and scope tokens — the one change to a facet a holder
45
- * may make client-direct. Those patches are built here and applied in
46
- * batches with a per-row fallback, so a refused row is named rather than
47
- * failing the other 399 with it.
48
- *
49
- * Pure apart from the Firestore sentinels the detach carries: the runner takes
50
- * its writers as ports, so the bar wires `writeBatch`/`updateDoc`/`deleteDoc`
51
- * in and a spec wires a ledger in.
52
- */ import { planContactCompanyLink, planContactDetach } from "@aglyn/aglyn";
53
- import { arrayRemove, deleteField } from "firebase/firestore";
54
- import { CRM_BULK_WRITE_CHUNK, chunked, runCrmBulkWrites } from "./crm-bulk-writes.js";
55
- /*
56
- * The runner and its chunk size are the shared ones (`crm-bulk-writes.ts`,
57
- * AGL-2621), under the names this module has always exported: the batch
58
- * cap and the per-row fallback are about Firestore, not about people.
59
- */ export const CONTACT_BULK_WRITE_CHUNK = CRM_BULK_WRITE_CHUNK;
60
- export { chunked };
38
+ */ import { planContactCompanyLink } from "@aglyn/aglyn";
61
39
  /** The record page's cap on a holder's tags — the same number, so the two agree. */ export const CONTACT_TAGS_CAP = 20;
62
40
  /** The address a report names a row by, falling back to the id. */ export const contactBulkAddressOf = (row)=>String(row.email || row.$id);
63
41
  /**
@@ -134,63 +112,5 @@ export { chunked };
134
112
  skipped
135
113
  };
136
114
  }
137
- /**
138
- * Let every row go — the record page's DELETE IS A DETACH, per row.
139
- *
140
- * `planContactDetach` decides for each document whether this holder is the
141
- * last one (delete) or one of several (drop this group's facet, consent
142
- * entries, capture attribution and scope tokens). The `arrayRemove` and
143
- * `deleteField` sentinels are the ones the record page writes, so a row
144
- * detached here and a row detached there are the same document afterwards.
145
- *
146
- * ⛔ Not the erasure path: a privacy erasure removes the person everywhere
147
- * regardless of holders.
148
- */ export function planDetach(rows, group, nowMs) {
149
- return {
150
- writes: rows.map((row)=>{
151
- var _row_visibleTo;
152
- // Only the holder tokens reach the plan: that is all it reads, and a
153
- // projected table row carries fields the detach must not consult.
154
- const plan = planContactDetach({
155
- visibleTo: (_row_visibleTo = row.visibleTo) != null ? _row_visibleTo : []
156
- }, group);
157
- if (plan.action === 'delete') {
158
- return {
159
- id: row.$id,
160
- email: contactBulkAddressOf(row),
161
- kind: 'delete'
162
- };
163
- }
164
- return {
165
- id: row.$id,
166
- email: contactBulkAddressOf(row),
167
- kind: 'update',
168
- data: _extends({}, Object.fromEntries(plan.remove.map((path)=>[
169
- path,
170
- deleteField()
171
- ])), {
172
- visibleTo: arrayRemove(...plan.removeTokens),
173
- capturedByHostIds: arrayRemove(...plan.removeHostIds),
174
- updatedAt: new Date(nowMs)
175
- })
176
- };
177
- }),
178
- skipped: []
179
- };
180
- }
181
- /**
182
- * The shared runner over contact writes — batched, with a per-row pass for
183
- * the chunk that failed (`crm-bulk-writes.ts`) — reporting by ADDRESS, which
184
- * is the name a contacts report lists a row under.
185
- */ export async function runContactBulkWrites(writers, writes, chunkSize = CONTACT_BULK_WRITE_CHUNK) {
186
- const outcome = await runCrmBulkWrites(writers, writes, (write)=>write.email, chunkSize);
187
- return {
188
- done: outcome.done,
189
- refused: outcome.refused.map((row)=>({
190
- email: row.label,
191
- error: row.error
192
- }))
193
- };
194
- }
195
115
 
196
116
  //# sourceMappingURL=contacts-bulk-writes.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contacts-bulk-writes.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 * What a bulk action on the contacts table does to each row (AGL-2603, and\n * through the server since AGL-2804).\n *\n * The bar over the table offers one act over many rows — tag them, hand them\n * to an owner, file them under a company, move them along the funnel, let\n * them go. A contact's tags, owner and company are one holder's records in\n * that holder's facet, and a facet is the server's to write: the rules cannot\n * tell one field of it from another, so they leave a client nothing there\n * but letting a holder go. So every act but that one is a request to a route,\n * and what this module decides is WHICH rows the request names — the bar\n * holds only the controls.\n *\n * ## A row with nothing to change is left out, and a row that cannot be\n * ## reached is REPORTED\n *\n * A row already carrying the tag, or already at the company, is left out of\n * the request silently: the route would write nothing, and a report saying\n * so would be noise. A row already at the tag cap, or whose company link the\n * table could not project, is left out and named by address — a count of\n * \"398 of 400\" is a number nobody can act on; an address is. The route asks\n * the same questions of the stored document, so a row that changed since the\n * table read it is still refused by name rather than slipped past.\n *\n * ## Letting go is still written here\n *\n * Removing a contact from a site drops that holder's facet, consent entries,\n * capture attribution and scope tokens — the one change to a facet a holder\n * may make client-direct. Those patches are built here and applied in\n * batches with a per-row fallback, so a refused row is named rather than\n * failing the other 399 with it.\n *\n * Pure apart from the Firestore sentinels the detach carries: the runner takes\n * its writers as ports, so the bar wires `writeBatch`/`updateDoc`/`deleteDoc`\n * in and a spec wires a ledger in.\n */\n\nimport {\n type ContactCompanyLinkState,\n planContactCompanyLink,\n planContactDetach,\n} from '@aglyn/aglyn'\nimport { arrayRemove, deleteField } from 'firebase/firestore'\nimport {\n CRM_BULK_WRITE_CHUNK,\n chunked,\n runCrmBulkWrites,\n} from './crm-bulk-writes'\n\n/*\n * The runner and its chunk size are the shared ones (`crm-bulk-writes.ts`,\n * AGL-2621), under the names this module has always exported: the batch\n * cap and the per-row fallback are about Firestore, not about people.\n */\nexport const CONTACT_BULK_WRITE_CHUNK = CRM_BULK_WRITE_CHUNK\nexport { chunked }\n\n/** The record page's cap on a holder's tags — the same number, so the two agree. */\nexport const CONTACT_TAGS_CAP = 20\n\n/** As much of a table row as a bulk act reads. */\nexport interface ContactBulkRow {\n $id: string\n email?: string\n /**\n * The holder this row was flattened through (`ContactRecord.groupId`),\n * which a cross-holder reader — the organization-level list (AGL-2630) —\n * writes each row through, since no one viewing group covers every\n * selected person there.\n */\n groupId?: string\n /** A site of that holder's group — where the row's stage is moved (`ContactRecord.holderHostId`). */\n holderHostId?: string\n /** THIS holder's tags, already read through the facet by the table. */\n tags?: string[]\n /** The holder tokens — what `planContactDetach` counts. */\n visibleTo?: string[]\n /** The link state the company planner reads — see `ContactRecord.companyLink`. */\n companyLink?: ContactCompanyLinkState\n}\n\n/** One client-direct write to one contact document, named by the address it is about. */\nexport type ContactBulkWrite = { id: string; email: string } & (\n | { kind: 'update'; data: Record<string, unknown> }\n | { kind: 'delete' }\n)\n\n/** A row an action deliberately left alone, and why. */\nexport interface ContactBulkSkip {\n email: string\n reason: string\n}\n\n/** What a letting-go wants written, and what it declined to. */\nexport interface ContactBulkPlan {\n writes: ContactBulkWrite[]\n skipped: ContactBulkSkip[]\n}\n\n/** The rows a request to a route names, and the rows it leaves out by name. */\nexport interface ContactBulkSelection {\n rows: ContactBulkRow[]\n skipped: ContactBulkSkip[]\n}\n\n/** The address a report names a row by, falling back to the id. */\nexport const contactBulkAddressOf = (row: ContactBulkRow): string =>\n String(row.email || row.$id)\n\n/**\n * A typed tag as the record page stores one, or `null` when nothing survives.\n *\n * Lowercased and trimmed because the record page lowercases and trims, and\n * `contactMatchesSegment` compares lowercased — a bulk \"VIP\" beside a typed\n * \"vip\" would otherwise be two tags on one list and one filter finding half\n * of them.\n */\nexport function normalizeBulkTag(input: string): string | null {\n const tag = input.trim().toLowerCase().slice(0, 60)\n return tag || null\n}\n\n/**\n * The rows adding one tag reaches.\n *\n * A row that already carries the tag is left out silently. A row already at\n * the cap without it is left out AND reported: the cap is the record page's,\n * and a bulk path that slipped past it would leave a tag the page's next\n * save silently drops.\n */\nexport function planAddTag(\n rows: readonly ContactBulkRow[],\n tag: string,\n): ContactBulkSelection {\n const reached: ContactBulkRow[] = []\n const skipped: ContactBulkSkip[] = []\n for (const row of rows) {\n const held = (row.tags ?? []).map((held) => held.toLowerCase())\n if (held.includes(tag)) continue\n if (held.length >= CONTACT_TAGS_CAP) {\n skipped.push({\n email: contactBulkAddressOf(row),\n reason: `already has ${CONTACT_TAGS_CAP} tags`,\n })\n continue\n }\n reached.push(row)\n }\n return { rows: reached, skipped }\n}\n\n/** The rows taking one tag off reaches: the ones that carry it. */\nexport function planRemoveTag(\n rows: readonly ContactBulkRow[],\n tag: string,\n): ContactBulkSelection {\n return {\n rows: rows.filter((row) =>\n (row.tags ?? []).map((held) => held.toLowerCase()).includes(tag),\n ),\n skipped: [],\n }\n}\n\n/**\n * The rows filing under one company reaches — or unlinking, with `null`\n * (AGL-2613).\n *\n * A row already where it was asked to be is left out silently, as a row\n * already tagged is. A row the table could not project a link state for is\n * left out AND named: whether its link would change cannot be told, and the\n * request is not the place to guess.\n */\nexport function planSetCompany(\n rows: readonly ContactBulkRow[],\n companyId: string | null,\n): ContactBulkSelection {\n const reached: ContactBulkRow[] = []\n const skipped: ContactBulkSkip[] = []\n for (const row of rows) {\n if (!row.companyLink) {\n skipped.push({\n email: contactBulkAddressOf(row),\n reason: 'its company link could not be read',\n })\n continue\n }\n if (planContactCompanyLink(row.companyLink, companyId)) reached.push(row)\n }\n return { rows: reached, skipped }\n}\n\n/**\n * Let every row go — the record page's DELETE IS A DETACH, per row.\n *\n * `planContactDetach` decides for each document whether this holder is the\n * last one (delete) or one of several (drop this group's facet, consent\n * entries, capture attribution and scope tokens). The `arrayRemove` and\n * `deleteField` sentinels are the ones the record page writes, so a row\n * detached here and a row detached there are the same document afterwards.\n *\n * ⛔ Not the erasure path: a privacy erasure removes the person everywhere\n * regardless of holders.\n */\nexport function planDetach(\n rows: readonly ContactBulkRow[],\n group: { groupId: string; hostIds: readonly string[] },\n nowMs: number,\n): ContactBulkPlan {\n return {\n writes: rows.map((row) => {\n // Only the holder tokens reach the plan: that is all it reads, and a\n // projected table row carries fields the detach must not consult.\n const plan = planContactDetach({ visibleTo: row.visibleTo ?? [] }, group)\n if (plan.action === 'delete') {\n return { id: row.$id, email: contactBulkAddressOf(row), kind: 'delete' as const }\n }\n return {\n id: row.$id,\n email: contactBulkAddressOf(row),\n kind: 'update' as const,\n data: {\n ...Object.fromEntries(plan.remove.map((path) => [path, deleteField()])),\n visibleTo: arrayRemove(...plan.removeTokens),\n capturedByHostIds: arrayRemove(...plan.removeHostIds),\n updatedAt: new Date(nowMs),\n },\n }\n }),\n skipped: [],\n }\n}\n\n/** The writers the runner needs — Firestore's, or a spec's ledger. */\nexport interface ContactBulkWriters {\n /** Apply every write atomically, or throw. */\n commitBatch: (writes: readonly ContactBulkWrite[]) => Promise<void>\n /** Apply one write, or throw. */\n commitOne: (write: ContactBulkWrite) => Promise<void>\n}\n\nexport interface ContactBulkOutcome {\n /** Rows written. */\n done: number\n /** Rows the store refused, by address, with the reason it gave. */\n refused: Array<{ email: string; error: string }>\n}\n\n/**\n * The shared runner over contact writes — batched, with a per-row pass for\n * the chunk that failed (`crm-bulk-writes.ts`) — reporting by ADDRESS, which\n * is the name a contacts report lists a row under.\n */\nexport async function runContactBulkWrites(\n writers: ContactBulkWriters,\n writes: readonly ContactBulkWrite[],\n chunkSize: number = CONTACT_BULK_WRITE_CHUNK,\n): Promise<ContactBulkOutcome> {\n const outcome = await runCrmBulkWrites(\n writers,\n writes,\n (write) => write.email,\n chunkSize,\n )\n return {\n done: outcome.done,\n refused: outcome.refused.map((row) => ({ email: row.label, error: row.error })),\n }\n}\n"],"names":["planContactCompanyLink","planContactDetach","arrayRemove","deleteField","CRM_BULK_WRITE_CHUNK","chunked","runCrmBulkWrites","CONTACT_BULK_WRITE_CHUNK","CONTACT_TAGS_CAP","contactBulkAddressOf","row","String","email","$id","normalizeBulkTag","input","tag","trim","toLowerCase","slice","planAddTag","rows","reached","skipped","held","tags","map","includes","length","push","reason","planRemoveTag","filter","planSetCompany","companyId","companyLink","planDetach","group","nowMs","writes","plan","visibleTo","action","id","kind","data","Object","fromEntries","remove","path","removeTokens","capturedByHostIds","removeHostIds","updatedAt","Date","runContactBulkWrites","writers","chunkSize","outcome","write","done","refused","label","error"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contacts-bulk-writes.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 * What a bulk action on the contacts table does to each row (AGL-2603, and\n * through the server since AGL-2804).\n *\n * The bar over the table offers one act over many rows — tag them, hand them\n * to an owner, file them under a company, move them along the funnel, let\n * them go. A contact's tags, owner and company are one holder's records in\n * that holder's facet, and a facet is the server's to write: the rules cannot\n * tell one field of it from another. So every act is a request to a route,\n * and what this module decides is WHICH rows the request names — the bar\n * holds only the controls.\n *\n * ## A row with nothing to change is left out, and a row that cannot be\n * ## reached is REPORTED\n *\n * A row already carrying the tag, or already at the company, is left out of\n * the request silently: the route would write nothing, and a report saying\n * so would be noise. A row already at the tag cap, or whose company link the\n * table could not project, is left out and named by address — a count of\n * \"398 of 400\" is a number nobody can act on; an address is. The route asks\n * the same questions of the stored document, so a row that changed since the\n * table read it is still refused by name rather than slipped past.\n */\n\nimport {\n type ContactCompanyLinkState,\n planContactCompanyLink,\n} from '@aglyn/aglyn'\n\n/** The record page's cap on a holder's tags — the same number, so the two agree. */\nexport const CONTACT_TAGS_CAP = 20\n\n/** As much of a table row as a bulk act reads. */\nexport interface ContactBulkRow {\n $id: string\n email?: string\n /**\n * The holder this row was flattened through (`ContactRecord.groupId`),\n * which a cross-holder reader — the organization-level list (AGL-2630) —\n * writes each row through, since no one viewing group covers every\n * selected person there.\n */\n groupId?: string\n /** A site of that holder's group — where the row's stage is moved (`ContactRecord.holderHostId`). */\n holderHostId?: string\n /** THIS holder's tags, already read through the facet by the table. */\n tags?: string[]\n /** The holder tokens the row is shared with. */\n visibleTo?: string[]\n /** The link state the company planner reads — see `ContactRecord.companyLink`. */\n companyLink?: ContactCompanyLinkState\n}\n\n/** A row an action deliberately left alone, and why. */\nexport interface ContactBulkSkip {\n email: string\n reason: string\n}\n\n/** The rows a request to a route names, and the rows it leaves out by name. */\nexport interface ContactBulkSelection {\n rows: ContactBulkRow[]\n skipped: ContactBulkSkip[]\n}\n\n/** The address a report names a row by, falling back to the id. */\nexport const contactBulkAddressOf = (row: ContactBulkRow): string =>\n String(row.email || row.$id)\n\n/**\n * A typed tag as the record page stores one, or `null` when nothing survives.\n *\n * Lowercased and trimmed because the record page lowercases and trims, and\n * `contactMatchesSegment` compares lowercased — a bulk \"VIP\" beside a typed\n * \"vip\" would otherwise be two tags on one list and one filter finding half\n * of them.\n */\nexport function normalizeBulkTag(input: string): string | null {\n const tag = input.trim().toLowerCase().slice(0, 60)\n return tag || null\n}\n\n/**\n * The rows adding one tag reaches.\n *\n * A row that already carries the tag is left out silently. A row already at\n * the cap without it is left out AND reported: the cap is the record page's,\n * and a bulk path that slipped past it would leave a tag the page's next\n * save silently drops.\n */\nexport function planAddTag(\n rows: readonly ContactBulkRow[],\n tag: string,\n): ContactBulkSelection {\n const reached: ContactBulkRow[] = []\n const skipped: ContactBulkSkip[] = []\n for (const row of rows) {\n const held = (row.tags ?? []).map((held) => held.toLowerCase())\n if (held.includes(tag)) continue\n if (held.length >= CONTACT_TAGS_CAP) {\n skipped.push({\n email: contactBulkAddressOf(row),\n reason: `already has ${CONTACT_TAGS_CAP} tags`,\n })\n continue\n }\n reached.push(row)\n }\n return { rows: reached, skipped }\n}\n\n/** The rows taking one tag off reaches: the ones that carry it. */\nexport function planRemoveTag(\n rows: readonly ContactBulkRow[],\n tag: string,\n): ContactBulkSelection {\n return {\n rows: rows.filter((row) =>\n (row.tags ?? []).map((held) => held.toLowerCase()).includes(tag),\n ),\n skipped: [],\n }\n}\n\n/**\n * The rows filing under one company reaches — or unlinking, with `null`\n * (AGL-2613).\n *\n * A row already where it was asked to be is left out silently, as a row\n * already tagged is. A row the table could not project a link state for is\n * left out AND named: whether its link would change cannot be told, and the\n * request is not the place to guess.\n */\nexport function planSetCompany(\n rows: readonly ContactBulkRow[],\n companyId: string | null,\n): ContactBulkSelection {\n const reached: ContactBulkRow[] = []\n const skipped: ContactBulkSkip[] = []\n for (const row of rows) {\n if (!row.companyLink) {\n skipped.push({\n email: contactBulkAddressOf(row),\n reason: 'its company link could not be read',\n })\n continue\n }\n if (planContactCompanyLink(row.companyLink, companyId)) reached.push(row)\n }\n return { rows: reached, skipped }\n}\n"],"names":["planContactCompanyLink","CONTACT_TAGS_CAP","contactBulkAddressOf","row","String","email","$id","normalizeBulkTag","input","tag","trim","toLowerCase","slice","planAddTag","rows","reached","skipped","held","tags","map","includes","length","push","reason","planRemoveTag","filter","planSetCompany","companyId","companyLink"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,SAEEA,sBAAsB,QACjB,eAAc;AAErB,kFAAkF,GAClF,OAAO,MAAMC,mBAAmB,GAAE;AAmClC,iEAAiE,GACjE,OAAO,MAAMC,uBAAuB,CAACC,MACnCC,OAAOD,IAAIE,KAAK,IAAIF,IAAIG,GAAG,EAAC;AAE9B;;;;;;;CAOC,GACD,OAAO,SAASC,iBAAiBC,KAAa;IAC5C,MAAMC,MAAMD,MAAME,IAAI,GAAGC,WAAW,GAAGC,KAAK,CAAC,GAAG;IAChD,OAAOH,OAAO;AAChB;AAEA;;;;;;;CAOC,GACD,OAAO,SAASI,WACdC,IAA+B,EAC/BL,GAAW;IAEX,MAAMM,UAA4B,EAAE;IACpC,MAAMC,UAA6B,EAAE;IACrC,KAAK,MAAMb,OAAOW,KAAM;YACRX;QAAd,MAAMc,OAAO,EAACd,YAAAA,IAAIe,IAAI,YAARf,YAAY,EAAE,EAAEgB,GAAG,CAAC,CAACF,OAASA,KAAKN,WAAW;QAC5D,IAAIM,KAAKG,QAAQ,CAACX,MAAM;QACxB,IAAIQ,KAAKI,MAAM,IAAIpB,kBAAkB;YACnCe,QAAQM,IAAI,CAAC;gBACXjB,OAAOH,qBAAqBC;gBAC5BoB,QAAQ,CAAC,YAAY,EAAEtB,iBAAiB,KAAK,CAAC;YAChD;YACA;QACF;QACAc,QAAQO,IAAI,CAACnB;IACf;IACA,OAAO;QAAEW,MAAMC;QAASC;IAAQ;AAClC;AAEA,iEAAiE,GACjE,OAAO,SAASQ,cACdV,IAA+B,EAC/BL,GAAW;IAEX,OAAO;QACLK,MAAMA,KAAKW,MAAM,CAAC,CAACtB;gBAChBA;mBAAD,EAACA,YAAAA,IAAIe,IAAI,YAARf,YAAY,EAAE,EAAEgB,GAAG,CAAC,CAACF,OAASA,KAAKN,WAAW,IAAIS,QAAQ,CAACX;;QAE9DO,SAAS,EAAE;IACb;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASU,eACdZ,IAA+B,EAC/Ba,SAAwB;IAExB,MAAMZ,UAA4B,EAAE;IACpC,MAAMC,UAA6B,EAAE;IACrC,KAAK,MAAMb,OAAOW,KAAM;QACtB,IAAI,CAACX,IAAIyB,WAAW,EAAE;YACpBZ,QAAQM,IAAI,CAAC;gBACXjB,OAAOH,qBAAqBC;gBAC5BoB,QAAQ;YACV;YACA;QACF;QACA,IAAIvB,uBAAuBG,IAAIyB,WAAW,EAAED,YAAYZ,QAAQO,IAAI,CAACnB;IACvE;IACA,OAAO;QAAEW,MAAMC;QAASC;IAAQ;AAClC"}
@@ -37,8 +37,8 @@
37
37
  * written, because they are what a merchant reads the file for; they map to
38
38
  * "Do not import" on the way back.
39
39
  */
40
- export { CONTACT_CSV_COLUMNS, contactCsvHeader, contactsCsv, type ContactCsvOptions, type ContactCsvRow, } from '@aglyn/aglyn';
41
- import { type ContactCsvOptions } from '@aglyn/aglyn';
40
+ export { CONTACT_CSV_COLUMNS, contactCsvHeader, contactsCsv, type ContactCsvOptions, type ContactCsvRow, } from './crm-csv';
41
+ import { type ContactCsvOptions } from './crm-csv';
42
42
  /**
43
43
  * The file the Import drawer hands out to start from: the export's header
44
44
  * and nothing under it, so a sheet filled in against it maps itself.
@@ -35,8 +35,8 @@
35
35
  * the last interaction and the last campaign engagement — are still
36
36
  * written, because they are what a merchant reads the file for; they map to
37
37
  * "Do not import" on the way back.
38
- */ /* The writer moved to `@aglyn/aglyn` under AGL-2662 — see `deals-csv.ts`. */ export { CONTACT_CSV_COLUMNS, contactCsvHeader, contactsCsv } from "@aglyn/aglyn";
39
- import { contactsCsv } from "@aglyn/aglyn";
38
+ */ /* The writer is `crm-csv.ts`, shared with the server's export — see `deals-csv.ts`. */ export { CONTACT_CSV_COLUMNS, contactCsvHeader, contactsCsv } from "./crm-csv.js";
39
+ import { contactsCsv } from "./crm-csv.js";
40
40
  /**
41
41
  * The file the Import drawer hands out to start from: the export's header
42
42
  * and nothing under it, so a sheet filled in against it maps itself.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contacts-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 contacts CSV — every CRM column, the same file from the table's\n * Export button and the bulk bar's (AGL-2603, widened by AGL-2621).\n *\n * The selection's export and the table's export are the same file over a\n * different set of rows: the same columns in the same order, the same `|`\n * between multi-valued cells, the same quoting. A second column list would\n * be a second file format for one feature, and the person opening both in\n * a spreadsheet would be the one to find the difference.\n *\n * ## The header row is the import's own vocabulary\n *\n * Each column is headed by a name `guessContactImportMapping` recognizes —\n * \"Job title\", \"Address line 1\", \"Lifecycle stage\", a custom field's label\n * — so an export re-imports without a hand mapping, and the template the\n * Import drawer offers IS this header over no rows. The owner column\n * carries the member's ADDRESS rather than a uid for the same reason: the\n * import resolves an owner by email, and a uid is nothing a spreadsheet\n * can read anyway. The three columns the import has no field for — sources,\n * the last interaction and the last campaign engagement — are still\n * written, because they are what a merchant reads the file for; they map to\n * \"Do not import\" on the way back.\n */\n\n/* The writer moved to `@aglyn/aglyn` under AGL-2662 — see `deals-csv.ts`. */\nexport {\n CONTACT_CSV_COLUMNS,\n contactCsvHeader,\n contactsCsv,\n type ContactCsvOptions,\n type ContactCsvRow,\n} from '@aglyn/aglyn'\n\nimport { contactsCsv, type ContactCsvOptions } 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, so a sheet filled in against it maps itself.\n */\nexport function contactImportTemplateCsv(\n customFields: ContactCsvOptions['customFields'] = [],\n): string {\n return contactsCsv([], { customFields })\n}\n\n/**\n * Hand the browser a file to save.\n *\n * The one writer that stayed: it touches `URL.createObjectURL` and\n * `document`, so it belongs to the browser and not to the library a server\n * route imports.\n */\nexport function downloadTextFile(name: string, mime: string, text: string): void {\n const url = URL.createObjectURL(new Blob([text], { type: mime }))\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = name\n anchor.click()\n URL.revokeObjectURL(url)\n}\n"],"names":["CONTACT_CSV_COLUMNS","contactCsvHeader","contactsCsv","contactImportTemplateCsv","customFields","downloadTextFile","name","mime","text","url","URL","createObjectURL","Blob","type","anchor","document","createElement","href","download","click","revokeObjectURL"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,2EAA2E,GAC3E,SACEA,mBAAmB,EACnBC,gBAAgB,EAChBC,WAAW,QAGN,eAAc;AAErB,SAASA,WAAW,QAAgC,eAAc;AAElE;;;CAGC,GACD,OAAO,SAASC,yBACdC,eAAkD,EAAE;IAEpD,OAAOF,YAAY,EAAE,EAAE;QAAEE;IAAa;AACxC;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,iBAAiBC,IAAY,EAAEC,IAAY,EAAEC,IAAY;IACvE,MAAMC,MAAMC,IAAIC,eAAe,CAAC,IAAIC,KAAK;QAACJ;KAAK,EAAE;QAAEK,MAAMN;IAAK;IAC9D,MAAMO,SAASC,SAASC,aAAa,CAAC;IACtCF,OAAOG,IAAI,GAAGR;IACdK,OAAOI,QAAQ,GAAGZ;IAClBQ,OAAOK,KAAK;IACZT,IAAIU,eAAe,CAACX;AACtB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/contacts-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 contacts CSV — every CRM column, the same file from the table's\n * Export button and the bulk bar's (AGL-2603, widened by AGL-2621).\n *\n * The selection's export and the table's export are the same file over a\n * different set of rows: the same columns in the same order, the same `|`\n * between multi-valued cells, the same quoting. A second column list would\n * be a second file format for one feature, and the person opening both in\n * a spreadsheet would be the one to find the difference.\n *\n * ## The header row is the import's own vocabulary\n *\n * Each column is headed by a name `guessContactImportMapping` recognizes —\n * \"Job title\", \"Address line 1\", \"Lifecycle stage\", a custom field's label\n * — so an export re-imports without a hand mapping, and the template the\n * Import drawer offers IS this header over no rows. The owner column\n * carries the member's ADDRESS rather than a uid for the same reason: the\n * import resolves an owner by email, and a uid is nothing a spreadsheet\n * can read anyway. The three columns the import has no field for — sources,\n * the last interaction and the last campaign engagement — are still\n * written, because they are what a merchant reads the file for; they map to\n * \"Do not 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 CONTACT_CSV_COLUMNS,\n contactCsvHeader,\n contactsCsv,\n type ContactCsvOptions,\n type ContactCsvRow,\n} from './crm-csv'\n\nimport { contactsCsv, type ContactCsvOptions } 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, so a sheet filled in against it maps itself.\n */\nexport function contactImportTemplateCsv(\n customFields: ContactCsvOptions['customFields'] = [],\n): string {\n return contactsCsv([], { customFields })\n}\n\n/**\n * Hand the browser a file to save.\n *\n * The one writer that stayed: it touches `URL.createObjectURL` and\n * `document`, so it belongs to the browser and not to the library a server\n * route imports.\n */\nexport function downloadTextFile(name: string, mime: string, text: string): void {\n const url = URL.createObjectURL(new Blob([text], { type: mime }))\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = name\n anchor.click()\n URL.revokeObjectURL(url)\n}\n"],"names":["CONTACT_CSV_COLUMNS","contactCsvHeader","contactsCsv","contactImportTemplateCsv","customFields","downloadTextFile","name","mime","text","url","URL","createObjectURL","Blob","type","anchor","document","createElement","href","download","click","revokeObjectURL"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,qFAAqF,GACrF,SACEA,mBAAmB,EACnBC,gBAAgB,EAChBC,WAAW,QAGN,eAAW;AAElB,SAASA,WAAW,QAAgC,eAAW;AAE/D;;;CAGC,GACD,OAAO,SAASC,yBACdC,eAAkD,EAAE;IAEpD,OAAOF,YAAY,EAAE,EAAE;QAAEE;IAAa;AACxC;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,iBAAiBC,IAAY,EAAEC,IAAY,EAAEC,IAAY;IACvE,MAAMC,MAAMC,IAAIC,eAAe,CAAC,IAAIC,KAAK;QAACJ;KAAK,EAAE;QAAEK,MAAMN;IAAK;IAC9D,MAAMO,SAASC,SAASC,aAAa,CAAC;IACtCF,OAAOG,IAAI,GAAGR;IACdK,OAAOI,QAAQ,GAAGZ;IAClBQ,OAAOK,KAAK;IACZT,IAAIU,eAAe,CAACX;AACtB"}