@aglyn/plugins-crm 1.0.0-beta.221 → 1.0.0-beta.222

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 (514) hide show
  1. package/README.md +2 -1
  2. package/package.json +13 -13
  3. package/src/lib/components/activity-list.js +12 -5
  4. package/src/lib/components/activity-list.js.map +1 -1
  5. package/src/lib/components/companies-bulk-bar.d.ts +1 -4
  6. package/src/lib/components/companies-bulk-bar.js +7 -20
  7. package/src/lib/components/companies-bulk-bar.js.map +1 -1
  8. package/src/lib/components/companies-section.d.ts +3 -3
  9. package/src/lib/components/companies-section.js +149 -46
  10. package/src/lib/components/companies-section.js.map +1 -1
  11. package/src/lib/components/company-edit-drawer.js +281 -20
  12. package/src/lib/components/company-edit-drawer.js.map +1 -1
  13. package/src/lib/components/company-properties-card.js +99 -8
  14. package/src/lib/components/company-properties-card.js.map +1 -1
  15. package/src/lib/components/contact-detail-page.js +3 -1
  16. package/src/lib/components/contact-detail-page.js.map +1 -1
  17. package/src/lib/components/contact-merge-dialog.js +33 -10
  18. package/src/lib/components/contact-merge-dialog.js.map +1 -1
  19. package/src/lib/components/contact-properties-card.d.ts +0 -69
  20. package/src/lib/components/contact-properties-card.js +350 -60
  21. package/src/lib/components/contact-properties-card.js.map +1 -1
  22. package/src/lib/components/contact-reports-to-field.d.ts +46 -0
  23. package/src/lib/components/contact-reports-to-field.js +67 -0
  24. package/src/lib/components/contact-reports-to-field.js.map +1 -0
  25. package/src/lib/components/contacts-bulk-bar.d.ts +1 -8
  26. package/src/lib/components/contacts-bulk-bar.js +7 -19
  27. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  28. package/src/lib/components/contacts-section.js +46 -33
  29. package/src/lib/components/contacts-section.js.map +1 -1
  30. package/src/lib/components/crm-call-actions.d.ts +10 -0
  31. package/src/lib/components/crm-call-actions.js +26 -9
  32. package/src/lib/components/crm-call-actions.js.map +1 -1
  33. package/src/lib/components/crm-record-picker.d.ts +15 -0
  34. package/src/lib/components/crm-record-picker.js +16 -10
  35. package/src/lib/components/crm-record-picker.js.map +1 -1
  36. package/src/lib/components/crm-transfer-buttons.d.ts +54 -0
  37. package/src/lib/components/crm-transfer-buttons.js +167 -0
  38. package/src/lib/components/crm-transfer-buttons.js.map +1 -0
  39. package/src/lib/components/deal-contact-roles-card.d.ts +46 -0
  40. package/src/lib/components/deal-contact-roles-card.js +368 -0
  41. package/src/lib/components/deal-contact-roles-card.js.map +1 -0
  42. package/src/lib/components/deal-detail-page.d.ts +3 -2
  43. package/src/lib/components/deal-detail-page.js +25 -6
  44. package/src/lib/components/deal-detail-page.js.map +1 -1
  45. package/src/lib/components/deal-edit-drawer.d.ts +10 -0
  46. package/src/lib/components/deal-edit-drawer.js +219 -10
  47. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  48. package/src/lib/components/deal-properties-card.d.ts +7 -0
  49. package/src/lib/components/deal-properties-card.js +34 -3
  50. package/src/lib/components/deal-properties-card.js.map +1 -1
  51. package/src/lib/components/deal-stage-card.d.ts +4 -0
  52. package/src/lib/components/deal-stage-card.js +10 -2
  53. package/src/lib/components/deal-stage-card.js.map +1 -1
  54. package/src/lib/components/deals-bulk-bar.d.ts +0 -3
  55. package/src/lib/components/deals-bulk-bar.js +7 -20
  56. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  57. package/src/lib/components/deals-section.d.ts +3 -3
  58. package/src/lib/components/deals-section.js +123 -63
  59. package/src/lib/components/deals-section.js.map +1 -1
  60. package/src/lib/components/fields-section.js +31 -17
  61. package/src/lib/components/fields-section.js.map +1 -1
  62. package/src/lib/components/lead-convert-dialog.d.ts +4 -0
  63. package/src/lib/components/lead-convert-dialog.js +23 -1
  64. package/src/lib/components/lead-convert-dialog.js.map +1 -1
  65. package/src/lib/components/lead-detail-page.js +8 -2
  66. package/src/lib/components/lead-detail-page.js.map +1 -1
  67. package/src/lib/components/lead-profile-fields.d.ts +71 -0
  68. package/src/lib/components/lead-profile-fields.js +386 -0
  69. package/src/lib/components/lead-profile-fields.js.map +1 -0
  70. package/src/lib/components/lead-properties-card.d.ts +2 -1
  71. package/src/lib/components/lead-properties-card.js +85 -176
  72. package/src/lib/components/lead-properties-card.js.map +1 -1
  73. package/src/lib/components/lead-source-select.d.ts +4 -23
  74. package/src/lib/components/lead-source-select.js +9 -73
  75. package/src/lib/components/lead-source-select.js.map +1 -1
  76. package/src/lib/components/lead-status-chip.d.ts +9 -3
  77. package/src/lib/components/lead-status-chip.js +8 -4
  78. package/src/lib/components/lead-status-chip.js.map +1 -1
  79. package/src/lib/components/lead-status-options.d.ts +20 -0
  80. package/src/lib/components/lead-status-options.js +51 -0
  81. package/src/lib/components/lead-status-options.js.map +1 -0
  82. package/src/lib/components/lead-unqualify-dialog.d.ts +5 -0
  83. package/src/lib/components/lead-unqualify-dialog.js +50 -18
  84. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  85. package/src/lib/components/leads-bulk-bar.d.ts +0 -3
  86. package/src/lib/components/leads-bulk-bar.js +29 -29
  87. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  88. package/src/lib/components/leads-section.js +120 -68
  89. package/src/lib/components/leads-section.js.map +1 -1
  90. package/src/lib/components/linked-deals-card.d.ts +4 -0
  91. package/src/lib/components/linked-deals-card.js +47 -3
  92. package/src/lib/components/linked-deals-card.js.map +1 -1
  93. package/src/lib/components/log-activity-dialog.js +43 -3
  94. package/src/lib/components/log-activity-dialog.js.map +1 -1
  95. package/src/lib/components/new-contact-drawer.d.ts +8 -0
  96. package/src/lib/components/new-contact-drawer.js +108 -6
  97. package/src/lib/components/new-contact-drawer.js.map +1 -1
  98. package/src/lib/components/new-lead-drawer.d.ts +9 -2
  99. package/src/lib/components/new-lead-drawer.js +88 -121
  100. package/src/lib/components/new-lead-drawer.js.map +1 -1
  101. package/src/lib/components/picklist-select.d.ts +36 -0
  102. package/src/lib/components/picklist-select.js +114 -0
  103. package/src/lib/components/picklist-select.js.map +1 -0
  104. package/src/lib/components/picklist-values-card.d.ts +34 -0
  105. package/src/lib/components/{lead-source-values-card.js → picklist-values-card.js} +241 -58
  106. package/src/lib/components/picklist-values-card.js.map +1 -0
  107. package/src/lib/components/pipeline-stages-dialog.d.ts +7 -0
  108. package/src/lib/components/pipeline-stages-dialog.js +26 -4
  109. package/src/lib/components/pipeline-stages-dialog.js.map +1 -1
  110. package/src/lib/components/pipelines-dialog.js +15 -6
  111. package/src/lib/components/pipelines-dialog.js.map +1 -1
  112. package/src/lib/components/recent-activity-feed.js +20 -5
  113. package/src/lib/components/recent-activity-feed.js.map +1 -1
  114. package/src/lib/components/recipes-card.js +4 -2
  115. package/src/lib/components/recipes-card.js.map +1 -1
  116. package/src/lib/components/record-tasks-card.js +15 -8
  117. package/src/lib/components/record-tasks-card.js.map +1 -1
  118. package/src/lib/components/reports/forecast-card.d.ts +6 -0
  119. package/src/lib/components/reports/forecast-card.js +101 -3
  120. package/src/lib/components/reports/forecast-card.js.map +1 -1
  121. package/src/lib/components/reports/lead-funnel-card.d.ts +12 -0
  122. package/src/lib/components/reports/lead-funnel-card.js +9 -5
  123. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  124. package/src/lib/components/reports/lead-source-card.d.ts +23 -0
  125. package/src/lib/components/reports/lead-source-card.js +318 -0
  126. package/src/lib/components/reports/lead-source-card.js.map +1 -0
  127. package/src/lib/components/reports/report-export.js +1 -1
  128. package/src/lib/components/reports/report-export.js.map +1 -1
  129. package/src/lib/components/reports-section.js +8 -0
  130. package/src/lib/components/reports-section.js.map +1 -1
  131. package/src/lib/components/sharing-rule-drawer.d.ts +5 -0
  132. package/src/lib/components/sharing-rule-drawer.js +108 -19
  133. package/src/lib/components/sharing-rule-drawer.js.map +1 -1
  134. package/src/lib/components/sharing-rules-card.js +11 -3
  135. package/src/lib/components/sharing-rules-card.js.map +1 -1
  136. package/src/lib/components/task-cells.d.ts +9 -1
  137. package/src/lib/components/task-cells.js +9 -4
  138. package/src/lib/components/task-cells.js.map +1 -1
  139. package/src/lib/components/task-edit-drawer.js +111 -43
  140. package/src/lib/components/task-edit-drawer.js.map +1 -1
  141. package/src/lib/components/tasks-bulk-bar.d.ts +0 -3
  142. package/src/lib/components/tasks-bulk-bar.js +7 -20
  143. package/src/lib/components/tasks-bulk-bar.js.map +1 -1
  144. package/src/lib/components/tasks-calendar.d.ts +3 -0
  145. package/src/lib/components/tasks-calendar.js +8 -1
  146. package/src/lib/components/tasks-calendar.js.map +1 -1
  147. package/src/lib/components/tasks-section.d.ts +3 -3
  148. package/src/lib/components/tasks-section.js +58 -43
  149. package/src/lib/components/tasks-section.js.map +1 -1
  150. package/src/lib/constants/api-routes.d.ts +2 -9
  151. package/src/lib/constants/api-routes.js +1 -7
  152. package/src/lib/constants/api-routes.js.map +1 -1
  153. package/src/lib/constants/bundle-common.d.ts +6 -0
  154. package/src/lib/constants/bundle-common.js +11 -0
  155. package/src/lib/constants/bundle-common.js.map +1 -1
  156. package/src/lib/constants/company-filters.d.ts +14 -0
  157. package/src/lib/constants/company-filters.js +51 -1
  158. package/src/lib/constants/company-filters.js.map +1 -1
  159. package/src/lib/constants/contact-filters.d.ts +2 -0
  160. package/src/lib/constants/contact-filters.js +18 -1
  161. package/src/lib/constants/contact-filters.js.map +1 -1
  162. package/src/lib/constants/deal-filters.d.ts +22 -1
  163. package/src/lib/constants/deal-filters.js +67 -4
  164. package/src/lib/constants/deal-filters.js.map +1 -1
  165. package/src/lib/declarations.console-server.d.ts +3 -0
  166. package/src/lib/declarations.console-server.js +35 -0
  167. package/src/lib/declarations.console-server.js.map +1 -1
  168. package/src/lib/declarations.d.ts +26 -0
  169. package/src/lib/declarations.js +35 -0
  170. package/src/lib/declarations.js.map +1 -0
  171. package/src/lib/declarations.server.d.ts +11 -0
  172. package/src/lib/declarations.server.js +92 -1
  173. package/src/lib/declarations.server.js.map +1 -1
  174. package/src/lib/hooks/use-company-picklists.d.ts +15 -0
  175. package/src/lib/hooks/use-company-picklists.js +55 -0
  176. package/src/lib/hooks/use-company-picklists.js.map +1 -0
  177. package/src/lib/hooks/use-crm-picklist.d.ts +25 -0
  178. package/src/lib/hooks/use-crm-picklist.js +53 -0
  179. package/src/lib/hooks/use-crm-picklist.js.map +1 -0
  180. package/src/lib/hooks/use-crm-record-names.d.ts +16 -0
  181. package/src/lib/hooks/use-crm-record-names.js +45 -19
  182. package/src/lib/hooks/use-crm-record-names.js.map +1 -1
  183. package/src/lib/hooks/use-crm-task-picklists.d.ts +11 -0
  184. package/src/lib/hooks/use-crm-task-picklists.js +45 -0
  185. package/src/lib/hooks/use-crm-task-picklists.js.map +1 -0
  186. package/src/lib/hooks/use-deals.d.ts +14 -0
  187. package/src/lib/hooks/use-deals.js +29 -0
  188. package/src/lib/hooks/use-deals.js.map +1 -1
  189. package/src/lib/hooks/use-lead-picklists.d.ts +14 -0
  190. package/src/lib/hooks/use-lead-picklists.js +46 -0
  191. package/src/lib/hooks/use-lead-picklists.js.map +1 -0
  192. package/src/lib/hooks/use-lead-source-picklist.d.ts +5 -19
  193. package/src/lib/hooks/use-lead-source-picklist.js +12 -22
  194. package/src/lib/hooks/use-lead-source-picklist.js.map +1 -1
  195. package/src/lib/hooks/use-lead-status-picklist.d.ts +9 -0
  196. package/src/lib/hooks/use-lead-status-picklist.js +40 -0
  197. package/src/lib/hooks/use-lead-status-picklist.js.map +1 -0
  198. package/src/lib/model/companies.d.ts +32 -3
  199. package/src/lib/model/companies.js +133 -16
  200. package/src/lib/model/companies.js.map +1 -1
  201. package/src/lib/model/contact-merge.d.ts +27 -3
  202. package/src/lib/model/contact-merge.js +81 -7
  203. package/src/lib/model/contact-merge.js.map +1 -1
  204. package/src/lib/model/contact-record.d.ts +17 -0
  205. package/src/lib/model/contact-record.js +15 -1
  206. package/src/lib/model/contact-record.js.map +1 -1
  207. package/src/lib/model/contact-update.d.ts +31 -2
  208. package/src/lib/model/contact-update.js +14 -0
  209. package/src/lib/model/contact-update.js.map +1 -1
  210. package/src/lib/model/crm-list-query.d.ts +5 -0
  211. package/src/lib/model/crm-list-query.js +8 -0
  212. package/src/lib/model/crm-list-query.js.map +1 -1
  213. package/src/lib/model/crm-recipes.d.ts +40 -0
  214. package/src/lib/model/crm-recipes.js +244 -0
  215. package/src/lib/model/crm-recipes.js.map +1 -0
  216. package/src/lib/model/crm-record-counts.d.ts +36 -0
  217. package/src/lib/model/crm-record-counts.js +54 -0
  218. package/src/lib/model/crm-record-counts.js.map +1 -0
  219. package/src/lib/model/crm-record-lists.d.ts +23 -0
  220. package/src/lib/model/crm-record-lists.js +161 -0
  221. package/src/lib/model/crm-record-lists.js.map +1 -0
  222. package/src/lib/model/crm-reports.d.ts +73 -3
  223. package/src/lib/model/crm-reports.js +120 -1
  224. package/src/lib/model/crm-reports.js.map +1 -1
  225. package/src/lib/model/crm-sharing.d.ts +21 -3
  226. package/src/lib/model/crm-sharing.js +27 -6
  227. package/src/lib/model/crm-sharing.js.map +1 -1
  228. package/src/lib/model/deal-board-model.d.ts +8 -2
  229. package/src/lib/model/deal-board-model.js +15 -2
  230. package/src/lib/model/deal-board-model.js.map +1 -1
  231. package/src/lib/model/deal-contact-roles.d.ts +45 -0
  232. package/src/lib/model/deal-contact-roles.js +73 -0
  233. package/src/lib/model/deal-contact-roles.js.map +1 -0
  234. package/src/lib/model/deal-form-model.d.ts +56 -0
  235. package/src/lib/model/deal-form-model.js +79 -7
  236. package/src/lib/model/deal-form-model.js.map +1 -1
  237. package/src/lib/model/download-text-file.d.ts +23 -0
  238. package/src/lib/model/download-text-file.js +34 -0
  239. package/src/lib/model/download-text-file.js.map +1 -0
  240. package/src/lib/model/lead-filters.d.ts +36 -2
  241. package/src/lib/model/lead-filters.js +123 -2
  242. package/src/lib/model/lead-filters.js.map +1 -1
  243. package/src/lib/model/lead-source-direction.d.ts +38 -0
  244. package/src/lib/model/lead-source-direction.js +52 -0
  245. package/src/lib/model/lead-source-direction.js.map +1 -0
  246. package/src/lib/model/picklist-values.d.ts +119 -0
  247. package/src/lib/model/picklist-values.js +256 -0
  248. package/src/lib/model/picklist-values.js.map +1 -0
  249. package/src/lib/model/record-facts-disclosed.d.ts +81 -0
  250. package/src/lib/model/record-facts-disclosed.js +281 -0
  251. package/src/lib/model/record-facts-disclosed.js.map +1 -0
  252. package/src/lib/model/record-facts.d.ts +215 -34
  253. package/src/lib/model/record-facts.js +307 -128
  254. package/src/lib/model/record-facts.js.map +1 -1
  255. package/src/lib/model/task-picklists.d.ts +52 -0
  256. package/src/lib/model/task-picklists.js +83 -0
  257. package/src/lib/model/task-picklists.js.map +1 -0
  258. package/src/lib/model/task-routes.d.ts +13 -0
  259. package/src/lib/model/task-routes.js +23 -4
  260. package/src/lib/model/task-routes.js.map +1 -1
  261. package/src/lib/plan-entitlements.js +6 -4
  262. package/src/lib/plan-entitlements.js.map +1 -1
  263. package/src/lib/plugin.js +17 -0
  264. package/src/lib/plugin.js.map +1 -1
  265. package/src/lib/server/api-v1/contacts.js +220 -14
  266. package/src/lib/server/api-v1/contacts.js.map +1 -1
  267. package/src/lib/server/api-v1/crm-activities.js +14 -1
  268. package/src/lib/server/api-v1/crm-activities.js.map +1 -1
  269. package/src/lib/server/api-v1/crm-companies.js +123 -14
  270. package/src/lib/server/api-v1/crm-companies.js.map +1 -1
  271. package/src/lib/server/api-v1/crm-deals.js +291 -26
  272. package/src/lib/server/api-v1/crm-deals.js.map +1 -1
  273. package/src/lib/server/api-v1/crm-leads.js +144 -38
  274. package/src/lib/server/api-v1/crm-leads.js.map +1 -1
  275. package/src/lib/server/api-v1/crm-pipelines.d.ts +6 -0
  276. package/src/lib/server/api-v1/crm-pipelines.js +8 -2
  277. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -1
  278. package/src/lib/server/api-v1/crm-tasks.js +75 -33
  279. package/src/lib/server/api-v1/crm-tasks.js.map +1 -1
  280. package/src/lib/server/api-v1/openapi.d.ts +0 -11
  281. package/src/lib/server/api-v1/openapi.js +171 -16
  282. package/src/lib/server/api-v1/openapi.js.map +1 -1
  283. package/src/lib/server/assign-contact-owner.d.ts +162 -0
  284. package/src/lib/server/assign-contact-owner.js +340 -0
  285. package/src/lib/server/assign-contact-owner.js.map +1 -0
  286. package/src/lib/server/associate-company-by-domain.d.ts +70 -0
  287. package/src/lib/server/associate-company-by-domain.js +153 -0
  288. package/src/lib/server/associate-company-by-domain.js.map +1 -0
  289. package/src/lib/server/automation-steps.d.ts +139 -0
  290. package/src/lib/server/automation-steps.js +472 -0
  291. package/src/lib/server/automation-steps.js.map +1 -0
  292. package/src/lib/server/capture-contact.js +65 -21
  293. package/src/lib/server/capture-contact.js.map +1 -1
  294. package/src/lib/server/capture-host-contact.d.ts +104 -0
  295. package/src/lib/server/capture-host-contact.js +151 -0
  296. package/src/lib/server/capture-host-contact.js.map +1 -0
  297. package/src/lib/server/company-children.d.ts +25 -0
  298. package/src/lib/server/company-children.js +42 -0
  299. package/src/lib/server/company-children.js.map +1 -0
  300. package/src/lib/server/company-delete.d.ts +18 -0
  301. package/src/lib/server/company-delete.js +44 -21
  302. package/src/lib/server/company-delete.js.map +1 -1
  303. package/src/lib/server/contact-email-history.js +21 -19
  304. package/src/lib/server/contact-email-history.js.map +1 -1
  305. package/src/lib/server/contact-remove.js +27 -2
  306. package/src/lib/server/contact-remove.js.map +1 -1
  307. package/src/lib/server/contact-reports-to.d.ts +61 -0
  308. package/src/lib/server/contact-reports-to.js +114 -0
  309. package/src/lib/server/contact-reports-to.js.map +1 -0
  310. package/src/lib/server/contact-update.d.ts +42 -1
  311. package/src/lib/server/contact-update.js +204 -23
  312. package/src/lib/server/contact-update.js.map +1 -1
  313. package/src/lib/server/convert-host-lead.d.ts +46 -1
  314. package/src/lib/server/convert-host-lead.js +173 -15
  315. package/src/lib/server/convert-host-lead.js.map +1 -1
  316. package/src/lib/server/convert-open-lead.js +12 -5
  317. package/src/lib/server/convert-open-lead.js.map +1 -1
  318. package/src/lib/server/crm-record-indexes.d.ts +45 -0
  319. package/src/lib/server/crm-record-indexes.js +78 -0
  320. package/src/lib/server/crm-record-indexes.js.map +1 -0
  321. package/src/lib/server/crm-sharing.d.ts +9 -1
  322. package/src/lib/server/crm-sharing.js +23 -3
  323. package/src/lib/server/crm-sharing.js.map +1 -1
  324. package/src/lib/server/deal-contact-roles.d.ts +27 -0
  325. package/src/lib/server/deal-contact-roles.js +89 -0
  326. package/src/lib/server/deal-contact-roles.js.map +1 -0
  327. package/src/lib/server/lead-convert.d.ts +5 -0
  328. package/src/lib/server/lead-convert.js +19 -3
  329. package/src/lib/server/lead-convert.js.map +1 -1
  330. package/src/lib/server/lead-create.d.ts +17 -2
  331. package/src/lib/server/lead-create.js +46 -14
  332. package/src/lib/server/lead-create.js.map +1 -1
  333. package/src/lib/server/lead-nurturing.d.ts +6 -0
  334. package/src/lib/server/lead-nurturing.js +57 -20
  335. package/src/lib/server/lead-nurturing.js.map +1 -1
  336. package/src/lib/server/lead-picklists.d.ts +35 -0
  337. package/src/lib/server/lead-picklists.js +52 -0
  338. package/src/lib/server/lead-picklists.js.map +1 -0
  339. package/src/lib/server/lead-source-picklist.d.ts +1 -13
  340. package/src/lib/server/lead-source-picklist.js +6 -33
  341. package/src/lib/server/lead-source-picklist.js.map +1 -1
  342. package/src/lib/server/merge-contacts.js +22 -4
  343. package/src/lib/server/merge-contacts.js.map +1 -1
  344. package/src/lib/server/person-eraser.d.ts +53 -0
  345. package/src/lib/server/person-eraser.js +149 -0
  346. package/src/lib/server/person-eraser.js.map +1 -0
  347. package/src/lib/server/person-records.d.ts +69 -0
  348. package/src/lib/server/person-records.js +317 -0
  349. package/src/lib/server/person-records.js.map +1 -0
  350. package/src/lib/server/person-refund.d.ts +97 -0
  351. package/src/lib/server/person-refund.js +221 -0
  352. package/src/lib/server/person-refund.js.map +1 -0
  353. package/src/lib/server/picklist-values.d.ts +21 -0
  354. package/src/lib/server/picklist-values.js +308 -0
  355. package/src/lib/server/picklist-values.js.map +1 -0
  356. package/src/lib/server/read-picklist.d.ts +44 -0
  357. package/src/lib/server/read-picklist.js +65 -0
  358. package/src/lib/server/read-picklist.js.map +1 -0
  359. package/src/lib/server/recipe-routes.js +15 -7
  360. package/src/lib/server/recipe-routes.js.map +1 -1
  361. package/src/lib/server/record-email-state.js +7 -1
  362. package/src/lib/server/record-email-state.js.map +1 -1
  363. package/src/lib/server/record-facts.d.ts +3 -0
  364. package/src/lib/server/record-facts.js +233 -31
  365. package/src/lib/server/record-facts.js.map +1 -1
  366. package/src/lib/server/record-origin.d.ts +27 -0
  367. package/src/lib/server/record-origin.js +104 -0
  368. package/src/lib/server/record-origin.js.map +1 -0
  369. package/src/lib/server/record-timeline.js +15 -2
  370. package/src/lib/server/record-timeline.js.map +1 -1
  371. package/src/lib/server/suite-gate.js +4 -3
  372. package/src/lib/server/suite-gate.js.map +1 -1
  373. package/src/lib/server/task-routes.js +30 -16
  374. package/src/lib/server/task-routes.js.map +1 -1
  375. package/src/lib/server-deal-stage.d.ts +5 -1
  376. package/src/lib/server-deal-stage.js +10 -5
  377. package/src/lib/server-deal-stage.js.map +1 -1
  378. package/src/lib/server.d.ts +8 -3
  379. package/src/lib/server.js +47 -43
  380. package/src/lib/server.js.map +1 -1
  381. package/src/lib/testing/memory-firestore.d.ts +55 -0
  382. package/src/lib/testing/memory-firestore.js +335 -0
  383. package/src/lib/testing/memory-firestore.js.map +1 -0
  384. package/src/lib/testing/stand-in-email-send-index.d.ts +25 -0
  385. package/src/lib/testing/stand-in-email-send-index.js +49 -0
  386. package/src/lib/testing/stand-in-email-send-index.js.map +1 -0
  387. package/src/lib/transfer/activities.d.ts +30 -0
  388. package/src/lib/transfer/activities.js +132 -0
  389. package/src/lib/transfer/activities.js.map +1 -0
  390. package/src/lib/transfer/aliases.d.ts +22 -0
  391. package/src/lib/transfer/aliases.js +1342 -0
  392. package/src/lib/transfer/aliases.js.map +1 -0
  393. package/src/lib/transfer/common.d.ts +222 -0
  394. package/src/lib/transfer/common.js +832 -0
  395. package/src/lib/transfer/common.js.map +1 -0
  396. package/src/lib/transfer/companies.d.ts +43 -0
  397. package/src/lib/transfer/companies.js +549 -0
  398. package/src/lib/transfer/companies.js.map +1 -0
  399. package/src/lib/transfer/contacts.d.ts +31 -0
  400. package/src/lib/transfer/contacts.js +727 -0
  401. package/src/lib/transfer/contacts.js.map +1 -0
  402. package/src/lib/transfer/deals.d.ts +66 -0
  403. package/src/lib/transfer/deals.js +698 -0
  404. package/src/lib/transfer/deals.js.map +1 -0
  405. package/src/lib/transfer/email-templates-package.d.ts +40 -0
  406. package/src/lib/transfer/email-templates-package.js +76 -0
  407. package/src/lib/transfer/email-templates-package.js.map +1 -0
  408. package/src/lib/transfer/email-templates-package.server.d.ts +32 -0
  409. package/src/lib/transfer/email-templates-package.server.js +176 -0
  410. package/src/lib/transfer/email-templates-package.server.js.map +1 -0
  411. package/src/lib/transfer/fields.d.ts +102 -0
  412. package/src/lib/transfer/fields.js +1755 -0
  413. package/src/lib/transfer/fields.js.map +1 -0
  414. package/src/lib/transfer/leads.d.ts +30 -0
  415. package/src/lib/transfer/leads.js +582 -0
  416. package/src/lib/transfer/leads.js.map +1 -0
  417. package/src/lib/transfer/pipelines.d.ts +21 -0
  418. package/src/lib/transfer/pipelines.js +172 -0
  419. package/src/lib/transfer/pipelines.js.map +1 -0
  420. package/src/lib/transfer/register.d.ts +47 -0
  421. package/src/lib/transfer/register.js +277 -0
  422. package/src/lib/transfer/register.js.map +1 -0
  423. package/src/lib/transfer/tasks.d.ts +30 -0
  424. package/src/lib/transfer/tasks.js +418 -0
  425. package/src/lib/transfer/tasks.js.map +1 -0
  426. package/src/lib/transfer/ui.d.ts +20 -0
  427. package/src/lib/transfer/ui.js +42 -0
  428. package/src/lib/transfer/ui.js.map +1 -0
  429. package/src/lib/components/company-import-drawer.d.ts +0 -42
  430. package/src/lib/components/company-import-drawer.js +0 -99
  431. package/src/lib/components/company-import-drawer.js.map +0 -1
  432. package/src/lib/components/contact-import-drawer.d.ts +0 -46
  433. package/src/lib/components/contact-import-drawer.js +0 -149
  434. package/src/lib/components/contact-import-drawer.js.map +0 -1
  435. package/src/lib/components/crm-export-all-button.d.ts +0 -39
  436. package/src/lib/components/crm-export-all-button.js +0 -98
  437. package/src/lib/components/crm-export-all-button.js.map +0 -1
  438. package/src/lib/components/csv-import-drawer.d.ts +0 -139
  439. package/src/lib/components/csv-import-drawer.js +0 -757
  440. package/src/lib/components/csv-import-drawer.js.map +0 -1
  441. package/src/lib/components/deal-import-drawer.d.ts +0 -24
  442. package/src/lib/components/deal-import-drawer.js +0 -83
  443. package/src/lib/components/deal-import-drawer.js.map +0 -1
  444. package/src/lib/components/lead-import-drawer.d.ts +0 -27
  445. package/src/lib/components/lead-import-drawer.js +0 -118
  446. package/src/lib/components/lead-import-drawer.js.map +0 -1
  447. package/src/lib/components/lead-source-values-card.d.ts +0 -27
  448. package/src/lib/components/lead-source-values-card.js.map +0 -1
  449. package/src/lib/components/task-import-drawer.d.ts +0 -24
  450. package/src/lib/components/task-import-drawer.js +0 -82
  451. package/src/lib/components/task-import-drawer.js.map +0 -1
  452. package/src/lib/model/companies-csv.d.ts +0 -36
  453. package/src/lib/model/companies-csv.js +0 -38
  454. package/src/lib/model/companies-csv.js.map +0 -1
  455. package/src/lib/model/contacts-csv.d.ts +0 -54
  456. package/src/lib/model/contacts-csv.js +0 -67
  457. package/src/lib/model/contacts-csv.js.map +0 -1
  458. package/src/lib/model/crm-company-import.d.ts +0 -154
  459. package/src/lib/model/crm-company-import.js +0 -331
  460. package/src/lib/model/crm-company-import.js.map +0 -1
  461. package/src/lib/model/crm-csv.d.ts +0 -239
  462. package/src/lib/model/crm-csv.js +0 -467
  463. package/src/lib/model/crm-csv.js.map +0 -1
  464. package/src/lib/model/crm-deal-import.d.ts +0 -112
  465. package/src/lib/model/crm-deal-import.js +0 -263
  466. package/src/lib/model/crm-deal-import.js.map +0 -1
  467. package/src/lib/model/crm-import.d.ts +0 -250
  468. package/src/lib/model/crm-import.js +0 -467
  469. package/src/lib/model/crm-import.js.map +0 -1
  470. package/src/lib/model/crm-lead-import.d.ts +0 -138
  471. package/src/lib/model/crm-lead-import.js +0 -446
  472. package/src/lib/model/crm-lead-import.js.map +0 -1
  473. package/src/lib/model/crm-task-import.d.ts +0 -94
  474. package/src/lib/model/crm-task-import.js +0 -275
  475. package/src/lib/model/crm-task-import.js.map +0 -1
  476. package/src/lib/model/deals-csv.d.ts +0 -32
  477. package/src/lib/model/deals-csv.js +0 -36
  478. package/src/lib/model/deals-csv.js.map +0 -1
  479. package/src/lib/model/import-column-shapes.d.ts +0 -36
  480. package/src/lib/model/import-column-shapes.js +0 -76
  481. package/src/lib/model/import-column-shapes.js.map +0 -1
  482. package/src/lib/model/lead-source-values.d.ts +0 -82
  483. package/src/lib/model/lead-source-values.js +0 -149
  484. package/src/lib/model/lead-source-values.js.map +0 -1
  485. package/src/lib/model/leads-csv.d.ts +0 -40
  486. package/src/lib/model/leads-csv.js +0 -40
  487. package/src/lib/model/leads-csv.js.map +0 -1
  488. package/src/lib/model/tasks-csv.d.ts +0 -28
  489. package/src/lib/model/tasks-csv.js +0 -28
  490. package/src/lib/model/tasks-csv.js.map +0 -1
  491. package/src/lib/server/companies-import.d.ts +0 -57
  492. package/src/lib/server/companies-import.js +0 -242
  493. package/src/lib/server/companies-import.js.map +0 -1
  494. package/src/lib/server/contacts-import.d.ts +0 -109
  495. package/src/lib/server/contacts-import.js +0 -393
  496. package/src/lib/server/contacts-import.js.map +0 -1
  497. package/src/lib/server/deals-import.d.ts +0 -76
  498. package/src/lib/server/deals-import.js +0 -248
  499. package/src/lib/server/deals-import.js.map +0 -1
  500. package/src/lib/server/export-route.d.ts +0 -60
  501. package/src/lib/server/export-route.js +0 -447
  502. package/src/lib/server/export-route.js.map +0 -1
  503. package/src/lib/server/import-context.d.ts +0 -87
  504. package/src/lib/server/import-context.js +0 -182
  505. package/src/lib/server/import-context.js.map +0 -1
  506. package/src/lib/server/lead-source-values.d.ts +0 -50
  507. package/src/lib/server/lead-source-values.js +0 -191
  508. package/src/lib/server/lead-source-values.js.map +0 -1
  509. package/src/lib/server/leads-import.d.ts +0 -84
  510. package/src/lib/server/leads-import.js +0 -367
  511. package/src/lib/server/leads-import.js.map +0 -1
  512. package/src/lib/server/tasks-import.d.ts +0 -54
  513. package/src/lib/server/tasks-import.js +0 -160
  514. package/src/lib/server/tasks-import.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/plan-entitlements.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// Types only: the manifest generator loads this module on its own to compile\n// the plan figures, so nothing here may reach a core module at runtime.\nimport type { OrgPlan } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { PluginPlanEntitlementsDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-plan-entitlements'\n\n/**\n * THE CRM'S PLAN BAND (AGL-3080): the one-to-one email pace each plan\n * includes. Compiled into core's `PLAN_ENTITLEMENTS` by the manifest\n * generator (`register.planEntitlements`); this is where the figure is\n * written, beside the reasoning that sized it.\n */\ndeclare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {\n interface PluginEntitlementQuotas {\n /**\n * One-to-one emails a workspace may send from CRM records per UTC day —\n * the message a rep writes to one person from their contact page, as\n * against a campaign (`emailSendsPerMonth`) or transactional mail.\n *\n * A HARD cap on every tier and a daily one, because it is the only send\n * class a person can produce by hand at volume: a campaign is one act\n * over a metered audience, and a receipt follows an order somebody paid\n * for, but a rep with a template and a list can put a thousand messages\n * onto the platform's sending reputation in an afternoon. The day\n * boundary is what makes the cap a pace rather than a wall — tomorrow the\n * count is zero again — and the number is sized so that every tier holds\n * its margin with the whole day spent, which is the arithmetic the Drive\n * pricing decision of 2026-09-05 records. `checkCrmEmailQuota` reads it;\n * the counter it is enforced against is\n * `orgs/{orgId}/crmEmailUsage/{day}`.\n *\n * Every send still counts on the org's `emailSends` cost meter, like any\n * other message the provider charged for. 0 on Free, which has no CRM,\n * and finite on every paid tier, Enterprise's 2,000 included.\n *\n * ## This ladder and `emailSendsPerMonth` are NOT the same ladder\n *\n * They answer to different principles, deliberately, and a tier's two\n * numbers are not expected to look like each other. `emailSendsPerMonth`\n * is sized against SHARED-DOMAIN REPUTATION: campaign mail is bulk on a\n * pool every tier without its own verified sending domain rides under\n * `p=reject`, so what one workspace sends is a cost every other workspace\n * pays. This one is sized against COGS SHARE: one-to-one mail is a\n * per-tier margin question, which is why it is a daily pace rather than a\n * monthly band.\n *\n * The two are far apart at the entry tier — a band of 0 beside a real\n * daily allowance — and that reads as an inconsistency until the\n * principles are named. It is not one. One-to-one mail is addressed to a\n * single person, usually inside a relationship the recipient started, and\n * it reaches the provider through a per-user pace limit, both suppression\n * lists, a `declined` consent basis as a hard stop, and an atomic daily\n * reservation. Complaint rates on it are structurally unlike bulk.\n * Reconciling the two ladders would price a reply to one customer as\n * though it carried a campaign's reputation risk.\n */\n crmEmailsPerDay?: number\n }\n}\n\n/** The one-to-one emails each plan may send a UTC day — see the key's docblock. */\nexport const CRM_EMAILS_PER_DAY_BY_PLAN: Readonly<Record<OrgPlan, number>> = {\n // No one-to-one email on Free, which has no CRM: the send lives on a\n // record's page, and Free opens no record. Zero here is what\n // `checkCrmEmailQuota` refuses against, so a per-org grant of\n // `features.crm` alone still sends nothing until the band is raised with\n // it.\n free: 0,\n // One-to-one email from a CRM record, per UTC day (AGL-2611). A hard daily\n // pace rather than a monthly meter, sized so the whole day spent at $0.0009\n // a message leaves the tier's CRM axis inside the 20% cost share the Drive\n // pricing decision of 2026-09-05 sets. 50 a day is $1.35 a month against a\n // $16 annual price. Every send still lands on the `emailSends` cost meter\n // beside the transactional mail this tier already sends.\n starter: 50,\n pro: 150,\n business: 200,\n scale: 300,\n advanced: 500,\n agency: 1000,\n // Finite like the campaign allowance beside it, and for the same reason\n // one-to-one mail is capped on every other tier: a rep with a template and\n // a list can put thousands of messages onto the platform's sending\n // reputation in an afternoon. A negotiated agreement whose reps need more\n // than 2,000 a day says so, and the override is what carries it.\n enterprise: 2_000,\n}\n\n/** The CRM's plan figures, for the manifest generator. */\nexport function crmPlanEntitlements(): PluginPlanEntitlementsDeclaration {\n return {\n quotas: [\n {\n key: 'crmEmailsPerDay',\n label: 'One-to-one emails / day',\n byPlan: CRM_EMAILS_PER_DAY_BY_PLAN,\n },\n ],\n }\n}\n"],"names":["CRM_EMAILS_PER_DAY_BY_PLAN","free","starter","pro","business","scale","advanced","agency","enterprise","crmPlanEntitlements","quotas","key","label","byPlan"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,wEAAwE;AA0DxE,iFAAiF,GACjF,OAAO,MAAMA,6BAAgE;IAC3E,qEAAqE;IACrE,6DAA6D;IAC7D,8DAA8D;IAC9D,yEAAyE;IACzE,MAAM;IACNC,MAAM;IACN,2EAA2E;IAC3E,4EAA4E;IAC5E,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,yDAAyD;IACzDC,SAAS;IACTC,KAAK;IACLC,UAAU;IACVC,OAAO;IACPC,UAAU;IACVC,QAAQ;IACR,wEAAwE;IACxE,2EAA2E;IAC3E,mEAAmE;IACnE,0EAA0E;IAC1E,iEAAiE;IACjEC,YAAY;AACd,EAAC;AAED,wDAAwD,GACxD,OAAO,SAASC;IACd,OAAO;QACLC,QAAQ;YACN;gBACEC,KAAK;gBACLC,OAAO;gBACPC,QAAQb;YACV;SACD;IACH;AACF"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/plan-entitlements.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// Types only: the manifest generator loads this module on its own to compile\n// the plan figures, so nothing here may reach a core module at runtime.\nimport type { OrgPlan } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { PluginPlanEntitlementsDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-plan-entitlements'\n\n/**\n * THE CRM'S PLAN BAND (AGL-3080): the one-to-one email pace each plan\n * includes. Compiled into core's `PLAN_ENTITLEMENTS` by the manifest\n * generator (`register.planEntitlements`); this is where the figure is\n * written, beside the reasoning that sized it.\n */\ndeclare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {\n interface PluginEntitlementQuotas {\n /**\n * One-to-one emails a workspace may send from CRM records per UTC day —\n * the message a rep writes to one person from their contact page, as\n * against a campaign (`emailSendsPerMonth`) or transactional mail.\n *\n * A HARD cap on every tier and a daily one, because it is the only send\n * class a person can produce by hand at volume: a campaign is one act\n * over a metered audience, and a receipt follows an order somebody paid\n * for, but a rep with a template and a list can put a thousand messages\n * onto the platform's sending reputation in an afternoon. The day\n * boundary is what makes the cap a pace rather than a wall — tomorrow the\n * count is zero again — and the number is sized so that every tier holds\n * its margin with the whole day spent, which is the arithmetic the Drive\n * pricing decision of 2026-09-05 records. `checkCrmEmailQuota` reads it;\n * the counter it is enforced against is\n * `orgs/{orgId}/crmEmailUsage/{day}`.\n *\n * Every send still counts on the org's `emailSends` cost meter, like any\n * other message the provider charged for. 0 on Free, which has no CRM,\n * and finite on every paid tier, Enterprise's 2,000 included.\n *\n * ## This ladder and `emailSendsPerMonth` are NOT the same ladder\n *\n * They answer to different principles, deliberately, and a tier's two\n * numbers are not expected to look like each other. `emailSendsPerMonth`\n * is sized against SHARED-DOMAIN REPUTATION: campaign mail is bulk on a\n * pool every tier without its own verified sending domain rides under\n * `p=reject`, so what one workspace sends is a cost every other workspace\n * pays. This one is sized against COGS SHARE: one-to-one mail is a\n * per-tier margin question, which is why it is a daily pace rather than a\n * monthly band.\n *\n * The two are far apart at the entry tier — a band of 0 beside a real\n * daily allowance — and that reads as an inconsistency until the\n * principles are named. It is not one. One-to-one mail is addressed to a\n * single person, usually inside a relationship the recipient started, and\n * it reaches the provider through a per-user pace limit, both suppression\n * lists, a `declined` consent basis as a hard stop, and an atomic daily\n * reservation. Complaint rates on it are structurally unlike bulk.\n * Reconciling the two ladders would price a reply to one customer as\n * though it carried a campaign's reputation risk.\n */\n crmEmailsPerDay?: number\n }\n}\n\n/** The one-to-one emails each plan may send a UTC day — see the key's docblock. */\nexport const CRM_EMAILS_PER_DAY_BY_PLAN: Readonly<Record<OrgPlan, number>> = {\n // No one-to-one email on Free, which has no CRM: the send lives on a\n // record's page, and Free opens no record. Zero here is what\n // `checkCrmEmailQuota` refuses against, so a per-org grant of\n // `features.crm` alone still sends nothing until the band is raised with\n // it.\n free: 0,\n // One-to-one email from a CRM record, per UTC day (AGL-2611). A hard daily\n // pace rather than a monthly meter, sized so the whole day spent at $0.0009\n // a message leaves the tier's CRM axis inside the 20% cost share the Drive\n // pricing decision of 2026-09-05 sets. 35 a day is $0.95 a month against a\n // $16 annual price — 50 was, until the whole Starter tier had to cover its\n // full-use cost plus 30% (AGL-3469). Every send still lands on the\n // `emailSends` cost meter beside the transactional mail this tier already\n // sends.\n starter: 35,\n pro: 150,\n business: 200,\n scale: 300,\n advanced: 500,\n agency: 1000,\n // Finite like the campaign allowance beside it, and for the same reason\n // one-to-one mail is capped on every other tier: a rep with a template and\n // a list can put thousands of messages onto the platform's sending\n // reputation in an afternoon. A negotiated agreement whose reps need more\n // than 2,000 a day says so, and the override is what carries it.\n enterprise: 2_000,\n}\n\n/** The CRM's plan figures, for the manifest generator. */\nexport function crmPlanEntitlements(): PluginPlanEntitlementsDeclaration {\n return {\n quotas: [\n {\n key: 'crmEmailsPerDay',\n label: 'One-to-one emails / day',\n byPlan: CRM_EMAILS_PER_DAY_BY_PLAN,\n },\n ],\n }\n}\n"],"names":["CRM_EMAILS_PER_DAY_BY_PLAN","free","starter","pro","business","scale","advanced","agency","enterprise","crmPlanEntitlements","quotas","key","label","byPlan"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,wEAAwE;AA0DxE,iFAAiF,GACjF,OAAO,MAAMA,6BAAgE;IAC3E,qEAAqE;IACrE,6DAA6D;IAC7D,8DAA8D;IAC9D,yEAAyE;IACzE,MAAM;IACNC,MAAM;IACN,2EAA2E;IAC3E,4EAA4E;IAC5E,2EAA2E;IAC3E,2EAA2E;IAC3E,2EAA2E;IAC3E,mEAAmE;IACnE,0EAA0E;IAC1E,SAAS;IACTC,SAAS;IACTC,KAAK;IACLC,UAAU;IACVC,OAAO;IACPC,UAAU;IACVC,QAAQ;IACR,wEAAwE;IACxE,2EAA2E;IAC3E,mEAAmE;IACnE,0EAA0E;IAC1E,iEAAiE;IACjEC,YAAY;AACd,EAAC;AAED,wDAAwD,GACxD,OAAO,SAASC;IACd,OAAO;QACLC,QAAQ;YACN;gBACEC,KAAK;gBACLC,OAAO;gBACPC,QAAQb;YACV;SACD;IACH;AACF"}
package/src/lib/plugin.js CHANGED
@@ -19,10 +19,14 @@ import { lazy } from "react";
19
19
  import { CRM_CONSOLE_SECTIONS } from "./components/crm-console-sections.js";
20
20
  import { CrmGlanceCard } from "./components/crm-glance-card.js";
21
21
  import { BUNDLE_ID } from "./constants/bundle-common.js";
22
+ import { registerCrmTransferResourceUis } from "./transfer/ui.js";
22
23
  import { withCrmOrgMount } from "./hooks/use-crm-org-mount.js";
24
+ import { registerCrmRecordCounts } from "./model/crm-record-counts.js";
23
25
  import { registerCrmRecordRoutes } from "./model/crm-record-routes.js";
26
+ import { registerCrmRecordLists } from "./model/crm-record-lists.js";
24
27
  import { CRM_SEARCH_SOURCES } from "./model/crm-search-sources.js";
25
28
  import { registerPluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
29
+ import { registerPluginTransferResourceUi } from "@aglyn/aglyn/plugin-manager/plugin-transfer-resources";
26
30
  import { CRM_RECORD_ATTRIBUTION_ZONE } from "./components/crm-attribution-zone.js";
27
31
  import { CRM_RECORD_BOOKING_ZONE } from "./components/crm-booking-zone.js";
28
32
  /** Code-split: the CRM hub only loads when opened. */ const CrmConsolePage = lazy(()=>import("./components/crm-console-page.js"));
@@ -43,7 +47,20 @@ const CrmTasksDueCard = lazy(()=>import("./components/crm-tasks-due-card.js"));
43
47
  * once that backfill reported nothing left (AGL-2614). The `/contacts`
44
48
  * address is a URL, not a stored id, and the nav item keeps redirecting it.
45
49
  */ export function registerCrmConsole() {
50
+ // How the Import & export hub names email templates in a workspace package (AGL-3535).
51
+ registerPluginTransferResourceUi('crm.email-templates', {
52
+ label: 'Email templates'
53
+ }, {
54
+ pluginId: BUNDLE_ID
55
+ });
46
56
  registerCrmRecordRoutes();
57
+ // How the import wizard and the export dialog name the CRM's records (AGL-3527).
58
+ registerCrmTransferResourceUis(BUNDLE_ID);
59
+ // The CRM's records as another plugin's picker lists them (AGL-3080).
60
+ registerCrmRecordLists();
61
+ // The leads a site captured and the organization's contacts, counted for
62
+ // another plugin's figure.
63
+ registerCrmRecordCounts();
47
64
  registerPluginZone({
48
65
  zone: CRM_RECORD_BOOKING_ZONE,
49
66
  label: 'Book a meeting from a CRM record',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/plugin.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 { mdiCardAccountDetailsOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { CRM_CONSOLE_SECTIONS } from './components/crm-console-sections'\nimport { CrmGlanceCard } from './components/crm-glance-card'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { withCrmOrgMount } from './hooks/use-crm-org-mount'\nimport { registerCrmRecordRoutes } from './model/crm-record-routes'\nimport { CRM_SEARCH_SOURCES } from './model/crm-search-sources'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { CRM_RECORD_ATTRIBUTION_ZONE } from './components/crm-attribution-zone'\nimport { CRM_RECORD_BOOKING_ZONE } from './components/crm-booking-zone'\n\n/** Code-split: the CRM hub only loads when opened. */\nconst CrmConsolePage = lazy(\n () => import('./components/crm-console-page'),\n)\n/** The dashboard glance, split the same way: it loads where the slot mounts it. */\n/** Where a form's fields save on a contact, drawn on the form's own page. */\nconst FormContactFieldsCard = lazy(\n () => import('./components/form-contact-fields-card'),\n)\n\nconst CrmTasksDueCard = lazy(\n () => import('./components/crm-tasks-due-card'),\n)\n\n/**\n * CRM feature plugin (AGL-395, the hub since AGL-2595). Console-only — its\n * records live in Firestore and have no canvas element, so there is no UI\n * bundle. The console half declares the CRM nav + hub through the\n * ConsoleExtension registry (release_crm gate via the nav tab); the\n * contacts section reads the `contactsPerHost` quota off the shell-passed\n * `org`.\n *\n * The plugin id is `crm` (AGL-2595). It was `contacts` while the surface was\n * one list; the id is persisted in every org's `enabledPlugins` and every\n * host's `disabledPlugins`, so the runtime read the old value as this one\n * until the stored documents had been rewritten, and the alias was retired\n * once that backfill reported nothing left (AGL-2614). The `/contacts`\n * address is a URL, not a stored id, and the nav item keeps redirecting it.\n */\nexport function registerCrmConsole(): void {\n registerCrmRecordRoutes()\n registerPluginZone(\n {\n zone: CRM_RECORD_BOOKING_ZONE,\n label: 'Book a meeting from a CRM record',\n surface: 'console',\n // One control in a record header's row of actions, and one beside the\n // composer's fields: the page lays them out, so the zone adds nothing.\n layout: 'bare',\n description:\n 'In a record’s header and beside the one-to-one composer. A widget here offers the site’s bookable services and the link a visitor books one at; it is handed the site, the record and, from the composer, a way to insert the chosen link. It writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: CRM_RECORD_ATTRIBUTION_ZONE,\n label: 'Where a CRM record came from',\n surface: 'console',\n description:\n 'On a contact’s page and in a lead’s history. A widget here says which campaign or link brought the person; it is handed the site, what the record is and its id, and it writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'CRM',\n /*\n * WHO may open the CRM, declared so the shell enforces it.\n *\n * `navTabId` below is a release flag, not authorization: it says whether\n * the surface has shipped, and `FeatureGate` reads it as\n * `released || isStaff`. Nothing above this page decided who among a\n * workspace's members may read it, and the page is a full editor over\n * org-shared people data — names, addresses, order history, notes,\n * consent, and a CSV export of all of it.\n *\n * `data.manage` rather than a key of this plugin's own, because it is\n * the catalog key whose subject is exactly this data. The Firestore\n * rules place contacts in the org-shared data block with datasets and\n * gate writes on `canWriteOrgData()` — owner, admin, editor — which is\n * the population `data.manage` defaults to, so the console gate and the\n * rules agree on the built-in roles by construction. A custom role or a\n * per-member override then narrows the console further, and the rules\n * stay the boundary for the data itself.\n *\n * Minting a `contacts.*` key instead would advertise a control that does\n * not exist. Console contact writes are client-direct against the rules,\n * so a new key would have no server enforcement point to sit on, and a\n * permission a customer can untick that changes nothing is worse than\n * its absence.\n *\n * A SITE COLLABORATOR holding it reaches this page deliberately. They\n * are a real org member document, so this gate is the first thing that\n * has ever asked about them here — and the answer is yes, scoped: the\n * listener filters on `visibleTo`, the rules prove the same predicate\n * per document, and every field the page shows is read through the\n * viewing group's own facet. Refusing them outright would delete a\n * shipped capability rather than close a hole.\n */\n permission: 'data.manage',\n /*\n * WHAT the workspace must have bought (AGL-2851). The CRM is included\n * from Starter and a Free workspace has none of it, so the flag is\n * declared once, here, and the shell refuses every surface this\n * registers on a plan without it: the hub body at both mounts, every\n * section and record beneath it, drawn locked beside the upgrade notice,\n * and the dashboard cards. The nav tab stays, as every plan-locked tab\n * does, because it is the way to that notice. The CRM's routes, `/v1`\n * and the Firestore rules ask the same question where the data is\n * (`server/suite-gate.ts`).\n */\n featureFlag: 'crm',\n // What the console's search finds here, under the two gates above.\n searchSources: CRM_SEARCH_SOURCES,\n /*\n * Dashboard cards (AGL-2599, AGL-2604): the site dashboard's `hostDashboard`\n * slot composes this extension's permission over each, so a card appears\n * only where the CRM is enabled and only for a reader who may open it —\n * registered here rather than imported by the dashboard so the shell's\n * enablement and entitlement gate decides whether a site that never turned\n * the CRM on sees them, the rule every card on that row follows. The tasks\n * card is a glance at the reader's own overdue and due-today work and\n * renders nothing on a workspace that has never made a task; the glance\n * card is four server-counted figures, each a link into the hub.\n *\n * Both name `features.crm`, as the extension does (AGL-2611, AGL-2851):\n * the slot composes the two, so a plan without the CRM gets neither card\n * — absent, not upsold, the treatment every gated card gets.\n *\n * The same two cards again on the organization's `orgDashboard` slot\n * (AGL-2636) — the org's sites page — under the same flag, with the same\n * ids: the id names the card, and it is one card totaling the whole\n * organization there instead of one site. The shell hands that slot the\n * org mount as a prop, and `withCrmOrgMount` is what puts the mount's\n * provider around each card, since the sites page cannot import one.\n */\n widgets: [\n // Which contact field each of a form's fields saves to, on the form's\n // own page. The forms plugin hosts the zone and writes the form\n // document; what a contact's fields ARE is this plugin's, so the card\n // is too.\n {\n slot: 'formContactFields',\n widgetId: 'crm-form-contact-fields',\n title: 'Saves to contact fields',\n // A contact's custom fields are part of the suite, so a workspace\n // without it has none to map a form's field onto.\n featureFlag: 'crm',\n Component: FormContactFieldsCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: CrmTasksDueCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n Component: CrmGlanceCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: withCrmOrgMount(CrmTasksDueCard),\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n Component: withCrmOrgMount(CrmGlanceCard),\n },\n ],\n navItems: [\n {\n label: 'CRM',\n href: '/crm',\n // The address the surface had while it was one list. A link kept\n // from then — a bookmark, a docs page, an email — still opens the\n // hub rather than the shell's \"not available\" notice.\n legacyHrefs: ['/contacts'],\n // Sections as ROUTES (AGL-2595): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read and a\n // bare `/crm` lands on the first. Every section inherits this\n // item's `release_crm` gate.\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n },\n Component: CrmConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's CRM hub (AGL-2630), at `/[orgSlug]/crm`: the same\n * sections over every site at once, mounted with no site and the org's\n * sites as `orgMount`. Its listeners carry no scope clause, so the\n * shell's org route refuses a site collaborator on REACH before it asks\n * the `data.manage` declared above; a permission alone, which a\n * collaborator's role can carry, never opens it.\n *\n * It carries the SITE tab's id on purpose: `release_contacts` names\n * `nav-tab-contacts`, so one flag holds both halves of the CRM, and the\n * org half cannot ship while the site half is switched off. First among\n * the organization's plugin tabs, beside Data, the org's other shared\n * collection of records. The help opens the docs heading about this\n * mount. Its old address, `/[orgSlug]/contacts`, is a console redirect\n * (`consoleRedirects`), so it needs no `legacyHrefs` here.\n */\n orgNavItems: [\n {\n label: 'CRM',\n href: '/crm',\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n tabOrder: -100,\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n docsAnchor: '#at-the-organization-level',\n },\n Component: CrmConsolePage,\n },\n ],\n })\n}\n"],"names":["Aglyn","mdiCardAccountDetailsOutline","lazy","CRM_CONSOLE_SECTIONS","CrmGlanceCard","BUNDLE_ID","withCrmOrgMount","registerCrmRecordRoutes","CRM_SEARCH_SOURCES","registerPluginZone","CRM_RECORD_ATTRIBUTION_ZONE","CRM_RECORD_BOOKING_ZONE","CrmConsolePage","FormContactFieldsCard","CrmTasksDueCard","registerCrmConsole","zone","label","surface","layout","description","pluginId","registerConsoleExtension","displayName","permission","featureFlag","searchSources","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","hostDashboard","orgDashboard","navItems","href","legacyHrefs","sections","navTabId","icon","path","header","docsTopic","orgNavItems","tabOrder","docsAnchor"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/plugin.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 { mdiCardAccountDetailsOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { CRM_CONSOLE_SECTIONS } from './components/crm-console-sections'\nimport { CrmGlanceCard } from './components/crm-glance-card'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerCrmTransferResourceUis } from './transfer/ui'\nimport { withCrmOrgMount } from './hooks/use-crm-org-mount'\nimport { registerCrmRecordCounts } from './model/crm-record-counts'\nimport { registerCrmRecordRoutes } from './model/crm-record-routes'\nimport { registerCrmRecordLists } from './model/crm-record-lists'\nimport { CRM_SEARCH_SOURCES } from './model/crm-search-sources'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { CRM_RECORD_ATTRIBUTION_ZONE } from './components/crm-attribution-zone'\nimport { CRM_RECORD_BOOKING_ZONE } from './components/crm-booking-zone'\n\n/** Code-split: the CRM hub only loads when opened. */\nconst CrmConsolePage = lazy(\n () => import('./components/crm-console-page'),\n)\n/** The dashboard glance, split the same way: it loads where the slot mounts it. */\n/** Where a form's fields save on a contact, drawn on the form's own page. */\nconst FormContactFieldsCard = lazy(\n () => import('./components/form-contact-fields-card'),\n)\n\nconst CrmTasksDueCard = lazy(\n () => import('./components/crm-tasks-due-card'),\n)\n\n/**\n * CRM feature plugin (AGL-395, the hub since AGL-2595). Console-only — its\n * records live in Firestore and have no canvas element, so there is no UI\n * bundle. The console half declares the CRM nav + hub through the\n * ConsoleExtension registry (release_crm gate via the nav tab); the\n * contacts section reads the `contactsPerHost` quota off the shell-passed\n * `org`.\n *\n * The plugin id is `crm` (AGL-2595). It was `contacts` while the surface was\n * one list; the id is persisted in every org's `enabledPlugins` and every\n * host's `disabledPlugins`, so the runtime read the old value as this one\n * until the stored documents had been rewritten, and the alias was retired\n * once that backfill reported nothing left (AGL-2614). The `/contacts`\n * address is a URL, not a stored id, and the nav item keeps redirecting it.\n */\nexport function registerCrmConsole(): void {\n // How the Import & export hub names email templates in a workspace package (AGL-3535).\n registerPluginTransferResourceUi('crm.email-templates', { label: 'Email templates' }, { pluginId: BUNDLE_ID })\n registerCrmRecordRoutes()\n // How the import wizard and the export dialog name the CRM's records (AGL-3527).\n registerCrmTransferResourceUis(BUNDLE_ID)\n // The CRM's records as another plugin's picker lists them (AGL-3080).\n registerCrmRecordLists()\n // The leads a site captured and the organization's contacts, counted for\n // another plugin's figure.\n registerCrmRecordCounts()\n registerPluginZone(\n {\n zone: CRM_RECORD_BOOKING_ZONE,\n label: 'Book a meeting from a CRM record',\n surface: 'console',\n // One control in a record header's row of actions, and one beside the\n // composer's fields: the page lays them out, so the zone adds nothing.\n layout: 'bare',\n description:\n 'In a record’s header and beside the one-to-one composer. A widget here offers the site’s bookable services and the link a visitor books one at; it is handed the site, the record and, from the composer, a way to insert the chosen link. It writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: CRM_RECORD_ATTRIBUTION_ZONE,\n label: 'Where a CRM record came from',\n surface: 'console',\n description:\n 'On a contact’s page and in a lead’s history. A widget here says which campaign or link brought the person; it is handed the site, what the record is and its id, and it writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'CRM',\n /*\n * WHO may open the CRM, declared so the shell enforces it.\n *\n * `navTabId` below is a release flag, not authorization: it says whether\n * the surface has shipped, and `FeatureGate` reads it as\n * `released || isStaff`. Nothing above this page decided who among a\n * workspace's members may read it, and the page is a full editor over\n * org-shared people data — names, addresses, order history, notes,\n * consent, and a CSV export of all of it.\n *\n * `data.manage` rather than a key of this plugin's own, because it is\n * the catalog key whose subject is exactly this data. The Firestore\n * rules place contacts in the org-shared data block with datasets and\n * gate writes on `canWriteOrgData()` — owner, admin, editor — which is\n * the population `data.manage` defaults to, so the console gate and the\n * rules agree on the built-in roles by construction. A custom role or a\n * per-member override then narrows the console further, and the rules\n * stay the boundary for the data itself.\n *\n * Minting a `contacts.*` key instead would advertise a control that does\n * not exist. Console contact writes are client-direct against the rules,\n * so a new key would have no server enforcement point to sit on, and a\n * permission a customer can untick that changes nothing is worse than\n * its absence.\n *\n * A SITE COLLABORATOR holding it reaches this page deliberately. They\n * are a real org member document, so this gate is the first thing that\n * has ever asked about them here — and the answer is yes, scoped: the\n * listener filters on `visibleTo`, the rules prove the same predicate\n * per document, and every field the page shows is read through the\n * viewing group's own facet. Refusing them outright would delete a\n * shipped capability rather than close a hole.\n */\n permission: 'data.manage',\n /*\n * WHAT the workspace must have bought (AGL-2851). The CRM is included\n * from Starter and a Free workspace has none of it, so the flag is\n * declared once, here, and the shell refuses every surface this\n * registers on a plan without it: the hub body at both mounts, every\n * section and record beneath it, drawn locked beside the upgrade notice,\n * and the dashboard cards. The nav tab stays, as every plan-locked tab\n * does, because it is the way to that notice. The CRM's routes, `/v1`\n * and the Firestore rules ask the same question where the data is\n * (`server/suite-gate.ts`).\n */\n featureFlag: 'crm',\n // What the console's search finds here, under the two gates above.\n searchSources: CRM_SEARCH_SOURCES,\n /*\n * Dashboard cards (AGL-2599, AGL-2604): the site dashboard's `hostDashboard`\n * slot composes this extension's permission over each, so a card appears\n * only where the CRM is enabled and only for a reader who may open it —\n * registered here rather than imported by the dashboard so the shell's\n * enablement and entitlement gate decides whether a site that never turned\n * the CRM on sees them, the rule every card on that row follows. The tasks\n * card is a glance at the reader's own overdue and due-today work and\n * renders nothing on a workspace that has never made a task; the glance\n * card is four server-counted figures, each a link into the hub.\n *\n * Both name `features.crm`, as the extension does (AGL-2611, AGL-2851):\n * the slot composes the two, so a plan without the CRM gets neither card\n * — absent, not upsold, the treatment every gated card gets.\n *\n * The same two cards again on the organization's `orgDashboard` slot\n * (AGL-2636) — the org's sites page — under the same flag, with the same\n * ids: the id names the card, and it is one card totaling the whole\n * organization there instead of one site. The shell hands that slot the\n * org mount as a prop, and `withCrmOrgMount` is what puts the mount's\n * provider around each card, since the sites page cannot import one.\n */\n widgets: [\n // Which contact field each of a form's fields saves to, on the form's\n // own page. The forms plugin hosts the zone and writes the form\n // document; what a contact's fields ARE is this plugin's, so the card\n // is too.\n {\n slot: 'formContactFields',\n widgetId: 'crm-form-contact-fields',\n title: 'Saves to contact fields',\n // A contact's custom fields are part of the suite, so a workspace\n // without it has none to map a form's field onto.\n featureFlag: 'crm',\n Component: FormContactFieldsCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: CrmTasksDueCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n Component: CrmGlanceCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: withCrmOrgMount(CrmTasksDueCard),\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n Component: withCrmOrgMount(CrmGlanceCard),\n },\n ],\n navItems: [\n {\n label: 'CRM',\n href: '/crm',\n // The address the surface had while it was one list. A link kept\n // from then — a bookmark, a docs page, an email — still opens the\n // hub rather than the shell's \"not available\" notice.\n legacyHrefs: ['/contacts'],\n // Sections as ROUTES (AGL-2595): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read and a\n // bare `/crm` lands on the first. Every section inherits this\n // item's `release_crm` gate.\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n },\n Component: CrmConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's CRM hub (AGL-2630), at `/[orgSlug]/crm`: the same\n * sections over every site at once, mounted with no site and the org's\n * sites as `orgMount`. Its listeners carry no scope clause, so the\n * shell's org route refuses a site collaborator on REACH before it asks\n * the `data.manage` declared above; a permission alone, which a\n * collaborator's role can carry, never opens it.\n *\n * It carries the SITE tab's id on purpose: `release_contacts` names\n * `nav-tab-contacts`, so one flag holds both halves of the CRM, and the\n * org half cannot ship while the site half is switched off. First among\n * the organization's plugin tabs, beside Data, the org's other shared\n * collection of records. The help opens the docs heading about this\n * mount. Its old address, `/[orgSlug]/contacts`, is a console redirect\n * (`consoleRedirects`), so it needs no `legacyHrefs` here.\n */\n orgNavItems: [\n {\n label: 'CRM',\n href: '/crm',\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n tabOrder: -100,\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n docsAnchor: '#at-the-organization-level',\n },\n Component: CrmConsolePage,\n },\n ],\n })\n}\n"],"names":["Aglyn","mdiCardAccountDetailsOutline","lazy","CRM_CONSOLE_SECTIONS","CrmGlanceCard","BUNDLE_ID","registerCrmTransferResourceUis","withCrmOrgMount","registerCrmRecordCounts","registerCrmRecordRoutes","registerCrmRecordLists","CRM_SEARCH_SOURCES","registerPluginZone","registerPluginTransferResourceUi","CRM_RECORD_ATTRIBUTION_ZONE","CRM_RECORD_BOOKING_ZONE","CrmConsolePage","FormContactFieldsCard","CrmTasksDueCard","registerCrmConsole","label","pluginId","zone","surface","layout","description","registerConsoleExtension","displayName","permission","featureFlag","searchSources","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","hostDashboard","orgDashboard","navItems","href","legacyHrefs","sections","navTabId","icon","path","header","docsTopic","orgNavItems","tabOrder","docsAnchor"],"mappings":"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"}
@@ -15,7 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { nameSearchFields } from "@aglyn/aglyn/app-utils/name-search";
18
- import { CAPTURED_BY_HOST_FIELD, CONTACT_COMPANY_IDS_FIELD, CONTACT_FACETS_FIELD, CONTACT_FIELDS_MAX_PER_ORG, CONTACT_LIFECYCLE_STAGES, consentGroupForHost, contactFacetPath, createResourceUid, CRM_COLLECTIONS, CRM_MEDIA_IDS_MAX, isContactLifecycleStage, MARKETING_CONSENT_BY_HOST_FIELD, marketingConsentFieldsForGroup, marketingConsentHostIds, normalizeAddress, normalizeContactEmail, normalizeCrmMediaIds, normalizePhone, ORG_SCOPE_TOKEN, planContactCompanyLink, readContactCompanyLink, readContactCustomInput, readContactFacet, soloConsentGroup } from "@aglyn/aglyn/server";
18
+ import { CAPTURED_BY_HOST_FIELD, composeContactName, CONTACT_BIRTHDATE_REFUSAL, CONTACT_COMPANY_IDS_FIELD, CONTACT_EXTRA_PHONE_FIELDS, CONTACT_FACETS_FIELD, CONTACT_FIELDS_MAX_PER_ORG, CONTACT_LIFECYCLE_STAGES, CONTACT_NAME_PART_MAX, consentGroupForHost, contactFacetPath, createResourceUid, CRM_COLLECTIONS, CRM_LEAD_SOURCE_PICKLIST, CRM_MEDIA_IDS_MAX, CRM_SALUTATION_PICKLIST, isContactLifecycleStage, judgeCrmPicklistValue, MARKETING_CONSENT_BY_HOST_FIELD, marketingConsentFieldsForGroup, marketingConsentHostIds, normalizeAddress, normalizeContactBirthdate, normalizeContactEmail, normalizeCrmMediaIds, normalizePhone, ORG_SCOPE_TOKEN, planContactCompanyLink, readContactCompanyLink, readContactCustomInput, readContactFacet, soloConsentGroup } from "@aglyn/aglyn/server";
19
19
  import { ApiErrors, apiJson, contactCompanyMirrorValue, crmRecordsQuotaForOrg, listResponse, restampCrmListFieldsAt, settleCompanyContactsCounts } from "@aglyn/tenant-data-admin";
20
20
  import { claimWrite, orgOwnsHost, paginate, readJsonBody, requireScope, serialize } from "@aglyn/tenant-data-admin/server/api-v1-kit";
21
21
  // The leaf, so a spec that stands a partial barrel in still reaches it.
@@ -28,7 +28,11 @@ import { findContactByEmail } from "@aglyn/tenant-data-admin/server/contact-emai
28
28
  import { deleteWholeContact, removeContactKeepingRefusals } from "@aglyn/tenant-data-admin/server/retained-refusals";
29
29
  import { FieldPath, Timestamp } from "firebase-admin/firestore";
30
30
  import { mergeContactRoute } from "./contacts-merge.js";
31
- import { CRM_ID_MAX, CRM_LABEL_MAX, createPayload, crmRefErrors, memberError, readChoice, readOptionalText, readRefId, updatePayload } from "./crm-shared.js";
31
+ import { sweepDealContactRoles } from "../deal-contact-roles.js";
32
+ import { cachedContactReader, clearReportsToOf, CONTACT_REPORTS_TO_LOOP_REFUSAL, CONTACT_REPORTS_TO_SELF_REFUSAL, contactReportsToLoops } from "../contact-reports-to.js";
33
+ import { readCrmPicklist } from "../read-picklist.js";
34
+ import { CRM_ID_MAX, CRM_LABEL_MAX, createPayload, crmRefErrors, memberError, readChoice, readOptionalText, readOrgLeadSourcePicklist, readRefId, updatePayload } from "./crm-shared.js";
35
+ import { resolveCrmPicklistWrite } from "../read-picklist.js";
32
36
  /**
33
37
  * `/v1/contacts` (AGL-618, AGL-2276, AGL-2606): the organization's people —
34
38
  * list, read, create, update, delete, and `POST /v1/contacts/{id}/merge` —
@@ -76,10 +80,27 @@ import { CRM_ID_MAX, CRM_LABEL_MAX, createPayload, crmRefErrors, memberError, re
76
80
  'address',
77
81
  'ownerUid',
78
82
  'lifecycleStage',
83
+ // The holder's Lead source, one of the org's picklist values (AGL-3511).
84
+ 'leadSource',
79
85
  // Files are a facet field like the rest (AGL-2662): an agency running two
80
86
  // client brands has one contact document between them, and a contract one
81
87
  // client filed is not the other client's to read.
82
- 'mediaIds'
88
+ 'mediaIds',
89
+ // Salesforce's standard contact fields (AGL-3515), per holder like the rest.
90
+ 'salutation',
91
+ 'firstName',
92
+ 'lastName',
93
+ 'department',
94
+ 'mobilePhone',
95
+ 'homePhone',
96
+ 'otherPhone',
97
+ 'fax',
98
+ 'birthdate',
99
+ 'assistantName',
100
+ 'assistantPhone',
101
+ 'reportsToContactId',
102
+ 'otherAddress',
103
+ 'doNotCall'
83
104
  ];
84
105
  /** The groups holding a facet on this row, in stable order. */ function contactFacetHolders(data) {
85
106
  const facets = data[CONTACT_FACETS_FIELD];
@@ -93,9 +114,25 @@ function contactCrmProfile(data, groupId) {
93
114
  address: null,
94
115
  ownerUid: null,
95
116
  lifecycleStage: null,
117
+ leadSource: null,
96
118
  // An empty ARRAY rather than null, so a client can index it without a
97
119
  // guard — the same shape `custom` publishes for the same reason.
98
- mediaIds: []
120
+ mediaIds: [],
121
+ salutation: null,
122
+ firstName: null,
123
+ lastName: null,
124
+ department: null,
125
+ mobilePhone: null,
126
+ homePhone: null,
127
+ otherPhone: null,
128
+ fax: null,
129
+ birthdate: null,
130
+ assistantName: null,
131
+ assistantPhone: null,
132
+ reportsToContactId: null,
133
+ otherAddress: null,
134
+ // A flag reads `false` rather than null: unset IS "may be called".
135
+ doNotCall: false
99
136
  };
100
137
  for (const holder of groupId ? [
101
138
  groupId
@@ -111,6 +148,10 @@ function contactCrmProfile(data, groupId) {
111
148
  if (!held.length) profile[field] = normalizeCrmMediaIds(value);
112
149
  continue;
113
150
  }
151
+ if (field === 'doNotCall') {
152
+ if (value === true) profile[field] = true;
153
+ continue;
154
+ }
114
155
  if (profile[field] === null && value !== undefined && value !== null) {
115
156
  profile[field] = value;
116
157
  }
@@ -303,6 +344,9 @@ function readContactInput(body, { partial }) {
303
344
  }
304
345
  const ownerUid = readOptionalText(body, 'ownerUid', CONTACT_NAME_MAX, errors);
305
346
  if (ownerUid !== undefined) crm.ownerUid = ownerUid;
347
+ // Shape only here; the org's list judges the value in `contactCrmRefErrors`.
348
+ const leadSource = readOptionalText(body, 'leadSource', CRM_LABEL_MAX, errors);
349
+ if (leadSource !== undefined) crm.leadSource = leadSource;
306
350
  if (body.mediaIds !== undefined) {
307
351
  if (!Array.isArray(body.mediaIds)) {
308
352
  errors.mediaIds = 'Must be an array of media ids';
@@ -320,6 +364,60 @@ function readContactInput(body, { partial }) {
320
364
  const stage = readChoice(body, 'lifecycleStage', CONTACT_LIFECYCLE_STAGES, errors);
321
365
  if (stage) crm.lifecycleStage = stage;
322
366
  }
367
+ /*
368
+ * Salesforce's standard contact fields (AGL-3515), through the normalizers
369
+ * the console's profile route uses. The salutation and the reports-to are
370
+ * judged after this pass, against the org's list and its contacts.
371
+ */ const salutation = readOptionalText(body, 'salutation', CRM_LABEL_MAX, errors);
372
+ if (salutation !== undefined) crm.salutation = salutation;
373
+ for (const key of [
374
+ 'firstName',
375
+ 'lastName'
376
+ ]){
377
+ const part = readOptionalText(body, key, CONTACT_NAME_PART_MAX, errors);
378
+ if (part !== undefined) crm[key] = part;
379
+ }
380
+ for (const key of [
381
+ 'department',
382
+ 'assistantName'
383
+ ]){
384
+ const text = readOptionalText(body, key, CONTACT_NAME_MAX, errors);
385
+ if (text !== undefined) crm[key] = text;
386
+ }
387
+ for (const key of CONTACT_EXTRA_PHONE_FIELDS){
388
+ const text = readOptionalText(body, key, CRM_LABEL_MAX, errors);
389
+ if (text === null) {
390
+ crm[key] = null;
391
+ } else if (text !== undefined) {
392
+ const normalized = normalizePhone(text);
393
+ if (normalized) crm[key] = normalized;
394
+ else errors[key] = 'Must be a phone number with a country code, like +15125550123';
395
+ }
396
+ }
397
+ const birthdate = readOptionalText(body, 'birthdate', 32, errors);
398
+ if (birthdate === null) {
399
+ crm.birthdate = null;
400
+ } else if (birthdate !== undefined) {
401
+ const normalized = normalizeContactBirthdate(birthdate);
402
+ if (normalized) crm.birthdate = normalized;
403
+ else errors.birthdate = CONTACT_BIRTHDATE_REFUSAL;
404
+ }
405
+ const reportsTo = readRefId(body, 'reportsToContactId', errors);
406
+ if (reportsTo !== undefined) crm.reportsToContactId = reportsTo;
407
+ if (body.otherAddress !== undefined) {
408
+ if (body.otherAddress === null) {
409
+ crm.otherAddress = null;
410
+ } else if (typeof body.otherAddress !== 'object' || Array.isArray(body.otherAddress)) {
411
+ errors.otherAddress = 'Must be an address object';
412
+ } else {
413
+ crm.otherAddress = normalizeAddress(body.otherAddress);
414
+ }
415
+ }
416
+ if (body.doNotCall !== undefined) {
417
+ if (body.doNotCall === null || body.doNotCall === false) crm.doNotCall = null;
418
+ else if (body.doNotCall === true) crm.doNotCall = true;
419
+ else errors.doNotCall = 'Must be true or false';
420
+ }
323
421
  values.crm = crm;
324
422
  // Whether the body MEANT to write a profile — judged by the keys it sent,
325
423
  // not by which of them parsed, so a body whose every profile value was
@@ -380,15 +478,92 @@ function readContactInput(body, { partial }) {
380
478
  * and `companyId`, so a contact cannot point at a company `/v1/deals` would
381
479
  * refuse. Run after the synchronous grammar, so a body already refused never
382
480
  * spends the reads.
383
- */ async function contactCrmRefErrors(ctx, crm) {
481
+ */ async function contactCrmRefErrors(ctx, crm, at = {
482
+ contactId: null
483
+ }) {
384
484
  var _crm_companyId;
385
- const [owner, refs] = await Promise.all([
485
+ var _facetAt;
486
+ const [owner, refs, leadSource, salutation, reportsTo] = await Promise.all([
386
487
  memberError(ctx, 'ownerUid', crm.ownerUid),
387
488
  crmRefErrors(ctx, {
388
489
  companyId: (_crm_companyId = crm.companyId) != null ? _crm_companyId : undefined
389
- })
490
+ }),
491
+ // The holder's current lead source is kept even when the list no longer offers it.
492
+ contactLeadSourceErrors(ctx, crm, (_facetAt = facetAt(ctx, at)) == null ? void 0 : _facetAt.facet.leadSource),
493
+ contactSalutationError(ctx, crm, at),
494
+ contactReportsToError(ctx, crm, at)
390
495
  ]);
391
- return _extends({}, owner, refs);
496
+ return _extends({}, owner, refs, leadSource, salutation, reportsTo);
497
+ }
498
+ /**
499
+ * The lead source a contact write stores (AGL-3511), judged against the
500
+ * org's Lead source list exactly as a lead's is: an active value stores the
501
+ * list's spelling, the holder's current value is always kept, and anything
502
+ * else is refused naming what the list allows. Rewrites `crm.leadSource` to
503
+ * the label the list spells. A contact takes no default — the list's default
504
+ * is for a new LEAD.
505
+ */ async function contactLeadSourceErrors(ctx, crm, current) {
506
+ var _resolved_write;
507
+ if (typeof crm.leadSource !== 'string') return {};
508
+ const resolved = resolveCrmPicklistWrite(CRM_LEAD_SOURCE_PICKLIST, await readOrgLeadSourcePicklist(ctx), crm.leadSource, {
509
+ current,
510
+ created: false
511
+ });
512
+ if (resolved.ok === false) return {
513
+ leadSource: resolved.error
514
+ };
515
+ crm.leadSource = (_resolved_write = resolved.write) != null ? _resolved_write : null;
516
+ return {};
517
+ }
518
+ /** The facet a write at `at` reads its current values from. */ function facetAt(ctx, at) {
519
+ if (!at.data || !at.siteId) return null;
520
+ const group = consentGroupForHost(ctx.org, at.siteId);
521
+ return {
522
+ groupId: group.groupId,
523
+ facet: readContactFacet(at.data, group.groupId)
524
+ };
525
+ }
526
+ /**
527
+ * The salutation, judged against the org's list (AGL-3515) the way the
528
+ * console's route judges it: an active value stores the list's spelling,
529
+ * the value the contact already holds is kept, anything else is named with
530
+ * the values allowed. Rewrites `crm.salutation` to the stored spelling.
531
+ */ async function contactSalutationError(ctx, crm, at) {
532
+ var _judged_value;
533
+ var _facetAt;
534
+ if (!crm.salutation) return {};
535
+ const judged = judgeCrmPicklistValue(CRM_SALUTATION_PICKLIST, await readCrmPicklist(ctx.firestore, ctx.orgId, CRM_SALUTATION_PICKLIST), crm.salutation, (_facetAt = facetAt(ctx, at)) == null ? void 0 : _facetAt.facet.salutation);
536
+ if (judged.ok === false) return {
537
+ salutation: judged.error
538
+ };
539
+ crm.salutation = (_judged_value = judged.value) != null ? _judged_value : null;
540
+ return {};
541
+ }
542
+ /**
543
+ * The reports-to (AGL-3515): a contact of this organization, never the
544
+ * contact itself, and never one whose own chain — through the named site's
545
+ * facets — leads back to it.
546
+ */ async function contactReportsToError(ctx, crm, at) {
547
+ const managerId = crm.reportsToContactId;
548
+ if (!managerId) return {};
549
+ if (managerId === at.contactId) {
550
+ return {
551
+ reportsToContactId: CONTACT_REPORTS_TO_SELF_REFUSAL
552
+ };
553
+ }
554
+ const read = cachedContactReader(contactsCollection(ctx));
555
+ if (!await read(managerId)) {
556
+ return {
557
+ reportsToContactId: 'No such contact in this organization'
558
+ };
559
+ }
560
+ const facet = facetAt(ctx, at);
561
+ if (at.contactId && facet && await contactReportsToLoops(read, at.contactId, managerId, facet.groupId)) {
562
+ return {
563
+ reportsToContactId: CONTACT_REPORTS_TO_LOOP_REFUSAL
564
+ };
565
+ }
566
+ return {};
392
567
  }
393
568
  /**
394
569
  * The facet a create writes when the body carried a CRM profile field, under
@@ -402,6 +577,9 @@ function readContactInput(body, { partial }) {
402
577
  */ function contactCrmCreateFields(ctx, crm, siteId) {
403
578
  const stored = createPayload(crm);
404
579
  if (Object.keys(stored).length === 0) return {};
580
+ // The holder's name is the first and last names' when either was given.
581
+ const composed = composeContactName(crm.firstName, crm.lastName);
582
+ if (composed) stored['name'] = composed;
405
583
  const group = consentGroupForHost(ctx.org, siteId);
406
584
  return _extends({
407
585
  [CONTACT_FACETS_FIELD]: {
@@ -439,6 +617,15 @@ function readContactInput(body, { partial }) {
439
617
  for (const [field, value] of Object.entries(updatePayload(crm))){
440
618
  update[contactFacetPath(group.groupId, field)] = value;
441
619
  }
620
+ /*
621
+ * The holder's name follows the first and last names (AGL-3515): either
622
+ * sent recomposes it from what the facet will hold, and both cleared
623
+ * leave it as it stands.
624
+ */ if (crm.firstName !== undefined || crm.lastName !== undefined) {
625
+ const facet = readContactFacet(data, group.groupId);
626
+ const composed = composeContactName(crm.firstName === undefined ? facet.firstName : crm.firstName, crm.lastName === undefined ? facet.lastName : crm.lastName);
627
+ if (composed) update[contactFacetPath(group.groupId, 'name')] = composed;
628
+ }
442
629
  let link = null;
443
630
  if (crm.companyId !== undefined) {
444
631
  link = planContactCompanyLink(readContactCompanyLink(data, group.groupId), crm.companyId);
@@ -586,6 +773,8 @@ const contactsCollection = (ctx)=>ctx.firestore.collection('orgs').doc(ctx.orgId
586
773
  headers: ctx.headers
587
774
  });
588
775
  }
776
+ // The canonical name, or the first and last names' for a body with only those.
777
+ const canonicalName = name || composeContactName(crm.firstName, crm.lastName);
589
778
  const collection = contactsCollection(ctx);
590
779
  const claimed = await claimWrite(ctx, '*', request.headers.get('Idempotency-Key'), 'contacts');
591
780
  if ('replay' in claimed) return claimed.replay;
@@ -625,7 +814,7 @@ const contactsCollection = (ctx)=>ctx.firestore.collection('orgs').doc(ctx.orgId
625
814
  const id = createResourceUid();
626
815
  await collection.doc(id).create(_extends({
627
816
  email
628
- }, name ? nameSearchFields(name) : {}, {
817
+ }, canonicalName ? nameSearchFields(canonicalName) : {}, {
629
818
  tags: tags != null ? tags : []
630
819
  }, notes ? {
631
820
  notes
@@ -695,6 +884,7 @@ const contactsCollection = (ctx)=>ctx.firestore.collection('orgs').doc(ctx.orgId
695
884
  * does not grow the audience, and charging a plan refusal for renaming
696
885
  * somebody would make a downgraded org unable to correct its own data.
697
886
  */ async function updateContact(request, ctx, contactRef) {
887
+ var _snap_data;
698
888
  const parsed = readContactInput(await readJsonBody(request), {
699
889
  partial: true
700
890
  });
@@ -724,7 +914,11 @@ const contactsCollection = (ctx)=>ctx.firestore.collection('orgs').doc(ctx.orgId
724
914
  headers: ctx.headers
725
915
  });
726
916
  }
727
- const crmErrors = await contactCrmRefErrors(ctx, crm);
917
+ const crmErrors = await contactCrmRefErrors(ctx, crm, {
918
+ contactId: contactRef.id,
919
+ siteId: consentSiteId,
920
+ data: (_snap_data = snap.data()) != null ? _snap_data : {}
921
+ });
728
922
  if (Object.keys(crmErrors).length) {
729
923
  return ApiErrors.badRequest({
730
924
  message: 'Contact failed validation',
@@ -788,8 +982,8 @@ const contactsCollection = (ctx)=>ctx.firestore.collection('orgs').doc(ctx.orgId
788
982
  }
789
983
  let link = null;
790
984
  if (consentSiteId) {
791
- var _snap_data;
792
- const crmFields = contactCrmUpdateFields(ctx, (_snap_data = snap.data()) != null ? _snap_data : {}, crm, consentSiteId);
985
+ var _snap_data1;
986
+ const crmFields = contactCrmUpdateFields(ctx, (_snap_data1 = snap.data()) != null ? _snap_data1 : {}, crm, consentSiteId);
793
987
  Object.assign(update, crmFields.update);
794
988
  link = crmFields.link;
795
989
  }
@@ -820,7 +1014,10 @@ const contactsCollection = (ctx)=>ctx.firestore.collection('orgs').doc(ctx.orgId
820
1014
  * Deletes the document whoever else holds it, and keeps every refusal it
821
1015
  * held — each site's and the unscoped one — in the organization's retained
822
1016
  * store in the same transaction (AGL-3338), so deleting a person over the
823
- * API is not a way to add somebody who said no to a list.
1017
+ * API is not a way to add somebody who said no to a list. Then what pointed
1018
+ * at the person goes: every other contact's reports-to naming them, in
1019
+ * every holder's facet (AGL-3537), and their contact roles on deals
1020
+ * (AGL-3521).
824
1021
  *
825
1022
  * Takes an `Idempotency-Key` with `deleteRecord`'s exact semantics, and for a
826
1023
  * sharper reason: an erasure request is the operation most likely to be run
@@ -831,9 +1028,14 @@ const contactsCollection = (ctx)=>ctx.firestore.collection('orgs').doc(ctx.orgId
831
1028
  if ('replay' in claimed) return claimed.replay;
832
1029
  const { claim } = claimed;
833
1030
  try {
1031
+ // The holders whose facets may name the person as a reports-to (AGL-3537).
1032
+ let holders = [];
834
1033
  const removed = await removeContactKeepingRefusals({
835
1034
  contactRef,
836
- decide: deleteWholeContact,
1035
+ decide: (contact)=>{
1036
+ holders = contactFacetHolders(contact);
1037
+ return deleteWholeContact(contact);
1038
+ },
837
1039
  nowMs: Date.now()
838
1040
  });
839
1041
  if (removed.outcome === 'missing') {
@@ -843,6 +1045,10 @@ const contactsCollection = (ctx)=>ctx.firestore.collection('orgs').doc(ctx.orgId
843
1045
  headers: ctx.headers
844
1046
  });
845
1047
  }
1048
+ // Nobody reports to a deleted person (AGL-3537)…
1049
+ await clearReportsToOf(ctx.firestore, ctx.firestore.collection('orgs').doc(ctx.orgId).collection('contacts'), holders, contactRef.id);
1050
+ // …and they leave every deal's contact roles (AGL-3521).
1051
+ await sweepDealContactRoles(ctx.firestore, ctx.firestore.collection('orgs').doc(ctx.orgId).collection(CRM_COLLECTIONS.deals), contactRef.id, null, '[api-v1] contacts delete');
846
1052
  const view = {
847
1053
  id: contactRef.id,
848
1054
  object: 'contact',