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

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 (541) 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-next-activity-column.js +1 -1
  34. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  35. package/src/lib/components/crm-record-picker.d.ts +15 -0
  36. package/src/lib/components/crm-record-picker.js +16 -10
  37. package/src/lib/components/crm-record-picker.js.map +1 -1
  38. package/src/lib/components/crm-transfer-buttons.d.ts +54 -0
  39. package/src/lib/components/crm-transfer-buttons.js +167 -0
  40. package/src/lib/components/crm-transfer-buttons.js.map +1 -0
  41. package/src/lib/components/deal-contact-roles-card.d.ts +46 -0
  42. package/src/lib/components/deal-contact-roles-card.js +368 -0
  43. package/src/lib/components/deal-contact-roles-card.js.map +1 -0
  44. package/src/lib/components/deal-detail-page.d.ts +3 -2
  45. package/src/lib/components/deal-detail-page.js +25 -6
  46. package/src/lib/components/deal-detail-page.js.map +1 -1
  47. package/src/lib/components/deal-edit-drawer.d.ts +10 -0
  48. package/src/lib/components/deal-edit-drawer.js +219 -10
  49. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  50. package/src/lib/components/deal-properties-card.d.ts +7 -0
  51. package/src/lib/components/deal-properties-card.js +34 -3
  52. package/src/lib/components/deal-properties-card.js.map +1 -1
  53. package/src/lib/components/deal-stage-card.d.ts +4 -0
  54. package/src/lib/components/deal-stage-card.js +10 -2
  55. package/src/lib/components/deal-stage-card.js.map +1 -1
  56. package/src/lib/components/deals-bulk-bar.d.ts +0 -3
  57. package/src/lib/components/deals-bulk-bar.js +7 -20
  58. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  59. package/src/lib/components/deals-section.d.ts +3 -3
  60. package/src/lib/components/deals-section.js +123 -63
  61. package/src/lib/components/deals-section.js.map +1 -1
  62. package/src/lib/components/fields-section.js +31 -17
  63. package/src/lib/components/fields-section.js.map +1 -1
  64. package/src/lib/components/lead-convert-dialog.d.ts +4 -0
  65. package/src/lib/components/lead-convert-dialog.js +23 -1
  66. package/src/lib/components/lead-convert-dialog.js.map +1 -1
  67. package/src/lib/components/lead-detail-page.js +8 -2
  68. package/src/lib/components/lead-detail-page.js.map +1 -1
  69. package/src/lib/components/lead-profile-fields.d.ts +71 -0
  70. package/src/lib/components/lead-profile-fields.js +386 -0
  71. package/src/lib/components/lead-profile-fields.js.map +1 -0
  72. package/src/lib/components/lead-properties-card.d.ts +2 -1
  73. package/src/lib/components/lead-properties-card.js +85 -176
  74. package/src/lib/components/lead-properties-card.js.map +1 -1
  75. package/src/lib/components/lead-source-select.d.ts +4 -23
  76. package/src/lib/components/lead-source-select.js +9 -73
  77. package/src/lib/components/lead-source-select.js.map +1 -1
  78. package/src/lib/components/lead-status-chip.d.ts +9 -3
  79. package/src/lib/components/lead-status-chip.js +8 -4
  80. package/src/lib/components/lead-status-chip.js.map +1 -1
  81. package/src/lib/components/lead-status-options.d.ts +20 -0
  82. package/src/lib/components/lead-status-options.js +51 -0
  83. package/src/lib/components/lead-status-options.js.map +1 -0
  84. package/src/lib/components/lead-unqualify-dialog.d.ts +5 -0
  85. package/src/lib/components/lead-unqualify-dialog.js +50 -18
  86. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  87. package/src/lib/components/leads-bulk-bar.d.ts +0 -3
  88. package/src/lib/components/leads-bulk-bar.js +29 -29
  89. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  90. package/src/lib/components/leads-section.js +120 -68
  91. package/src/lib/components/leads-section.js.map +1 -1
  92. package/src/lib/components/linked-deals-card.d.ts +4 -0
  93. package/src/lib/components/linked-deals-card.js +47 -3
  94. package/src/lib/components/linked-deals-card.js.map +1 -1
  95. package/src/lib/components/log-activity-dialog.js +43 -3
  96. package/src/lib/components/log-activity-dialog.js.map +1 -1
  97. package/src/lib/components/new-contact-drawer.d.ts +8 -0
  98. package/src/lib/components/new-contact-drawer.js +108 -6
  99. package/src/lib/components/new-contact-drawer.js.map +1 -1
  100. package/src/lib/components/new-lead-drawer.d.ts +9 -2
  101. package/src/lib/components/new-lead-drawer.js +88 -121
  102. package/src/lib/components/new-lead-drawer.js.map +1 -1
  103. package/src/lib/components/picklist-select.d.ts +36 -0
  104. package/src/lib/components/picklist-select.js +114 -0
  105. package/src/lib/components/picklist-select.js.map +1 -0
  106. package/src/lib/components/picklist-values-card.d.ts +34 -0
  107. package/src/lib/components/{lead-source-values-card.js → picklist-values-card.js} +241 -58
  108. package/src/lib/components/picklist-values-card.js.map +1 -0
  109. package/src/lib/components/pipeline-stages-dialog.d.ts +7 -0
  110. package/src/lib/components/pipeline-stages-dialog.js +26 -4
  111. package/src/lib/components/pipeline-stages-dialog.js.map +1 -1
  112. package/src/lib/components/pipelines-dialog.js +15 -6
  113. package/src/lib/components/pipelines-dialog.js.map +1 -1
  114. package/src/lib/components/recent-activity-feed.js +20 -5
  115. package/src/lib/components/recent-activity-feed.js.map +1 -1
  116. package/src/lib/components/recipes-card.js +4 -2
  117. package/src/lib/components/recipes-card.js.map +1 -1
  118. package/src/lib/components/record-tasks-card.js +15 -8
  119. package/src/lib/components/record-tasks-card.js.map +1 -1
  120. package/src/lib/components/reports/forecast-card.d.ts +6 -0
  121. package/src/lib/components/reports/forecast-card.js +101 -3
  122. package/src/lib/components/reports/forecast-card.js.map +1 -1
  123. package/src/lib/components/reports/lead-funnel-card.d.ts +12 -0
  124. package/src/lib/components/reports/lead-funnel-card.js +9 -5
  125. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  126. package/src/lib/components/reports/lead-source-card.d.ts +23 -0
  127. package/src/lib/components/reports/lead-source-card.js +318 -0
  128. package/src/lib/components/reports/lead-source-card.js.map +1 -0
  129. package/src/lib/components/reports/pipeline-card.js +2 -1
  130. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  131. package/src/lib/components/reports/report-export.js +1 -1
  132. package/src/lib/components/reports/report-export.js.map +1 -1
  133. package/src/lib/components/reports-section.js +8 -0
  134. package/src/lib/components/reports-section.js.map +1 -1
  135. package/src/lib/components/sharing-rule-drawer.d.ts +5 -0
  136. package/src/lib/components/sharing-rule-drawer.js +108 -19
  137. package/src/lib/components/sharing-rule-drawer.js.map +1 -1
  138. package/src/lib/components/sharing-rules-card.js +11 -3
  139. package/src/lib/components/sharing-rules-card.js.map +1 -1
  140. package/src/lib/components/task-cells.d.ts +9 -1
  141. package/src/lib/components/task-cells.js +9 -4
  142. package/src/lib/components/task-cells.js.map +1 -1
  143. package/src/lib/components/task-edit-drawer.js +111 -43
  144. package/src/lib/components/task-edit-drawer.js.map +1 -1
  145. package/src/lib/components/tasks-bulk-bar.d.ts +0 -3
  146. package/src/lib/components/tasks-bulk-bar.js +7 -20
  147. package/src/lib/components/tasks-bulk-bar.js.map +1 -1
  148. package/src/lib/components/tasks-calendar.d.ts +3 -0
  149. package/src/lib/components/tasks-calendar.js +8 -1
  150. package/src/lib/components/tasks-calendar.js.map +1 -1
  151. package/src/lib/components/tasks-section.d.ts +3 -3
  152. package/src/lib/components/tasks-section.js +58 -43
  153. package/src/lib/components/tasks-section.js.map +1 -1
  154. package/src/lib/constants/api-routes.d.ts +2 -9
  155. package/src/lib/constants/api-routes.js +1 -7
  156. package/src/lib/constants/api-routes.js.map +1 -1
  157. package/src/lib/constants/bundle-common.d.ts +6 -0
  158. package/src/lib/constants/bundle-common.js +11 -0
  159. package/src/lib/constants/bundle-common.js.map +1 -1
  160. package/src/lib/constants/company-filters.d.ts +14 -0
  161. package/src/lib/constants/company-filters.js +51 -1
  162. package/src/lib/constants/company-filters.js.map +1 -1
  163. package/src/lib/constants/contact-filters.d.ts +2 -0
  164. package/src/lib/constants/contact-filters.js +18 -1
  165. package/src/lib/constants/contact-filters.js.map +1 -1
  166. package/src/lib/constants/deal-filters.d.ts +22 -1
  167. package/src/lib/constants/deal-filters.js +67 -4
  168. package/src/lib/constants/deal-filters.js.map +1 -1
  169. package/src/lib/declarations.console-server.d.ts +3 -0
  170. package/src/lib/declarations.console-server.js +35 -0
  171. package/src/lib/declarations.console-server.js.map +1 -1
  172. package/src/lib/declarations.d.ts +26 -0
  173. package/src/lib/declarations.js +35 -0
  174. package/src/lib/declarations.js.map +1 -0
  175. package/src/lib/declarations.server.d.ts +11 -0
  176. package/src/lib/declarations.server.js +97 -1
  177. package/src/lib/declarations.server.js.map +1 -1
  178. package/src/lib/hooks/use-company-picklists.d.ts +15 -0
  179. package/src/lib/hooks/use-company-picklists.js +55 -0
  180. package/src/lib/hooks/use-company-picklists.js.map +1 -0
  181. package/src/lib/hooks/use-crm-picklist.d.ts +25 -0
  182. package/src/lib/hooks/use-crm-picklist.js +53 -0
  183. package/src/lib/hooks/use-crm-picklist.js.map +1 -0
  184. package/src/lib/hooks/use-crm-record-names.d.ts +16 -0
  185. package/src/lib/hooks/use-crm-record-names.js +45 -19
  186. package/src/lib/hooks/use-crm-record-names.js.map +1 -1
  187. package/src/lib/hooks/use-crm-task-picklists.d.ts +11 -0
  188. package/src/lib/hooks/use-crm-task-picklists.js +45 -0
  189. package/src/lib/hooks/use-crm-task-picklists.js.map +1 -0
  190. package/src/lib/hooks/use-deals.d.ts +14 -0
  191. package/src/lib/hooks/use-deals.js +29 -0
  192. package/src/lib/hooks/use-deals.js.map +1 -1
  193. package/src/lib/hooks/use-lead-picklists.d.ts +14 -0
  194. package/src/lib/hooks/use-lead-picklists.js +46 -0
  195. package/src/lib/hooks/use-lead-picklists.js.map +1 -0
  196. package/src/lib/hooks/use-lead-source-picklist.d.ts +5 -19
  197. package/src/lib/hooks/use-lead-source-picklist.js +12 -22
  198. package/src/lib/hooks/use-lead-source-picklist.js.map +1 -1
  199. package/src/lib/hooks/use-lead-status-picklist.d.ts +9 -0
  200. package/src/lib/hooks/use-lead-status-picklist.js +40 -0
  201. package/src/lib/hooks/use-lead-status-picklist.js.map +1 -0
  202. package/src/lib/model/companies.d.ts +32 -3
  203. package/src/lib/model/companies.js +133 -16
  204. package/src/lib/model/companies.js.map +1 -1
  205. package/src/lib/model/contact-merge.d.ts +27 -3
  206. package/src/lib/model/contact-merge.js +81 -7
  207. package/src/lib/model/contact-merge.js.map +1 -1
  208. package/src/lib/model/contact-record.d.ts +17 -0
  209. package/src/lib/model/contact-record.js +17 -2
  210. package/src/lib/model/contact-record.js.map +1 -1
  211. package/src/lib/model/contact-update.d.ts +31 -2
  212. package/src/lib/model/contact-update.js +14 -0
  213. package/src/lib/model/contact-update.js.map +1 -1
  214. package/src/lib/model/crm-list-query.d.ts +5 -0
  215. package/src/lib/model/crm-list-query.js +8 -0
  216. package/src/lib/model/crm-list-query.js.map +1 -1
  217. package/src/lib/model/crm-next-activity.d.ts +107 -0
  218. package/src/lib/model/crm-next-activity.js +140 -0
  219. package/src/lib/model/crm-next-activity.js.map +1 -0
  220. package/src/lib/model/crm-recipes.d.ts +40 -0
  221. package/src/lib/model/crm-recipes.js +259 -0
  222. package/src/lib/model/crm-recipes.js.map +1 -0
  223. package/src/lib/model/crm-record-counts.d.ts +36 -0
  224. package/src/lib/model/crm-record-counts.js +54 -0
  225. package/src/lib/model/crm-record-counts.js.map +1 -0
  226. package/src/lib/model/crm-record-lists.d.ts +23 -0
  227. package/src/lib/model/crm-record-lists.js +161 -0
  228. package/src/lib/model/crm-record-lists.js.map +1 -0
  229. package/src/lib/model/crm-reports.d.ts +73 -3
  230. package/src/lib/model/crm-reports.js +120 -1
  231. package/src/lib/model/crm-reports.js.map +1 -1
  232. package/src/lib/model/crm-sharing.d.ts +21 -3
  233. package/src/lib/model/crm-sharing.js +27 -6
  234. package/src/lib/model/crm-sharing.js.map +1 -1
  235. package/src/lib/model/crm-task-reminders.d.ts +152 -0
  236. package/src/lib/model/crm-task-reminders.js +161 -0
  237. package/src/lib/model/crm-task-reminders.js.map +1 -0
  238. package/src/lib/model/deal-board-model.d.ts +8 -2
  239. package/src/lib/model/deal-board-model.js +15 -2
  240. package/src/lib/model/deal-board-model.js.map +1 -1
  241. package/src/lib/model/deal-contact-roles.d.ts +45 -0
  242. package/src/lib/model/deal-contact-roles.js +73 -0
  243. package/src/lib/model/deal-contact-roles.js.map +1 -0
  244. package/src/lib/model/deal-form-model.d.ts +56 -0
  245. package/src/lib/model/deal-form-model.js +79 -7
  246. package/src/lib/model/deal-form-model.js.map +1 -1
  247. package/src/lib/model/download-text-file.d.ts +23 -0
  248. package/src/lib/model/download-text-file.js +34 -0
  249. package/src/lib/model/download-text-file.js.map +1 -0
  250. package/src/lib/model/lead-filters.d.ts +36 -2
  251. package/src/lib/model/lead-filters.js +123 -2
  252. package/src/lib/model/lead-filters.js.map +1 -1
  253. package/src/lib/model/lead-source-direction.d.ts +38 -0
  254. package/src/lib/model/lead-source-direction.js +52 -0
  255. package/src/lib/model/lead-source-direction.js.map +1 -0
  256. package/src/lib/model/next-activity.d.ts +1 -1
  257. package/src/lib/model/next-activity.js.map +1 -1
  258. package/src/lib/model/picklist-values.d.ts +119 -0
  259. package/src/lib/model/picklist-values.js +256 -0
  260. package/src/lib/model/picklist-values.js.map +1 -0
  261. package/src/lib/model/record-facts-disclosed.d.ts +81 -0
  262. package/src/lib/model/record-facts-disclosed.js +281 -0
  263. package/src/lib/model/record-facts-disclosed.js.map +1 -0
  264. package/src/lib/model/record-facts.d.ts +215 -34
  265. package/src/lib/model/record-facts.js +307 -128
  266. package/src/lib/model/record-facts.js.map +1 -1
  267. package/src/lib/model/task-picklists.d.ts +52 -0
  268. package/src/lib/model/task-picklists.js +83 -0
  269. package/src/lib/model/task-picklists.js.map +1 -0
  270. package/src/lib/model/task-routes.d.ts +13 -0
  271. package/src/lib/model/task-routes.js +23 -4
  272. package/src/lib/model/task-routes.js.map +1 -1
  273. package/src/lib/plan-entitlements.js +6 -4
  274. package/src/lib/plan-entitlements.js.map +1 -1
  275. package/src/lib/plugin.js +17 -0
  276. package/src/lib/plugin.js.map +1 -1
  277. package/src/lib/server/api-v1/contacts.js +220 -14
  278. package/src/lib/server/api-v1/contacts.js.map +1 -1
  279. package/src/lib/server/api-v1/crm-activities.js +14 -1
  280. package/src/lib/server/api-v1/crm-activities.js.map +1 -1
  281. package/src/lib/server/api-v1/crm-companies.js +123 -14
  282. package/src/lib/server/api-v1/crm-companies.js.map +1 -1
  283. package/src/lib/server/api-v1/crm-deals.js +291 -26
  284. package/src/lib/server/api-v1/crm-deals.js.map +1 -1
  285. package/src/lib/server/api-v1/crm-leads.js +144 -38
  286. package/src/lib/server/api-v1/crm-leads.js.map +1 -1
  287. package/src/lib/server/api-v1/crm-pipelines.d.ts +6 -0
  288. package/src/lib/server/api-v1/crm-pipelines.js +8 -2
  289. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -1
  290. package/src/lib/server/api-v1/crm-tasks.js +78 -34
  291. package/src/lib/server/api-v1/crm-tasks.js.map +1 -1
  292. package/src/lib/server/api-v1/openapi.d.ts +0 -11
  293. package/src/lib/server/api-v1/openapi.js +171 -16
  294. package/src/lib/server/api-v1/openapi.js.map +1 -1
  295. package/src/lib/server/assign-contact-owner.d.ts +188 -0
  296. package/src/lib/server/assign-contact-owner.js +446 -0
  297. package/src/lib/server/assign-contact-owner.js.map +1 -0
  298. package/src/lib/server/associate-company-by-domain.d.ts +70 -0
  299. package/src/lib/server/associate-company-by-domain.js +153 -0
  300. package/src/lib/server/associate-company-by-domain.js.map +1 -0
  301. package/src/lib/server/automation-steps.d.ts +140 -0
  302. package/src/lib/server/automation-steps.js +669 -0
  303. package/src/lib/server/automation-steps.js.map +1 -0
  304. package/src/lib/server/capture-contact.d.ts +17 -0
  305. package/src/lib/server/capture-contact.js +95 -26
  306. package/src/lib/server/capture-contact.js.map +1 -1
  307. package/src/lib/server/capture-host-contact.d.ts +104 -0
  308. package/src/lib/server/capture-host-contact.js +151 -0
  309. package/src/lib/server/capture-host-contact.js.map +1 -0
  310. package/src/lib/server/company-children.d.ts +25 -0
  311. package/src/lib/server/company-children.js +42 -0
  312. package/src/lib/server/company-children.js.map +1 -0
  313. package/src/lib/server/company-delete.d.ts +18 -0
  314. package/src/lib/server/company-delete.js +44 -21
  315. package/src/lib/server/company-delete.js.map +1 -1
  316. package/src/lib/server/contact-email-history.js +21 -19
  317. package/src/lib/server/contact-email-history.js.map +1 -1
  318. package/src/lib/server/contact-remove.js +27 -2
  319. package/src/lib/server/contact-remove.js.map +1 -1
  320. package/src/lib/server/contact-reports-to.d.ts +61 -0
  321. package/src/lib/server/contact-reports-to.js +114 -0
  322. package/src/lib/server/contact-reports-to.js.map +1 -0
  323. package/src/lib/server/contact-update.d.ts +42 -1
  324. package/src/lib/server/contact-update.js +204 -23
  325. package/src/lib/server/contact-update.js.map +1 -1
  326. package/src/lib/server/convert-host-lead.d.ts +46 -1
  327. package/src/lib/server/convert-host-lead.js +173 -15
  328. package/src/lib/server/convert-host-lead.js.map +1 -1
  329. package/src/lib/server/convert-open-lead.js +12 -5
  330. package/src/lib/server/convert-open-lead.js.map +1 -1
  331. package/src/lib/server/crm-email-activity.d.ts +118 -0
  332. package/src/lib/server/crm-email-activity.js +148 -0
  333. package/src/lib/server/crm-email-activity.js.map +1 -0
  334. package/src/lib/server/crm-inbound-email.js +1 -1
  335. package/src/lib/server/crm-inbound-email.js.map +1 -1
  336. package/src/lib/server/crm-next-activity.d.ts +74 -0
  337. package/src/lib/server/crm-next-activity.js +194 -0
  338. package/src/lib/server/crm-next-activity.js.map +1 -0
  339. package/src/lib/server/crm-record-indexes.d.ts +45 -0
  340. package/src/lib/server/crm-record-indexes.js +78 -0
  341. package/src/lib/server/crm-record-indexes.js.map +1 -0
  342. package/src/lib/server/crm-sharing.d.ts +9 -1
  343. package/src/lib/server/crm-sharing.js +23 -3
  344. package/src/lib/server/crm-sharing.js.map +1 -1
  345. package/src/lib/server/deal-contact-roles.d.ts +27 -0
  346. package/src/lib/server/deal-contact-roles.js +89 -0
  347. package/src/lib/server/deal-contact-roles.js.map +1 -0
  348. package/src/lib/server/email-send.js +2 -1
  349. package/src/lib/server/email-send.js.map +1 -1
  350. package/src/lib/server/lead-convert.d.ts +5 -0
  351. package/src/lib/server/lead-convert.js +19 -3
  352. package/src/lib/server/lead-convert.js.map +1 -1
  353. package/src/lib/server/lead-create.d.ts +17 -2
  354. package/src/lib/server/lead-create.js +46 -14
  355. package/src/lib/server/lead-create.js.map +1 -1
  356. package/src/lib/server/lead-nurturing.d.ts +6 -0
  357. package/src/lib/server/lead-nurturing.js +57 -20
  358. package/src/lib/server/lead-nurturing.js.map +1 -1
  359. package/src/lib/server/lead-picklists.d.ts +35 -0
  360. package/src/lib/server/lead-picklists.js +52 -0
  361. package/src/lib/server/lead-picklists.js.map +1 -0
  362. package/src/lib/server/lead-source-picklist.d.ts +1 -13
  363. package/src/lib/server/lead-source-picklist.js +6 -33
  364. package/src/lib/server/lead-source-picklist.js.map +1 -1
  365. package/src/lib/server/merge-contacts.js +23 -5
  366. package/src/lib/server/merge-contacts.js.map +1 -1
  367. package/src/lib/server/next-activity-routes.js +2 -1
  368. package/src/lib/server/next-activity-routes.js.map +1 -1
  369. package/src/lib/server/person-eraser.d.ts +53 -0
  370. package/src/lib/server/person-eraser.js +149 -0
  371. package/src/lib/server/person-eraser.js.map +1 -0
  372. package/src/lib/server/person-records.d.ts +69 -0
  373. package/src/lib/server/person-records.js +317 -0
  374. package/src/lib/server/person-records.js.map +1 -0
  375. package/src/lib/server/person-refund.d.ts +97 -0
  376. package/src/lib/server/person-refund.js +221 -0
  377. package/src/lib/server/person-refund.js.map +1 -0
  378. package/src/lib/server/picklist-values.d.ts +21 -0
  379. package/src/lib/server/picklist-values.js +308 -0
  380. package/src/lib/server/picklist-values.js.map +1 -0
  381. package/src/lib/server/read-picklist.d.ts +44 -0
  382. package/src/lib/server/read-picklist.js +65 -0
  383. package/src/lib/server/read-picklist.js.map +1 -0
  384. package/src/lib/server/recipe-routes.js +22 -8
  385. package/src/lib/server/recipe-routes.js.map +1 -1
  386. package/src/lib/server/record-email-state.js +7 -1
  387. package/src/lib/server/record-email-state.js.map +1 -1
  388. package/src/lib/server/record-facts.d.ts +3 -0
  389. package/src/lib/server/record-facts.js +233 -31
  390. package/src/lib/server/record-facts.js.map +1 -1
  391. package/src/lib/server/record-origin.d.ts +27 -0
  392. package/src/lib/server/record-origin.js +104 -0
  393. package/src/lib/server/record-origin.js.map +1 -0
  394. package/src/lib/server/record-timeline.js +40 -3
  395. package/src/lib/server/record-timeline.js.map +1 -1
  396. package/src/lib/server/suite-gate.js +4 -3
  397. package/src/lib/server/suite-gate.js.map +1 -1
  398. package/src/lib/server/task-reminders-route.js +2 -1
  399. package/src/lib/server/task-reminders-route.js.map +1 -1
  400. package/src/lib/server/task-routes.js +33 -17
  401. package/src/lib/server/task-routes.js.map +1 -1
  402. package/src/lib/server-deal-stage.d.ts +5 -1
  403. package/src/lib/server-deal-stage.js +10 -5
  404. package/src/lib/server-deal-stage.js.map +1 -1
  405. package/src/lib/server.d.ts +8 -3
  406. package/src/lib/server.js +47 -43
  407. package/src/lib/server.js.map +1 -1
  408. package/src/lib/testing/memory-firestore.d.ts +55 -0
  409. package/src/lib/testing/memory-firestore.js +335 -0
  410. package/src/lib/testing/memory-firestore.js.map +1 -0
  411. package/src/lib/testing/stand-in-email-send-index.d.ts +25 -0
  412. package/src/lib/testing/stand-in-email-send-index.js +49 -0
  413. package/src/lib/testing/stand-in-email-send-index.js.map +1 -0
  414. package/src/lib/transfer/activities.d.ts +30 -0
  415. package/src/lib/transfer/activities.js +132 -0
  416. package/src/lib/transfer/activities.js.map +1 -0
  417. package/src/lib/transfer/aliases.d.ts +22 -0
  418. package/src/lib/transfer/aliases.js +1342 -0
  419. package/src/lib/transfer/aliases.js.map +1 -0
  420. package/src/lib/transfer/common.d.ts +222 -0
  421. package/src/lib/transfer/common.js +832 -0
  422. package/src/lib/transfer/common.js.map +1 -0
  423. package/src/lib/transfer/companies.d.ts +43 -0
  424. package/src/lib/transfer/companies.js +549 -0
  425. package/src/lib/transfer/companies.js.map +1 -0
  426. package/src/lib/transfer/contacts.d.ts +31 -0
  427. package/src/lib/transfer/contacts.js +727 -0
  428. package/src/lib/transfer/contacts.js.map +1 -0
  429. package/src/lib/transfer/deals.d.ts +66 -0
  430. package/src/lib/transfer/deals.js +698 -0
  431. package/src/lib/transfer/deals.js.map +1 -0
  432. package/src/lib/transfer/email-templates-package.d.ts +40 -0
  433. package/src/lib/transfer/email-templates-package.js +76 -0
  434. package/src/lib/transfer/email-templates-package.js.map +1 -0
  435. package/src/lib/transfer/email-templates-package.server.d.ts +32 -0
  436. package/src/lib/transfer/email-templates-package.server.js +176 -0
  437. package/src/lib/transfer/email-templates-package.server.js.map +1 -0
  438. package/src/lib/transfer/fields.d.ts +102 -0
  439. package/src/lib/transfer/fields.js +1755 -0
  440. package/src/lib/transfer/fields.js.map +1 -0
  441. package/src/lib/transfer/leads.d.ts +30 -0
  442. package/src/lib/transfer/leads.js +582 -0
  443. package/src/lib/transfer/leads.js.map +1 -0
  444. package/src/lib/transfer/pipelines.d.ts +21 -0
  445. package/src/lib/transfer/pipelines.js +172 -0
  446. package/src/lib/transfer/pipelines.js.map +1 -0
  447. package/src/lib/transfer/register.d.ts +47 -0
  448. package/src/lib/transfer/register.js +277 -0
  449. package/src/lib/transfer/register.js.map +1 -0
  450. package/src/lib/transfer/tasks.d.ts +30 -0
  451. package/src/lib/transfer/tasks.js +418 -0
  452. package/src/lib/transfer/tasks.js.map +1 -0
  453. package/src/lib/transfer/ui.d.ts +20 -0
  454. package/src/lib/transfer/ui.js +42 -0
  455. package/src/lib/transfer/ui.js.map +1 -0
  456. package/src/lib/components/company-import-drawer.d.ts +0 -42
  457. package/src/lib/components/company-import-drawer.js +0 -99
  458. package/src/lib/components/company-import-drawer.js.map +0 -1
  459. package/src/lib/components/contact-import-drawer.d.ts +0 -46
  460. package/src/lib/components/contact-import-drawer.js +0 -149
  461. package/src/lib/components/contact-import-drawer.js.map +0 -1
  462. package/src/lib/components/crm-export-all-button.d.ts +0 -39
  463. package/src/lib/components/crm-export-all-button.js +0 -98
  464. package/src/lib/components/crm-export-all-button.js.map +0 -1
  465. package/src/lib/components/csv-import-drawer.d.ts +0 -139
  466. package/src/lib/components/csv-import-drawer.js +0 -757
  467. package/src/lib/components/csv-import-drawer.js.map +0 -1
  468. package/src/lib/components/deal-import-drawer.d.ts +0 -24
  469. package/src/lib/components/deal-import-drawer.js +0 -83
  470. package/src/lib/components/deal-import-drawer.js.map +0 -1
  471. package/src/lib/components/lead-import-drawer.d.ts +0 -27
  472. package/src/lib/components/lead-import-drawer.js +0 -118
  473. package/src/lib/components/lead-import-drawer.js.map +0 -1
  474. package/src/lib/components/lead-source-values-card.d.ts +0 -27
  475. package/src/lib/components/lead-source-values-card.js.map +0 -1
  476. package/src/lib/components/task-import-drawer.d.ts +0 -24
  477. package/src/lib/components/task-import-drawer.js +0 -82
  478. package/src/lib/components/task-import-drawer.js.map +0 -1
  479. package/src/lib/model/companies-csv.d.ts +0 -36
  480. package/src/lib/model/companies-csv.js +0 -38
  481. package/src/lib/model/companies-csv.js.map +0 -1
  482. package/src/lib/model/contacts-csv.d.ts +0 -54
  483. package/src/lib/model/contacts-csv.js +0 -67
  484. package/src/lib/model/contacts-csv.js.map +0 -1
  485. package/src/lib/model/crm-company-import.d.ts +0 -154
  486. package/src/lib/model/crm-company-import.js +0 -331
  487. package/src/lib/model/crm-company-import.js.map +0 -1
  488. package/src/lib/model/crm-csv.d.ts +0 -239
  489. package/src/lib/model/crm-csv.js +0 -467
  490. package/src/lib/model/crm-csv.js.map +0 -1
  491. package/src/lib/model/crm-deal-import.d.ts +0 -112
  492. package/src/lib/model/crm-deal-import.js +0 -263
  493. package/src/lib/model/crm-deal-import.js.map +0 -1
  494. package/src/lib/model/crm-import.d.ts +0 -250
  495. package/src/lib/model/crm-import.js +0 -467
  496. package/src/lib/model/crm-import.js.map +0 -1
  497. package/src/lib/model/crm-lead-import.d.ts +0 -138
  498. package/src/lib/model/crm-lead-import.js +0 -446
  499. package/src/lib/model/crm-lead-import.js.map +0 -1
  500. package/src/lib/model/crm-task-import.d.ts +0 -94
  501. package/src/lib/model/crm-task-import.js +0 -275
  502. package/src/lib/model/crm-task-import.js.map +0 -1
  503. package/src/lib/model/deals-csv.d.ts +0 -32
  504. package/src/lib/model/deals-csv.js +0 -36
  505. package/src/lib/model/deals-csv.js.map +0 -1
  506. package/src/lib/model/import-column-shapes.d.ts +0 -36
  507. package/src/lib/model/import-column-shapes.js +0 -76
  508. package/src/lib/model/import-column-shapes.js.map +0 -1
  509. package/src/lib/model/lead-source-values.d.ts +0 -82
  510. package/src/lib/model/lead-source-values.js +0 -149
  511. package/src/lib/model/lead-source-values.js.map +0 -1
  512. package/src/lib/model/leads-csv.d.ts +0 -40
  513. package/src/lib/model/leads-csv.js +0 -40
  514. package/src/lib/model/leads-csv.js.map +0 -1
  515. package/src/lib/model/tasks-csv.d.ts +0 -28
  516. package/src/lib/model/tasks-csv.js +0 -28
  517. package/src/lib/model/tasks-csv.js.map +0 -1
  518. package/src/lib/server/companies-import.d.ts +0 -57
  519. package/src/lib/server/companies-import.js +0 -242
  520. package/src/lib/server/companies-import.js.map +0 -1
  521. package/src/lib/server/contacts-import.d.ts +0 -109
  522. package/src/lib/server/contacts-import.js +0 -393
  523. package/src/lib/server/contacts-import.js.map +0 -1
  524. package/src/lib/server/deals-import.d.ts +0 -76
  525. package/src/lib/server/deals-import.js +0 -248
  526. package/src/lib/server/deals-import.js.map +0 -1
  527. package/src/lib/server/export-route.d.ts +0 -60
  528. package/src/lib/server/export-route.js +0 -447
  529. package/src/lib/server/export-route.js.map +0 -1
  530. package/src/lib/server/import-context.d.ts +0 -87
  531. package/src/lib/server/import-context.js +0 -182
  532. package/src/lib/server/import-context.js.map +0 -1
  533. package/src/lib/server/lead-source-values.d.ts +0 -50
  534. package/src/lib/server/lead-source-values.js +0 -191
  535. package/src/lib/server/lead-source-values.js.map +0 -1
  536. package/src/lib/server/leads-import.d.ts +0 -84
  537. package/src/lib/server/leads-import.js +0 -367
  538. package/src/lib/server/leads-import.js.map +0 -1
  539. package/src/lib/server/tasks-import.d.ts +0 -54
  540. package/src/lib/server/tasks-import.js +0 -160
  541. package/src/lib/server/tasks-import.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-task-reminders.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 * A task's reminder at its own time (AGL-2659).\n *\n * The daily digest says once a morning what is late and what is due today;\n * nothing said \"the 3:00 call is at 3:00\". A task now carries `remindAtMs`\n * — the due time unless a person moved it, `null` for none — and an hourly\n * route reads every open task whose reminder has come due and tells the\n * assignee once, in the console and by mail. Every judgment here is pure so\n * the three writers of the field (the task routes, the REST resource, the\n * drawer) and the route that reads it agree on what a reminder is.\n *\n * ## Which reminder a save leaves\n *\n * A caller that SAYS what the reminder is gets what it said. A caller that\n * says nothing gets the rule: a new task's reminder is its due time, and an\n * edit that moves the due date moves a reminder that still sat on the old\n * due time — the reminder was never a choice of its own, so it follows —\n * while a reminder somebody set to a time of their own stays where they put\n * it. The one thing an edit never does is invent a reminder for a task that\n * has none.\n */\n\nimport type { CrmTask } from '@aglyn/aglyn/app-utils/crm'\n\n/**\n * The most due reminders one org's sweep reads per run.\n *\n * NEWEST FIRST, which is the whole reason a ceiling is safe here. A sent\n * reminder keeps its `remindAtMs` — the drawer shows when it fired — so the\n * range \"reminders up to now\" holds every reminder an open task ever had,\n * and a window of the OLDEST five hundred would fill with ones already sent\n * and never reach a new one. The newest five hundred always begin with what\n * fell due since the last run; the sent ones sit behind them.\n */\nexport const CRM_TASK_REMINDER_CEILING = 500\n\n/** The two fields the rule reads off a stored task, or off nothing on create. */\nexport interface CrmTaskReminderState {\n dueAtMs: number | null\n remindAtMs: number | null\n}\n\n/**\n * The reminder a save leaves on the task — see the module comment.\n *\n * `remindAtMs` is what the caller SAID: a time, `null` for \"no reminder\",\n * `undefined` for nothing said. `previous` is the stored task, or `null`\n * when the task is being created. A stored task from before reminders\n * existed carries no `remindAtMs` at all; it reads as `null` here — no\n * reminder — rather than as one that follows the due date, because a task\n * nobody ever set a reminder on should not start reminding the day its\n * title is corrected.\n */\nexport function crmTaskReminderAfterEdit(input: {\n dueAtMs: number | null\n remindAtMs?: number | null\n previous: CrmTaskReminderState | null\n}): number | null {\n const { dueAtMs, remindAtMs, previous } = input\n if (remindAtMs !== undefined) return remindAtMs\n if (!previous) return dueAtMs\n const followed =\n typeof previous.remindAtMs === 'number' && previous.remindAtMs === previous.dueAtMs\n if (followed && dueAtMs !== previous.dueAtMs) return dueAtMs\n return previous.remindAtMs ?? null\n}\n\n/** Whether a task's reminder is still owed: set, and not yet handled. */\nexport function crmTaskReminderPending(\n task: Pick<CrmTask, 'remindAtMs' | 'reminderSentAtMs'>,\n): boolean {\n return typeof task.remindAtMs === 'number' && task.reminderSentAtMs === undefined\n}\n\n/**\n * Whether the runner owes this task a reminder now: open, pending, and the\n * reminder's time has come. The query reads the first and third off the\n * index; the second is a fact about a field's absence, which a query cannot\n * select on, so the runner asks it here over the page it read.\n */\nexport function crmTaskReminderDue(\n task: Pick<CrmTask, 'status' | 'remindAtMs' | 'reminderSentAtMs'>,\n nowMs: number,\n): boolean {\n return (\n task.status === 'open' &&\n crmTaskReminderPending(task) &&\n (task.remindAtMs as number) <= nowMs\n )\n}\n\n/** A due reminder as the runner carries it: the fields a line and a link need. */\nexport interface CrmReminderTask\n extends Pick<CrmTask, 'title' | 'kind' | 'contactId' | 'companyId' | 'dealId'> {\n id: string\n dueAtMs: number | null\n remindAtMs: number\n assigneeUid: string\n /** The site the task was created on, or `''` for the organization's own. */\n hostId: string\n}\n\n/**\n * The console path a reminder opens: the record the task is for, in the\n * hub, or the tasks list when it is for nobody in particular — the same\n * page the assignment notification opens, in the `/{hostDocId}/rest` shape\n * every host notification uses and the `/org/rest` shape the console\n * rewrites onto the organization's own hub for a task with no site.\n */\nexport function crmTaskReminderLink(\n hostId: string | null,\n task: Pick<CrmTask, 'contactId' | 'companyId' | 'dealId'>,\n): string {\n const base = hostId ? `/${hostId}/crm` : '/org/crm'\n if (task.contactId) return `${base}/contacts/${encodeURIComponent(task.contactId)}`\n if (task.dealId) return `${base}/deals/${encodeURIComponent(task.dealId)}`\n if (task.companyId) return `${base}/companies/${encodeURIComponent(task.companyId)}`\n return `${base}/tasks`\n}\n\n/**\n * `Wed, Sep 9, 3:00 PM CDT` — the words a reminder says a time with. The zone\n * is the server's choice, not the reader's, so it is printed: a reader an\n * hour away would otherwise take it for their own clock.\n */\nexport function crmTaskReminderWhen(ms: number, timeZone: string): string {\n return new Date(ms).toLocaleString('en-US', {\n timeZone,\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n timeZoneName: 'short',\n })\n}\n\n/**\n * One task as the reminder email lists it: its title and when it is due, or\n * that it has no due date — a reminder can sit on a task with none, at a time\n * the person chose.\n */\nexport function crmTaskReminderLine(\n task: Pick<CrmReminderTask, 'title' | 'dueAtMs'>,\n timeZone: string,\n): string {\n return typeof task.dueAtMs === 'number'\n ? `${task.title} · due ${crmTaskReminderWhen(task.dueAtMs, timeZone)}`\n : `${task.title} · no due date`\n}\n\n/**\n * The console notification's body, one per task, which is also what a\n * preview of it shows. It says it is a reminder and names the workspace, so\n * it reads whole without the \"Task reminder\" title above it.\n */\nexport function composeCrmTaskReminderBody(\n task: Pick<CrmReminderTask, 'title' | 'dueAtMs'>,\n timeZone: string,\n workspaceName?: string,\n): string {\n const workspace = workspaceName?.trim()\n const when =\n typeof task.dueAtMs === 'number'\n ? `, due ${crmTaskReminderWhen(task.dueAtMs, timeZone)}.`\n : ' (no due date).'\n return `Reminder for your task${workspace ? ` in ${workspace}` : ''}: ${task.title}${when}`\n}\n\n/**\n * One mail per member per run, so five tasks due at nine o'clock are one\n * message and not five. The subject names the task when there is one, and\n * never says a task is due: a reminder can be set ahead of the due time, or\n * on a task with none.\n */\nexport function composeCrmTaskReminderSubject(\n tasks: ReadonlyArray<Pick<CrmReminderTask, 'title'>>,\n): string {\n if (tasks.length === 1) return `Reminder: ${tasks[0].title}`\n return `Reminder: ${tasks.length} tasks`\n}\n\n/** What the reminder email needs from the route besides the tasks. */\nexport interface CrmTaskReminderEmailInput {\n tasks: readonly CrmReminderTask[]\n timeZone: string\n /** The product name the mail reads as — the org's brand when white-labeled. */\n productName: string\n /**\n * The workspace the tasks are in. The system email shows only the body, so\n * the body names it: a member of two workspaces gets reminders from each.\n */\n workspaceName?: string\n /** The console page a task opens on, absolute. */\n taskUrl: (task: CrmReminderTask) => string\n /** Account settings → Notifications, where the category can be muted. */\n settingsUrl: string\n /** The brand's support line, already prefixed, or empty. */\n supportLine?: string\n /**\n * Leave out the closing paragraph and the support line (AGL-3367), for the\n * copy that goes inside a system email: its footer says why the mail came\n * and where to get help, and its body links the settings page.\n */\n omitClosing?: boolean\n}\n\n/**\n * The plain-text email. Text rather than a designed template for the\n * reason the digest gives: the body is a list, and `sendEmail` synthesizes\n * the HTML part that makes its links live. Soonest due first, each task on\n * its own line with its link under it, and the way out last.\n */\nexport function composeCrmTaskReminderEmailText(input: CrmTaskReminderEmailInput): string {\n const { tasks, timeZone, productName } = input\n const ordered = [...tasks].sort(\n (a, b) => (a.dueAtMs ?? a.remindAtMs) - (b.dueAtMs ?? b.remindAtMs),\n )\n // \"About\", never \"is due\": a reminder fires at the time somebody chose,\n // which may be a day ahead of the due date or on a task with none, and\n // each line below says when its task is actually due.\n const where = input.workspaceName?.trim() ? ` in ${input.workspaceName.trim()}` : ''\n const lines: string[] = [\n ordered.length === 1\n ? `A reminder from ${productName} about your task${where}:`\n : `A reminder from ${productName} about ${ordered.length} of your tasks${where}:`,\n '',\n ]\n for (const task of ordered) {\n lines.push(`- ${crmTaskReminderLine(task, timeZone)}`)\n lines.push(` ${input.taskUrl(task)}`)\n }\n if (input.omitClosing) return lines.join('\\n').trimEnd()\n lines.push(\n '',\n 'You get task reminders because the Forms & bookings category is on in ' +\n `your notification settings: ${input.settingsUrl}`,\n )\n return lines.join('\\n') + (input.supportLine ?? '')\n}\n"],"names":["CRM_TASK_REMINDER_CEILING","crmTaskReminderAfterEdit","input","previous","dueAtMs","remindAtMs","undefined","followed","crmTaskReminderPending","task","reminderSentAtMs","crmTaskReminderDue","nowMs","status","crmTaskReminderLink","hostId","base","contactId","encodeURIComponent","dealId","companyId","crmTaskReminderWhen","ms","timeZone","Date","toLocaleString","weekday","month","day","hour","minute","timeZoneName","crmTaskReminderLine","title","composeCrmTaskReminderBody","workspaceName","workspace","trim","when","composeCrmTaskReminderSubject","tasks","length","composeCrmTaskReminderEmailText","productName","ordered","sort","a","b","where","lines","push","taskUrl","omitClosing","join","trimEnd","settingsUrl","supportLine"],"mappings":"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"}
@@ -20,7 +20,7 @@
20
20
  * page and the server route read one definition of "what is this pipeline
21
21
  * worth" and "which stage edits are legal".
22
22
  */
23
- import { type CrmDeal, type CrmDealStage, type CrmPipeline } from '@aglyn/aglyn/app-utils/crm';
23
+ import { type CrmDeal, type CrmDealStage, type CrmForecastCategory, type CrmPipeline } from '@aglyn/aglyn/app-utils/crm';
24
24
  /** A deal as the console reads it: the model plus its document id. */
25
25
  export type DealDoc = CrmDeal & {
26
26
  $id: string;
@@ -144,7 +144,7 @@ export interface BoardSummary {
144
144
  * something closed. `weightedDealAmountCents` already answers zero for a
145
145
  * stage the pipeline no longer has.
146
146
  */
147
- export declare function boardSummary(deals: readonly Pick<CrmDeal, 'status' | 'amountCents' | 'currency' | 'stageId'>[], pipeline: Pick<CrmPipeline, 'stages'> | null | undefined): BoardSummary;
147
+ export declare function boardSummary(deals: readonly (Pick<CrmDeal, 'status' | 'amountCents' | 'currency' | 'stageId'> & Partial<Pick<CrmDeal, 'probability'>>)[], pipeline: Pick<CrmPipeline, 'stages'> | null | undefined): BoardSummary;
148
148
  /** `$1,200.00 · €300.00` — one figure per currency, largest first. */
149
149
  export declare function formatAmountByCurrency(totals: AmountByCurrency): string;
150
150
  /** The pipeline's stages in `order`, ties broken by position. */
@@ -189,6 +189,12 @@ export declare function renameStage(stages: readonly CrmDealStage[], stageId: st
189
189
  * stage at 60% would change nothing and only mislead the editor.
190
190
  */
191
191
  export declare function setStageProbability(stages: readonly CrmDealStage[], stageId: string, probability: unknown): CrmDealStage[];
192
+ /**
193
+ * Set an OPEN stage's forecast category (AGL-3516). Won is Closed and Lost
194
+ * is Omitted by definition, for the reason {@link setStageProbability}
195
+ * gives; a deal's own category is changed on the deal.
196
+ */
197
+ export declare function setStageForecastCategory(stages: readonly CrmDealStage[], stageId: string, category: CrmForecastCategory | string): CrmDealStage[];
192
198
  /**
193
199
  * Swap an open stage with its open neighbor. The closing stages are not in
194
200
  * the reorderable range, so an open stage can never be moved past Won.
@@ -23,7 +23,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
23
23
  // reached from the server entry through the deal-stage route, and the barrel
24
24
  // carries client-only context modules the App Router graph refuses on a
25
25
  // server path (AGL-405/1349).
26
- import { DEFAULT_DEAL_STAGES, dealStageById, isPipelineArchived, weightedDealAmountCents } from "@aglyn/aglyn/app-utils/crm";
26
+ import { DEFAULT_DEAL_STAGES, dealStageById, isCrmForecastCategory, isPipelineArchived, weightedDealAmountCents } from "@aglyn/aglyn/app-utils/crm";
27
27
  /** The document id the seeded default pipeline is written under. */ export const DEFAULT_PIPELINE_ID = 'default';
28
28
  /** Name of the pipeline every org starts with. */ export const DEFAULT_PIPELINE_NAME = 'Sales';
29
29
  export const PIPELINE_NAME_MAX = 60;
@@ -285,7 +285,8 @@ function addAmount(totals, currency, cents) {
285
285
  name: trimmed,
286
286
  order: at,
287
287
  probability: clampProbability(probability),
288
- kind: 'open'
288
+ kind: 'open',
289
+ forecastCategory: 'pipeline'
289
290
  };
290
291
  sorted.splice(at, 0, stage);
291
292
  return renumberStages(sorted);
@@ -305,6 +306,18 @@ export function renameStage(stages, stageId, name) {
305
306
  probability: clampProbability(probability)
306
307
  }) : stage);
307
308
  }
309
+ /**
310
+ * Set an OPEN stage's forecast category (AGL-3516). Won is Closed and Lost
311
+ * is Omitted by definition, for the reason {@link setStageProbability}
312
+ * gives; a deal's own category is changed on the deal.
313
+ */ export function setStageForecastCategory(stages, stageId, category) {
314
+ if (!isCrmForecastCategory(category)) return [
315
+ ...stages
316
+ ];
317
+ return stages.map((stage)=>stage.id === stageId && stage.kind === 'open' ? _extends({}, stage, {
318
+ forecastCategory: category
319
+ }) : stage);
320
+ }
308
321
  /**
309
322
  * Swap an open stage with its open neighbor. The closing stages are not in
310
323
  * the reorderable range, so an open stage can never be moved past Won.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/deal-board-model.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 pipeline's arithmetic and the stage editor's rules (AGL-2598), as pure\n * functions — no Firestore, no React — so the board, the table, the detail\n * page and the server route read one definition of \"what is this pipeline\n * worth\" and \"which stage edits are legal\".\n */\n\n// The module itself and never the `@aglyn/aglyn` barrel: this file is\n// reached from the server entry through the deal-stage route, and the barrel\n// carries client-only context modules the App Router graph refuses on a\n// server path (AGL-405/1349).\nimport {\n type CrmDeal,\n type CrmDealStage,\n type CrmPipeline,\n DEFAULT_DEAL_STAGES,\n dealStageById,\n isPipelineArchived,\n weightedDealAmountCents,\n} from '@aglyn/aglyn/app-utils/crm'\n\n/** A deal as the console reads it: the model plus its document id. */\nexport type DealDoc = CrmDeal & {\n $id: string\n /**\n * The contact's and the company's names, copied onto the deal when the\n * link is made. A board of two hundred cards cannot afford a keyed read per\n * card to caption each one, and the caption is allowed to lag a rename —\n * the link itself is the id, which never changes.\n */\n contactName?: string\n companyName?: string\n}\n\n/** A pipeline as the console reads it. */\nexport type PipelineDoc = CrmPipeline & { $id: string }\n\n/** The document id the seeded default pipeline is written under. */\nexport const DEFAULT_PIPELINE_ID = 'default'\n\n/** Name of the pipeline every org starts with. */\nexport const DEFAULT_PIPELINE_NAME = 'Sales'\n\nexport const PIPELINE_NAME_MAX = 60\n\n/** The pipelines a picker offers and a new deal may land in: the ones not archived. */\nexport function activePipelines<T extends Pick<CrmPipeline, 'archivedAt'>>(\n pipelines: readonly T[],\n): T[] {\n return pipelines.filter((pipeline) => !isPipelineArchived(pipeline))\n}\n\n/**\n * The pipeline a new deal lands in when nobody picks one: the active one\n * marked default, else the first active one, else nothing. An archived\n * pipeline is never the default even if its flag was left on — archiving\n * clears the flag, but a document written by an older console may not\n * have had it cleared, and a default nobody can create a deal in is a\n * board that opens on a pipeline the picker does not list.\n */\nexport function defaultPipelineOf<T extends Pick<CrmPipeline, 'archivedAt' | 'isDefault'>>(\n pipelines: readonly T[],\n): T | null {\n const active = activePipelines(pipelines)\n return active.find((pipeline) => pipeline.isDefault) ?? active[0] ?? null\n}\n\n/**\n * Why a pipeline cannot be created or renamed to `name`, or `null`.\n *\n * Names are unique among the ACTIVE pipelines, compared without case: the\n * picker lists them by name alone, and two rows reading \"Sales\" would be a\n * choice nobody can make. An archived pipeline's name is free to reuse —\n * \"Sales 2025\" retired and \"Sales\" started over is the point of archiving.\n */\nexport function pipelineNameProblem(\n name: string,\n pipelines: ReadonlyArray<Pick<CrmPipeline, 'name' | 'archivedAt'> & { $id: string }>,\n ownId?: string,\n): string | null {\n const trimmed = String(name ?? '').trim()\n if (!trimmed) return 'A pipeline needs a name.'\n const taken = activePipelines(pipelines).find(\n (pipeline) =>\n pipeline.$id !== ownId &&\n String(pipeline.name ?? '').trim().toLowerCase() === trimmed.toLowerCase(),\n )\n if (taken) return `Another pipeline is already called \"${taken.name}\".`\n return null\n}\n\n/**\n * Why a pipeline cannot be archived, or `null` when it can.\n *\n * `openDeals` is the server's count, asked for before this is called, the\n * way `stageRemovalRefusal` asks: an open deal in an archived pipeline is\n * a card on no board, and the count is taken at the click because a deal\n * can be created into the pipeline from another tab meanwhile. The default\n * pipeline cannot go while it is the default — a new deal has to land\n * somewhere — and the last active one cannot go at all, for the same\n * reason from the other side.\n */\nexport function pipelineArchiveRefusal(\n pipeline: Pick<CrmPipeline, 'archivedAt' | 'isDefault'> & { $id: string },\n pipelines: ReadonlyArray<Pick<CrmPipeline, 'archivedAt'> & { $id: string }>,\n openDeals: number,\n): string | null {\n if (isPipelineArchived(pipeline)) return 'This pipeline is already archived.'\n if (activePipelines(pipelines).filter((entry) => entry.$id !== pipeline.$id).length === 0) {\n return 'The last pipeline cannot be archived.'\n }\n if (pipeline.isDefault) {\n return 'This is the default pipeline. Make another one the default first.'\n }\n if (openDeals > 0) {\n return (\n `${openDeals.toLocaleString()} open ${openDeals === 1 ? 'deal is' : 'deals are'} ` +\n 'in this pipeline. Close or move them first, so none is left on a board nobody opens.'\n )\n }\n return null\n}\n\n/** What every pipeline write stamps besides its name. */\nexport interface PipelineWriteContext {\n visibleTo: readonly string[]\n hostId: string\n uid: string\n nowMs: number\n}\n\n/**\n * The document a new pipeline is created as: the name, a COPY of the\n * default stages, not the default, active. A copy for the reason\n * `DEFAULT_DEAL_STAGES` gives — a second pipeline seeded later must start\n * from the original set, whatever the first one's editor did to its own.\n */\nexport function newPipelineDocument(\n name: string,\n context: PipelineWriteContext,\n): Record<string, unknown> {\n return {\n name: String(name ?? '').trim().slice(0, PIPELINE_NAME_MAX),\n stages: DEFAULT_DEAL_STAGES.map((stage) => ({ ...stage })),\n isDefault: false,\n archivedAt: null,\n visibleTo: [...context.visibleTo],\n hostId: context.hostId,\n createdByUid: context.uid,\n createdAt: new Date(context.nowMs),\n updatedAt: new Date(context.nowMs),\n }\n}\n\n/**\n * Currencies the amount picker offers, lowercase ISO 4217 as the model\n * stores them. Short on purpose: a picker of a hundred and eighty currencies\n * is a scroll, and a deal in one outside this list is entered through the\n * API with whatever code it carries — the display below formats any code\n * `Intl` knows.\n */\nexport const DEAL_CURRENCIES = [\n 'usd',\n 'eur',\n 'gbp',\n 'cad',\n 'aud',\n 'nzd',\n 'chf',\n 'jpy',\n 'sek',\n 'nok',\n 'dkk',\n 'mxn',\n 'brl',\n 'inr',\n 'sgd',\n] as const\n\nexport const DEFAULT_DEAL_CURRENCY = 'usd'\n\n/** How a deal's `status` reads on screen. */\nexport const DEAL_STATUS_LABELS: Record<CrmDeal['status'], string> = {\n open: 'Open',\n won: 'Won',\n lost: 'Lost',\n}\n\n/** Cents → the reader's locale, or a plain number when the code is unknown. */\nexport function formatMoney(cents: number, currency: string | undefined): string {\n const code = String(currency || DEFAULT_DEAL_CURRENCY).toUpperCase()\n const amount = (Number(cents) || 0) / 100\n try {\n return new Intl.NumberFormat(undefined, {\n style: 'currency',\n currency: code,\n }).format(amount)\n } catch {\n return `${amount.toFixed(2)} ${code}`\n }\n}\n\n/**\n * A typed amount, as cents, or `null` when it is not a number.\n *\n * Accepts what people paste from an invoice — a thousands separator, a\n * currency symbol, surrounding space — and refuses a negative: a deal is\n * worth zero or more, and a negative amount here is a typo rather than a\n * refund. Rounded rather than truncated so `19.995` does not lose a cent.\n */\nexport function parseAmountInput(input: string): number | null {\n const cleaned = String(input ?? '')\n .replace(/[^0-9.-]/g, '')\n .trim()\n if (!cleaned) return null\n const value = Number(cleaned)\n if (!Number.isFinite(value) || value < 0) return null\n return Math.round(value * 100)\n}\n\n/** Cents → the string the amount field shows, `''` for nothing. */\nexport function amountInputValue(cents: number | undefined | null): string {\n if (typeof cents !== 'number' || !Number.isFinite(cents)) return ''\n return (cents / 100).toFixed(2)\n}\n\n/**\n * Epoch ms from whatever a timestamp field holds after a round trip — a\n * Firestore `Timestamp`, a `Date` a client just wrote, or a number.\n */\nexport function timestampMs(value: unknown): number | null {\n if (!value) return null\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (value instanceof Date) return value.getTime()\n const seconds = (value as { seconds?: unknown }).seconds\n if (typeof seconds === 'number') return seconds * 1000\n const toMillis = (value as { toMillis?: () => number }).toMillis\n if (typeof toMillis === 'function') return toMillis.call(value)\n return null\n}\n\n/** Whole days a deal has sat in its current stage, never negative. */\nexport function daysInStage(\n deal: Pick<CrmDeal, 'stageChangedAtMs' | 'createdAt'>,\n nowMs: number,\n): number {\n const since =\n typeof deal.stageChangedAtMs === 'number'\n ? deal.stageChangedAtMs\n : timestampMs(deal.createdAt)\n if (!since) return 0\n return Math.max(0, Math.floor((nowMs - since) / 86_400_000))\n}\n\n/** A sum per currency, because two currencies never add into one number. */\nexport type AmountByCurrency = Record<string, number>\n\nfunction addAmount(\n totals: AmountByCurrency,\n currency: string | undefined,\n cents: number,\n): void {\n const code = String(currency || DEFAULT_DEAL_CURRENCY).toLowerCase()\n totals[code] = (totals[code] ?? 0) + cents\n}\n\nexport interface BoardSummary {\n /** Deals whose `status` is `open`, whatever stage they sit in. */\n openCount: number\n /** Every open deal's full amount, per currency. */\n valueByCurrency: AmountByCurrency\n /** Every open deal's amount times its stage's probability, per currency. */\n weightedByCurrency: AmountByCurrency\n}\n\n/**\n * The three figures above the board.\n *\n * Closed deals are excluded from both totals rather than counted at full or\n * zero value: the summary describes what is still in play, and a won deal\n * counted in \"pipeline value\" would make the pipeline look larger every time\n * something closed. `weightedDealAmountCents` already answers zero for a\n * stage the pipeline no longer has.\n */\nexport function boardSummary(\n deals: readonly Pick<\n CrmDeal,\n 'status' | 'amountCents' | 'currency' | 'stageId'\n >[],\n pipeline: Pick<CrmPipeline, 'stages'> | null | undefined,\n): BoardSummary {\n const summary: BoardSummary = {\n openCount: 0,\n valueByCurrency: {},\n weightedByCurrency: {},\n }\n for (const deal of deals) {\n if (deal.status !== 'open') continue\n summary.openCount += 1\n const amount = Math.max(0, Math.round(Number(deal.amountCents ?? 0) || 0))\n addAmount(summary.valueByCurrency, deal.currency, amount)\n addAmount(\n summary.weightedByCurrency,\n deal.currency,\n weightedDealAmountCents(deal, dealStageById(pipeline, deal.stageId)),\n )\n }\n return summary\n}\n\n/** `$1,200.00 · €300.00` — one figure per currency, largest first. */\nexport function formatAmountByCurrency(totals: AmountByCurrency): string {\n const entries = Object.entries(totals).sort((a, b) => b[1] - a[1])\n if (!entries.length) return formatMoney(0, DEFAULT_DEAL_CURRENCY)\n return entries.map(([code, cents]) => formatMoney(cents, code)).join(' · ')\n}\n\n/** The pipeline's stages in `order`, ties broken by position. */\nexport function sortedStages(\n pipeline: Pick<CrmPipeline, 'stages'> | null | undefined,\n): CrmDealStage[] {\n return [...(pipeline?.stages ?? [])]\n .map((stage, index) => ({ stage, index }))\n .sort((a, b) => a.stage.order - b.stage.order || a.index - b.index)\n .map(({ stage }) => stage)\n}\n\n/** The stages a deal can be dragged between — everything that is not closed. */\nexport function openStages(\n pipeline: Pick<CrmPipeline, 'stages'> | null | undefined,\n): CrmDealStage[] {\n return sortedStages(pipeline).filter((stage) => stage.kind === 'open')\n}\n\n/** The stage whose `kind` is `won` or `lost`, or null when the pipeline lacks one. */\nexport function closingStage(\n pipeline: Pick<CrmPipeline, 'stages'> | null | undefined,\n kind: 'won' | 'lost',\n): CrmDealStage | null {\n return sortedStages(pipeline).find((stage) => stage.kind === kind) ?? null\n}\n\n/**\n * The id a new stage is stored under: the name as a slug, made unique\n * against the stages already there.\n *\n * A slug rather than a random id because the id appears in every deal that\n * sits in the stage and in every automation filter that names it, so a\n * reader looking at `stageId == \"demo-booked\"` should be able to tell which\n * stage that is without opening the pipeline. Uniqueness is what keeps a\n * second \"Demo\" from silently becoming the first.\n */\nexport function newStageId(\n name: string,\n existing: readonly CrmDealStage[],\n): string {\n const base =\n String(name ?? '')\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 40) || 'stage'\n const taken = new Set(existing.map((stage) => stage.id))\n if (!taken.has(base)) return base\n let suffix = 2\n while (taken.has(`${base}-${suffix}`)) suffix += 1\n return `${base}-${suffix}`\n}\n\n/**\n * Stages re-numbered 0..n in the order they are GIVEN.\n *\n * Positional on purpose: every caller hands this an array it has already\n * arranged — a swap, a splice, a filter over the sorted stages — and the\n * stored `order` on each entry is the arrangement being replaced. Sorting by\n * it here would put a swapped pair straight back where it was.\n */\nexport function renumberStages(\n stages: readonly CrmDealStage[],\n): CrmDealStage[] {\n return stages.map((stage, order) => ({ ...stage, order }))\n}\n\n/** 0–100, whole numbers; anything else becomes the nearest bound. */\nexport function clampProbability(value: unknown): number {\n const number = Math.round(Number(value))\n if (!Number.isFinite(number)) return 0\n return Math.min(100, Math.max(0, number))\n}\n\n/**\n * Add an open stage in front of the closing stages.\n *\n * Won and Lost stay last: a pipeline reads left to right as a progression,\n * and a stage added after \"Won\" would be a step past the end.\n */\nexport function addStage(\n stages: readonly CrmDealStage[],\n name: string,\n probability = 50,\n): CrmDealStage[] {\n const trimmed = String(name ?? '').trim()\n if (!trimmed) return [...stages]\n const sorted = sortedStages({ stages: [...stages] })\n const firstClosed = sorted.findIndex((stage) => stage.kind !== 'open')\n const at = firstClosed === -1 ? sorted.length : firstClosed\n const stage: CrmDealStage = {\n id: newStageId(trimmed, sorted),\n name: trimmed,\n order: at,\n probability: clampProbability(probability),\n kind: 'open',\n }\n sorted.splice(at, 0, stage)\n return renumberStages(sorted)\n}\n\nexport function renameStage(\n stages: readonly CrmDealStage[],\n stageId: string,\n name: string,\n): CrmDealStage[] {\n const trimmed = String(name ?? '').trim()\n return stages.map((stage) =>\n stage.id === stageId && trimmed ? { ...stage, name: trimmed } : stage,\n )\n}\n\n/**\n * Set an OPEN stage's probability. Won is 100 and Lost is 0 by definition —\n * `weightedDealAmountCents` reads the status before the stage, so a Won\n * stage at 60% would change nothing and only mislead the editor.\n */\nexport function setStageProbability(\n stages: readonly CrmDealStage[],\n stageId: string,\n probability: unknown,\n): CrmDealStage[] {\n return stages.map((stage) =>\n stage.id === stageId && stage.kind === 'open'\n ? { ...stage, probability: clampProbability(probability) }\n : stage,\n )\n}\n\n/**\n * Swap an open stage with its open neighbor. The closing stages are not in\n * the reorderable range, so an open stage can never be moved past Won.\n */\nexport function moveStage(\n stages: readonly CrmDealStage[],\n stageId: string,\n direction: 'up' | 'down',\n): CrmDealStage[] {\n const sorted = sortedStages({ stages: [...stages] })\n const index = sorted.findIndex((stage) => stage.id === stageId)\n if (index === -1 || sorted[index].kind !== 'open') return sorted\n const target = direction === 'up' ? index - 1 : index + 1\n if (target < 0 || target >= sorted.length) return sorted\n if (sorted[target].kind !== 'open') return sorted\n const swapped = [...sorted]\n const moving = swapped[index]\n swapped[index] = swapped[target]\n swapped[target] = moving\n return renumberStages(swapped)\n}\n\n/**\n * Why a stage cannot be removed, or `null` when it can.\n *\n * `dealsInStage` is the server's count, asked for before this is called: a\n * stage with deals in it cannot go, because every one of those deals would\n * be left pointing at a stage that no longer exists and worth nothing to the\n * forecast. The closing stages cannot go at all — a deal has to be able to\n * be won and lost.\n */\nexport function stageRemovalRefusal(\n stages: readonly CrmDealStage[],\n stageId: string,\n dealsInStage: number,\n): string | null {\n const stage = stages.find((entry) => entry.id === stageId)\n if (!stage) return 'This stage is no longer in the pipeline.'\n if (stage.kind !== 'open') {\n return `Every pipeline keeps its ${stage.kind === 'won' ? 'Won' : 'Lost'} stage.`\n }\n if (openStages({ stages: [...stages] }).length <= 1) {\n return 'A pipeline needs at least one open stage.'\n }\n if (dealsInStage > 0) {\n return (\n `${dealsInStage.toLocaleString()} ${dealsInStage === 1 ? 'deal is' : 'deals are'} ` +\n 'in this stage. Move them first, so none is left in a stage that no longer exists.'\n )\n }\n return null\n}\n\nexport function removeStage(\n stages: readonly CrmDealStage[],\n stageId: string,\n): CrmDealStage[] {\n return renumberStages(stages.filter((stage) => stage.id !== stageId))\n}\n\n/**\n * What a stored set of stages has to satisfy before it is written back:\n * unique ids, a name on every stage, exactly one Won and one Lost, at least\n * one open stage. The editor refuses a save that fails this rather than\n * writing a pipeline the board cannot draw.\n */\nexport function stagesProblem(stages: readonly CrmDealStage[]): string | null {\n if (!stages.length) return 'A pipeline needs stages.'\n const ids = new Set<string>()\n for (const stage of stages) {\n if (!stage.id) return 'Every stage needs an id.'\n if (ids.has(stage.id)) return `Two stages share the id \"${stage.id}\".`\n ids.add(stage.id)\n if (!String(stage.name ?? '').trim()) return 'Every stage needs a name.'\n }\n const won = stages.filter((stage) => stage.kind === 'won').length\n const lost = stages.filter((stage) => stage.kind === 'lost').length\n if (won !== 1) return 'A pipeline has exactly one Won stage.'\n if (lost !== 1) return 'A pipeline has exactly one Lost stage.'\n if (!stages.some((stage) => stage.kind === 'open')) {\n return 'A pipeline needs at least one open stage.'\n }\n return null\n}\n\n/**\n * The event a stage change emits, and the flat payload a workflow filter\n * reads (AGL-2598). Strings and numbers only — a workflow expression\n * evaluates over scalars, and an absent link is the empty string rather than\n * a missing key so `contactId == \"\"` is a filter somebody can write.\n */\nexport type DealEventName = 'dealStageChanged' | 'dealWon' | 'dealLost'\n\nexport function dealEventName(status: CrmDeal['status']): DealEventName {\n if (status === 'won') return 'dealWon'\n if (status === 'lost') return 'dealLost'\n return 'dealStageChanged'\n}\n\nexport function dealEventPayload(\n dealId: string,\n deal: Pick<\n CrmDeal,\n | 'title'\n | 'amountCents'\n | 'currency'\n | 'stageId'\n | 'ownerUid'\n | 'contactId'\n | 'companyId'\n | 'lostReason'\n >,\n previousStageId: string,\n): Record<string, string | number> {\n return {\n dealId,\n title: String(deal.title ?? ''),\n amountCents: Math.max(0, Math.round(Number(deal.amountCents ?? 0) || 0)),\n currency: String(deal.currency || DEFAULT_DEAL_CURRENCY).toLowerCase(),\n stageId: String(deal.stageId ?? ''),\n previousStageId: String(previousStageId ?? ''),\n ownerUid: String(deal.ownerUid ?? ''),\n contactId: String(deal.contactId ?? ''),\n companyId: String(deal.companyId ?? ''),\n ...(deal.lostReason ? { lostReason: String(deal.lostReason) } : {}),\n }\n}\n\n/** Initials for an avatar: the first letters of the first two words. */\nexport function initialsFor(label: string): string {\n const words = String(label ?? '')\n .trim()\n .split(/[\\s@._-]+/)\n .filter(Boolean)\n const letters = words\n .slice(0, 2)\n .map((word) => word[0])\n .join('')\n return (letters || '?').toUpperCase()\n}\n\n/**\n * A stable hue for one person, so an owner keeps one color across cards and\n * sessions. Hashing the label rather than picking from a palette is what\n * makes the same uid the same color on every machine.\n */\nexport function hueFor(label: string): number {\n let hash = 0\n for (let index = 0; index < label.length; index += 1) {\n hash = (hash * 31 + label.charCodeAt(index)) % 360\n }\n return hash\n}\n\n/** `2026-09-05` for a date input, from epoch ms; `''` for none. */\nexport function dateInputValue(ms: number | null | undefined): string {\n if (typeof ms !== 'number' || !Number.isFinite(ms)) return ''\n const at = new Date(ms)\n const offset = at.getTimezoneOffset() * 60_000\n return new Date(ms - offset).toISOString().slice(0, 10)\n}\n\n/**\n * Epoch ms at local noon of a typed `YYYY-MM-DD`, or `null`.\n *\n * Noon rather than midnight so the day survives a timezone round trip: a\n * close date stored at local midnight and read back one timezone west shows\n * the day before.\n */\nexport function dateInputMs(value: string): number | null {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(String(value ?? '').trim())\n if (!match) return null\n const at = new Date(\n Number(match[1]),\n Number(match[2]) - 1,\n Number(match[3]),\n 12,\n 0,\n 0,\n 0,\n )\n return Number.isNaN(at.getTime()) ? null : at.getTime()\n}\n"],"names":["DEFAULT_DEAL_STAGES","dealStageById","isPipelineArchived","weightedDealAmountCents","DEFAULT_PIPELINE_ID","DEFAULT_PIPELINE_NAME","PIPELINE_NAME_MAX","activePipelines","pipelines","filter","pipeline","defaultPipelineOf","active","find","isDefault","pipelineNameProblem","name","ownId","trimmed","String","trim","taken","$id","toLowerCase","pipelineArchiveRefusal","openDeals","entry","length","toLocaleString","newPipelineDocument","context","slice","stages","map","stage","archivedAt","visibleTo","hostId","createdByUid","uid","createdAt","Date","nowMs","updatedAt","DEAL_CURRENCIES","DEFAULT_DEAL_CURRENCY","DEAL_STATUS_LABELS","open","won","lost","formatMoney","cents","currency","code","toUpperCase","amount","Number","Intl","NumberFormat","undefined","style","format","toFixed","parseAmountInput","input","cleaned","replace","value","isFinite","Math","round","amountInputValue","timestampMs","getTime","seconds","toMillis","call","daysInStage","deal","since","stageChangedAtMs","max","floor","addAmount","totals","boardSummary","deals","summary","openCount","valueByCurrency","weightedByCurrency","status","amountCents","stageId","formatAmountByCurrency","entries","Object","sort","a","b","join","sortedStages","index","order","openStages","kind","closingStage","newStageId","existing","base","Set","id","has","suffix","renumberStages","clampProbability","number","min","addStage","probability","sorted","firstClosed","findIndex","at","splice","renameStage","setStageProbability","moveStage","direction","target","swapped","moving","stageRemovalRefusal","dealsInStage","removeStage","stagesProblem","ids","add","some","dealEventName","dealEventPayload","dealId","previousStageId","title","ownerUid","contactId","companyId","lostReason","initialsFor","label","words","split","Boolean","letters","word","hueFor","hash","charCodeAt","dateInputValue","ms","offset","getTimezoneOffset","toISOString","dateInputMs","match","exec","isNaN"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;CAKC,GAED,sEAAsE;AACtE,6EAA6E;AAC7E,wEAAwE;AACxE,8BAA8B;AAC9B,SAIEA,mBAAmB,EACnBC,aAAa,EACbC,kBAAkB,EAClBC,uBAAuB,QAClB,6BAA4B;AAkBnC,kEAAkE,GAClE,OAAO,MAAMC,sBAAsB,UAAS;AAE5C,gDAAgD,GAChD,OAAO,MAAMC,wBAAwB,QAAO;AAE5C,OAAO,MAAMC,oBAAoB,GAAE;AAEnC,qFAAqF,GACrF,OAAO,SAASC,gBACdC,SAAuB;IAEvB,OAAOA,UAAUC,MAAM,CAAC,CAACC,WAAa,CAACR,mBAAmBQ;AAC5D;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,kBACdH,SAAuB;QAGhBI,MAAAA;IADP,MAAMA,SAASL,gBAAgBC;IAC/B,QAAOI,QAAAA,eAAAA,OAAOC,IAAI,CAAC,CAACH,WAAaA,SAASI,SAAS,aAA5CF,eAAiDA,MAAM,CAAC,EAAE,YAA1DA,OAA8D;AACvE;AAEA;;;;;;;CAOC,GACD,OAAO,SAASG,oBACdC,IAAY,EACZR,SAAoF,EACpFS,KAAc;IAEd,MAAMC,UAAUC,OAAOH,eAAAA,OAAQ,IAAII,IAAI;IACvC,IAAI,CAACF,SAAS,OAAO;IACrB,MAAMG,QAAQd,gBAAgBC,WAAWK,IAAI,CAC3C,CAACH;YAEQA;eADPA,SAASY,GAAG,KAAKL,SACjBE,QAAOT,iBAAAA,SAASM,IAAI,YAAbN,iBAAiB,IAAIU,IAAI,GAAGG,WAAW,OAAOL,QAAQK,WAAW;;IAE5E,IAAIF,OAAO,OAAO,CAAC,oCAAoC,EAAEA,MAAML,IAAI,CAAC,EAAE,CAAC;IACvE,OAAO;AACT;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASQ,uBACdd,QAAyE,EACzEF,SAA2E,EAC3EiB,SAAiB;IAEjB,IAAIvB,mBAAmBQ,WAAW,OAAO;IACzC,IAAIH,gBAAgBC,WAAWC,MAAM,CAAC,CAACiB,QAAUA,MAAMJ,GAAG,KAAKZ,SAASY,GAAG,EAAEK,MAAM,KAAK,GAAG;QACzF,OAAO;IACT;IACA,IAAIjB,SAASI,SAAS,EAAE;QACtB,OAAO;IACT;IACA,IAAIW,YAAY,GAAG;QACjB,OACE,GAAGA,UAAUG,cAAc,GAAG,MAAM,EAAEH,cAAc,IAAI,YAAY,YAAY,CAAC,CAAC,GAClF;IAEJ;IACA,OAAO;AACT;AAUA;;;;;CAKC,GACD,OAAO,SAASI,oBACdb,IAAY,EACZc,OAA6B;IAE7B,OAAO;QACLd,MAAMG,OAAOH,eAAAA,OAAQ,IAAII,IAAI,GAAGW,KAAK,CAAC,GAAGzB;QACzC0B,QAAQhC,oBAAoBiC,GAAG,CAAC,CAACC,QAAW,aAAKA;QACjDpB,WAAW;QACXqB,YAAY;QACZC,WAAW;eAAIN,QAAQM,SAAS;SAAC;QACjCC,QAAQP,QAAQO,MAAM;QACtBC,cAAcR,QAAQS,GAAG;QACzBC,WAAW,IAAIC,KAAKX,QAAQY,KAAK;QACjCC,WAAW,IAAIF,KAAKX,QAAQY,KAAK;IACnC;AACF;AAEA;;;;;;CAMC,GACD,OAAO,MAAME,kBAAkB;IAC7B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAS;AAEV,OAAO,MAAMC,wBAAwB,MAAK;AAE1C,2CAA2C,GAC3C,OAAO,MAAMC,qBAAwD;IACnEC,MAAM;IACNC,KAAK;IACLC,MAAM;AACR,EAAC;AAED,6EAA6E,GAC7E,OAAO,SAASC,YAAYC,KAAa,EAAEC,QAA4B;IACrE,MAAMC,OAAOlC,OAAOiC,YAAYP,uBAAuBS,WAAW;IAClE,MAAMC,SAAS,AAACC,CAAAA,OAAOL,UAAU,CAAA,IAAK;IACtC,IAAI;QACF,OAAO,IAAIM,KAAKC,YAAY,CAACC,WAAW;YACtCC,OAAO;YACPR,UAAUC;QACZ,GAAGQ,MAAM,CAACN;IACZ,EAAE,eAAM;QACN,OAAO,GAAGA,OAAOO,OAAO,CAAC,GAAG,CAAC,EAAET,MAAM;IACvC;AACF;AAEA;;;;;;;CAOC,GACD,OAAO,SAASU,iBAAiBC,KAAa;IAC5C,MAAMC,UAAU9C,OAAO6C,gBAAAA,QAAS,IAC7BE,OAAO,CAAC,aAAa,IACrB9C,IAAI;IACP,IAAI,CAAC6C,SAAS,OAAO;IACrB,MAAME,QAAQX,OAAOS;IACrB,IAAI,CAACT,OAAOY,QAAQ,CAACD,UAAUA,QAAQ,GAAG,OAAO;IACjD,OAAOE,KAAKC,KAAK,CAACH,QAAQ;AAC5B;AAEA,iEAAiE,GACjE,OAAO,SAASI,iBAAiBpB,KAAgC;IAC/D,IAAI,OAAOA,UAAU,YAAY,CAACK,OAAOY,QAAQ,CAACjB,QAAQ,OAAO;IACjE,OAAO,AAACA,CAAAA,QAAQ,GAAE,EAAGW,OAAO,CAAC;AAC/B;AAEA;;;CAGC,GACD,OAAO,SAASU,YAAYL,KAAc;IACxC,IAAI,CAACA,OAAO,OAAO;IACnB,IAAI,OAAOA,UAAU,UAAU,OAAOX,OAAOY,QAAQ,CAACD,SAASA,QAAQ;IACvE,IAAIA,iBAAiB1B,MAAM,OAAO0B,MAAMM,OAAO;IAC/C,MAAMC,UAAU,AAACP,MAAgCO,OAAO;IACxD,IAAI,OAAOA,YAAY,UAAU,OAAOA,UAAU;IAClD,MAAMC,WAAW,AAACR,MAAsCQ,QAAQ;IAChE,IAAI,OAAOA,aAAa,YAAY,OAAOA,SAASC,IAAI,CAACT;IACzD,OAAO;AACT;AAEA,oEAAoE,GACpE,OAAO,SAASU,YACdC,IAAqD,EACrDpC,KAAa;IAEb,MAAMqC,QACJ,OAAOD,KAAKE,gBAAgB,KAAK,WAC7BF,KAAKE,gBAAgB,GACrBR,YAAYM,KAAKtC,SAAS;IAChC,IAAI,CAACuC,OAAO,OAAO;IACnB,OAAOV,KAAKY,GAAG,CAAC,GAAGZ,KAAKa,KAAK,CAAC,AAACxC,CAAAA,QAAQqC,KAAI,IAAK;AAClD;AAKA,SAASI,UACPC,MAAwB,EACxBhC,QAA4B,EAC5BD,KAAa;QAGGiC;IADhB,MAAM/B,OAAOlC,OAAOiC,YAAYP,uBAAuBtB,WAAW;IAClE6D,MAAM,CAAC/B,KAAK,GAAG,EAAC+B,eAAAA,MAAM,CAAC/B,KAAK,YAAZ+B,eAAgB,KAAKjC;AACvC;AAWA;;;;;;;;CAQC,GACD,OAAO,SAASkC,aACdC,KAGG,EACH5E,QAAwD;IAExD,MAAM6E,UAAwB;QAC5BC,WAAW;QACXC,iBAAiB,CAAC;QAClBC,oBAAoB,CAAC;IACvB;IACA,KAAK,MAAMZ,QAAQQ,MAAO;YAGqBR;QAF7C,IAAIA,KAAKa,MAAM,KAAK,QAAQ;QAC5BJ,QAAQC,SAAS,IAAI;QACrB,MAAMjC,SAASc,KAAKY,GAAG,CAAC,GAAGZ,KAAKC,KAAK,CAACd,QAAOsB,oBAAAA,KAAKc,WAAW,YAAhBd,oBAAoB,MAAM;QACvEK,UAAUI,QAAQE,eAAe,EAAEX,KAAK1B,QAAQ,EAAEG;QAClD4B,UACEI,QAAQG,kBAAkB,EAC1BZ,KAAK1B,QAAQ,EACbjD,wBAAwB2E,MAAM7E,cAAcS,UAAUoE,KAAKe,OAAO;IAEtE;IACA,OAAON;AACT;AAEA,oEAAoE,GACpE,OAAO,SAASO,uBAAuBV,MAAwB;IAC7D,MAAMW,UAAUC,OAAOD,OAAO,CAACX,QAAQa,IAAI,CAAC,CAACC,GAAGC,IAAMA,CAAC,CAAC,EAAE,GAAGD,CAAC,CAAC,EAAE;IACjE,IAAI,CAACH,QAAQpE,MAAM,EAAE,OAAOuB,YAAY,GAAGL;IAC3C,OAAOkD,QAAQ9D,GAAG,CAAC,CAAC,CAACoB,MAAMF,MAAM,GAAKD,YAAYC,OAAOE,OAAO+C,IAAI,CAAC;AACvE;AAEA,+DAA+D,GAC/D,OAAO,SAASC,aACd3F,QAAwD;;IAExD,OAAO;mBAAKA,4BAAAA,SAAUsB,MAAM,mBAAI,EAAE;KAAE,CACjCC,GAAG,CAAC,CAACC,OAAOoE,QAAW,CAAA;YAAEpE;YAAOoE;QAAM,CAAA,GACtCL,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEhE,KAAK,CAACqE,KAAK,GAAGJ,EAAEjE,KAAK,CAACqE,KAAK,IAAIL,EAAEI,KAAK,GAAGH,EAAEG,KAAK,EACjErE,GAAG,CAAC,CAAC,EAAEC,KAAK,EAAE,GAAKA;AACxB;AAEA,8EAA8E,GAC9E,OAAO,SAASsE,WACd9F,QAAwD;IAExD,OAAO2F,aAAa3F,UAAUD,MAAM,CAAC,CAACyB,QAAUA,MAAMuE,IAAI,KAAK;AACjE;AAEA,oFAAoF,GACpF,OAAO,SAASC,aACdhG,QAAwD,EACxD+F,IAAoB;QAEbJ;IAAP,QAAOA,qBAAAA,aAAa3F,UAAUG,IAAI,CAAC,CAACqB,QAAUA,MAAMuE,IAAI,KAAKA,iBAAtDJ,qBAA+D;AACxE;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASM,WACd3F,IAAY,EACZ4F,QAAiC;IAEjC,MAAMC,OACJ1F,OAAOH,eAAAA,OAAQ,IACZI,IAAI,GACJG,WAAW,GACX2C,OAAO,CAAC,eAAe,KACvBA,OAAO,CAAC,YAAY,IACpBnC,KAAK,CAAC,GAAG,OAAO;IACrB,MAAMV,QAAQ,IAAIyF,IAAIF,SAAS3E,GAAG,CAAC,CAACC,QAAUA,MAAM6E,EAAE;IACtD,IAAI,CAAC1F,MAAM2F,GAAG,CAACH,OAAO,OAAOA;IAC7B,IAAII,SAAS;IACb,MAAO5F,MAAM2F,GAAG,CAAC,GAAGH,KAAK,CAAC,EAAEI,QAAQ,EAAGA,UAAU;IACjD,OAAO,GAAGJ,KAAK,CAAC,EAAEI,QAAQ;AAC5B;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,eACdlF,MAA+B;IAE/B,OAAOA,OAAOC,GAAG,CAAC,CAACC,OAAOqE,QAAW,aAAKrE;YAAOqE;;AACnD;AAEA,mEAAmE,GACnE,OAAO,SAASY,iBAAiBhD,KAAc;IAC7C,MAAMiD,SAAS/C,KAAKC,KAAK,CAACd,OAAOW;IACjC,IAAI,CAACX,OAAOY,QAAQ,CAACgD,SAAS,OAAO;IACrC,OAAO/C,KAAKgD,GAAG,CAAC,KAAKhD,KAAKY,GAAG,CAAC,GAAGmC;AACnC;AAEA;;;;;CAKC,GACD,OAAO,SAASE,SACdtF,MAA+B,EAC/BhB,IAAY,EACZuG,cAAc,EAAE;IAEhB,MAAMrG,UAAUC,OAAOH,eAAAA,OAAQ,IAAII,IAAI;IACvC,IAAI,CAACF,SAAS,OAAO;WAAIc;KAAO;IAChC,MAAMwF,SAASnB,aAAa;QAAErE,QAAQ;eAAIA;SAAO;IAAC;IAClD,MAAMyF,cAAcD,OAAOE,SAAS,CAAC,CAACxF,QAAUA,MAAMuE,IAAI,KAAK;IAC/D,MAAMkB,KAAKF,gBAAgB,CAAC,IAAID,OAAO7F,MAAM,GAAG8F;IAChD,MAAMvF,QAAsB;QAC1B6E,IAAIJ,WAAWzF,SAASsG;QACxBxG,MAAME;QACNqF,OAAOoB;QACPJ,aAAaJ,iBAAiBI;QAC9Bd,MAAM;IACR;IACAe,OAAOI,MAAM,CAACD,IAAI,GAAGzF;IACrB,OAAOgF,eAAeM;AACxB;AAEA,OAAO,SAASK,YACd7F,MAA+B,EAC/B6D,OAAe,EACf7E,IAAY;IAEZ,MAAME,UAAUC,OAAOH,eAAAA,OAAQ,IAAII,IAAI;IACvC,OAAOY,OAAOC,GAAG,CAAC,CAACC,QACjBA,MAAM6E,EAAE,KAAKlB,WAAW3E,UAAU,aAAKgB;YAAOlB,MAAME;aAAYgB;AAEpE;AAEA;;;;CAIC,GACD,OAAO,SAAS4F,oBACd9F,MAA+B,EAC/B6D,OAAe,EACf0B,WAAoB;IAEpB,OAAOvF,OAAOC,GAAG,CAAC,CAACC,QACjBA,MAAM6E,EAAE,KAAKlB,WAAW3D,MAAMuE,IAAI,KAAK,SACnC,aAAKvE;YAAOqF,aAAaJ,iBAAiBI;aAC1CrF;AAER;AAEA;;;CAGC,GACD,OAAO,SAAS6F,UACd/F,MAA+B,EAC/B6D,OAAe,EACfmC,SAAwB;IAExB,MAAMR,SAASnB,aAAa;QAAErE,QAAQ;eAAIA;SAAO;IAAC;IAClD,MAAMsE,QAAQkB,OAAOE,SAAS,CAAC,CAACxF,QAAUA,MAAM6E,EAAE,KAAKlB;IACvD,IAAIS,UAAU,CAAC,KAAKkB,MAAM,CAAClB,MAAM,CAACG,IAAI,KAAK,QAAQ,OAAOe;IAC1D,MAAMS,SAASD,cAAc,OAAO1B,QAAQ,IAAIA,QAAQ;IACxD,IAAI2B,SAAS,KAAKA,UAAUT,OAAO7F,MAAM,EAAE,OAAO6F;IAClD,IAAIA,MAAM,CAACS,OAAO,CAACxB,IAAI,KAAK,QAAQ,OAAOe;IAC3C,MAAMU,UAAU;WAAIV;KAAO;IAC3B,MAAMW,SAASD,OAAO,CAAC5B,MAAM;IAC7B4B,OAAO,CAAC5B,MAAM,GAAG4B,OAAO,CAACD,OAAO;IAChCC,OAAO,CAACD,OAAO,GAAGE;IAClB,OAAOjB,eAAegB;AACxB;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASE,oBACdpG,MAA+B,EAC/B6D,OAAe,EACfwC,YAAoB;IAEpB,MAAMnG,QAAQF,OAAOnB,IAAI,CAAC,CAACa,QAAUA,MAAMqF,EAAE,KAAKlB;IAClD,IAAI,CAAC3D,OAAO,OAAO;IACnB,IAAIA,MAAMuE,IAAI,KAAK,QAAQ;QACzB,OAAO,CAAC,yBAAyB,EAAEvE,MAAMuE,IAAI,KAAK,QAAQ,QAAQ,OAAO,OAAO,CAAC;IACnF;IACA,IAAID,WAAW;QAAExE,QAAQ;eAAIA;SAAO;IAAC,GAAGL,MAAM,IAAI,GAAG;QACnD,OAAO;IACT;IACA,IAAI0G,eAAe,GAAG;QACpB,OACE,GAAGA,aAAazG,cAAc,GAAG,CAAC,EAAEyG,iBAAiB,IAAI,YAAY,YAAY,CAAC,CAAC,GACnF;IAEJ;IACA,OAAO;AACT;AAEA,OAAO,SAASC,YACdtG,MAA+B,EAC/B6D,OAAe;IAEf,OAAOqB,eAAelF,OAAOvB,MAAM,CAAC,CAACyB,QAAUA,MAAM6E,EAAE,KAAKlB;AAC9D;AAEA;;;;;CAKC,GACD,OAAO,SAAS0C,cAAcvG,MAA+B;IAC3D,IAAI,CAACA,OAAOL,MAAM,EAAE,OAAO;IAC3B,MAAM6G,MAAM,IAAI1B;IAChB,KAAK,MAAM5E,SAASF,OAAQ;YAIdE;QAHZ,IAAI,CAACA,MAAM6E,EAAE,EAAE,OAAO;QACtB,IAAIyB,IAAIxB,GAAG,CAAC9E,MAAM6E,EAAE,GAAG,OAAO,CAAC,yBAAyB,EAAE7E,MAAM6E,EAAE,CAAC,EAAE,CAAC;QACtEyB,IAAIC,GAAG,CAACvG,MAAM6E,EAAE;QAChB,IAAI,CAAC5F,QAAOe,cAAAA,MAAMlB,IAAI,YAAVkB,cAAc,IAAId,IAAI,IAAI,OAAO;IAC/C;IACA,MAAM4B,MAAMhB,OAAOvB,MAAM,CAAC,CAACyB,QAAUA,MAAMuE,IAAI,KAAK,OAAO9E,MAAM;IACjE,MAAMsB,OAAOjB,OAAOvB,MAAM,CAAC,CAACyB,QAAUA,MAAMuE,IAAI,KAAK,QAAQ9E,MAAM;IACnE,IAAIqB,QAAQ,GAAG,OAAO;IACtB,IAAIC,SAAS,GAAG,OAAO;IACvB,IAAI,CAACjB,OAAO0G,IAAI,CAAC,CAACxG,QAAUA,MAAMuE,IAAI,KAAK,SAAS;QAClD,OAAO;IACT;IACA,OAAO;AACT;AAUA,OAAO,SAASkC,cAAchD,MAAyB;IACrD,IAAIA,WAAW,OAAO,OAAO;IAC7B,IAAIA,WAAW,QAAQ,OAAO;IAC9B,OAAO;AACT;AAEA,OAAO,SAASiD,iBACdC,MAAc,EACd/D,IAUC,EACDgE,eAAuB;QAIPhE,aAC6BA,mBAE3BA,eAECA,gBACCA,iBACAA;IATpB,OAAO;QACL+D;QACAE,OAAO5H,QAAO2D,cAAAA,KAAKiE,KAAK,YAAVjE,cAAc;QAC5Bc,aAAavB,KAAKY,GAAG,CAAC,GAAGZ,KAAKC,KAAK,CAACd,QAAOsB,oBAAAA,KAAKc,WAAW,YAAhBd,oBAAoB,MAAM;QACrE1B,UAAUjC,OAAO2D,KAAK1B,QAAQ,IAAIP,uBAAuBtB,WAAW;QACpEsE,SAAS1E,QAAO2D,gBAAAA,KAAKe,OAAO,YAAZf,gBAAgB;QAChCgE,iBAAiB3H,OAAO2H,0BAAAA,kBAAmB;QAC3CE,UAAU7H,QAAO2D,iBAAAA,KAAKkE,QAAQ,YAAblE,iBAAiB;QAClCmE,WAAW9H,QAAO2D,kBAAAA,KAAKmE,SAAS,YAAdnE,kBAAkB;QACpCoE,WAAW/H,QAAO2D,kBAAAA,KAAKoE,SAAS,YAAdpE,kBAAkB;OAChCA,KAAKqE,UAAU,GAAG;QAAEA,YAAYhI,OAAO2D,KAAKqE,UAAU;IAAE,IAAI,CAAC;AAErE;AAEA,sEAAsE,GACtE,OAAO,SAASC,YAAYC,KAAa;IACvC,MAAMC,QAAQnI,OAAOkI,gBAAAA,QAAS,IAC3BjI,IAAI,GACJmI,KAAK,CAAC,aACN9I,MAAM,CAAC+I;IACV,MAAMC,UAAUH,MACbvH,KAAK,CAAC,GAAG,GACTE,GAAG,CAAC,CAACyH,OAASA,IAAI,CAAC,EAAE,EACrBtD,IAAI,CAAC;IACR,OAAO,AAACqD,CAAAA,WAAW,GAAE,EAAGnG,WAAW;AACrC;AAEA;;;;CAIC,GACD,OAAO,SAASqG,OAAON,KAAa;IAClC,IAAIO,OAAO;IACX,IAAK,IAAItD,QAAQ,GAAGA,QAAQ+C,MAAM1H,MAAM,EAAE2E,SAAS,EAAG;QACpDsD,OAAO,AAACA,CAAAA,OAAO,KAAKP,MAAMQ,UAAU,CAACvD,MAAK,IAAK;IACjD;IACA,OAAOsD;AACT;AAEA,iEAAiE,GACjE,OAAO,SAASE,eAAeC,EAA6B;IAC1D,IAAI,OAAOA,OAAO,YAAY,CAACvG,OAAOY,QAAQ,CAAC2F,KAAK,OAAO;IAC3D,MAAMpC,KAAK,IAAIlF,KAAKsH;IACpB,MAAMC,SAASrC,GAAGsC,iBAAiB,KAAK;IACxC,OAAO,IAAIxH,KAAKsH,KAAKC,QAAQE,WAAW,GAAGnI,KAAK,CAAC,GAAG;AACtD;AAEA;;;;;;CAMC,GACD,OAAO,SAASoI,YAAYhG,KAAa;IACvC,MAAMiG,QAAQ,4BAA4BC,IAAI,CAAClJ,OAAOgD,gBAAAA,QAAS,IAAI/C,IAAI;IACvE,IAAI,CAACgJ,OAAO,OAAO;IACnB,MAAMzC,KAAK,IAAIlF,KACbe,OAAO4G,KAAK,CAAC,EAAE,GACf5G,OAAO4G,KAAK,CAAC,EAAE,IAAI,GACnB5G,OAAO4G,KAAK,CAAC,EAAE,GACf,IACA,GACA,GACA;IAEF,OAAO5G,OAAO8G,KAAK,CAAC3C,GAAGlD,OAAO,MAAM,OAAOkD,GAAGlD,OAAO;AACvD"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/deal-board-model.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 pipeline's arithmetic and the stage editor's rules (AGL-2598), as pure\n * functions — no Firestore, no React — so the board, the table, the detail\n * page and the server route read one definition of \"what is this pipeline\n * worth\" and \"which stage edits are legal\".\n */\n\n// The module itself and never the `@aglyn/aglyn` barrel: this file is\n// reached from the server entry through the deal-stage route, and the barrel\n// carries client-only context modules the App Router graph refuses on a\n// server path (AGL-405/1349).\nimport {\n type CrmDeal,\n type CrmDealStage,\n type CrmForecastCategory,\n type CrmPipeline,\n DEFAULT_DEAL_STAGES,\n dealStageById,\n isCrmForecastCategory,\n isPipelineArchived,\n weightedDealAmountCents,\n} from '@aglyn/aglyn/app-utils/crm'\n\n/** A deal as the console reads it: the model plus its document id. */\nexport type DealDoc = CrmDeal & {\n $id: string\n /**\n * The contact's and the company's names, copied onto the deal when the\n * link is made. A board of two hundred cards cannot afford a keyed read per\n * card to caption each one, and the caption is allowed to lag a rename —\n * the link itself is the id, which never changes.\n */\n contactName?: string\n companyName?: string\n}\n\n/** A pipeline as the console reads it. */\nexport type PipelineDoc = CrmPipeline & { $id: string }\n\n/** The document id the seeded default pipeline is written under. */\nexport const DEFAULT_PIPELINE_ID = 'default'\n\n/** Name of the pipeline every org starts with. */\nexport const DEFAULT_PIPELINE_NAME = 'Sales'\n\nexport const PIPELINE_NAME_MAX = 60\n\n/** The pipelines a picker offers and a new deal may land in: the ones not archived. */\nexport function activePipelines<T extends Pick<CrmPipeline, 'archivedAt'>>(\n pipelines: readonly T[],\n): T[] {\n return pipelines.filter((pipeline) => !isPipelineArchived(pipeline))\n}\n\n/**\n * The pipeline a new deal lands in when nobody picks one: the active one\n * marked default, else the first active one, else nothing. An archived\n * pipeline is never the default even if its flag was left on — archiving\n * clears the flag, but a document written by an older console may not\n * have had it cleared, and a default nobody can create a deal in is a\n * board that opens on a pipeline the picker does not list.\n */\nexport function defaultPipelineOf<T extends Pick<CrmPipeline, 'archivedAt' | 'isDefault'>>(\n pipelines: readonly T[],\n): T | null {\n const active = activePipelines(pipelines)\n return active.find((pipeline) => pipeline.isDefault) ?? active[0] ?? null\n}\n\n/**\n * Why a pipeline cannot be created or renamed to `name`, or `null`.\n *\n * Names are unique among the ACTIVE pipelines, compared without case: the\n * picker lists them by name alone, and two rows reading \"Sales\" would be a\n * choice nobody can make. An archived pipeline's name is free to reuse —\n * \"Sales 2025\" retired and \"Sales\" started over is the point of archiving.\n */\nexport function pipelineNameProblem(\n name: string,\n pipelines: ReadonlyArray<Pick<CrmPipeline, 'name' | 'archivedAt'> & { $id: string }>,\n ownId?: string,\n): string | null {\n const trimmed = String(name ?? '').trim()\n if (!trimmed) return 'A pipeline needs a name.'\n const taken = activePipelines(pipelines).find(\n (pipeline) =>\n pipeline.$id !== ownId &&\n String(pipeline.name ?? '').trim().toLowerCase() === trimmed.toLowerCase(),\n )\n if (taken) return `Another pipeline is already called \"${taken.name}\".`\n return null\n}\n\n/**\n * Why a pipeline cannot be archived, or `null` when it can.\n *\n * `openDeals` is the server's count, asked for before this is called, the\n * way `stageRemovalRefusal` asks: an open deal in an archived pipeline is\n * a card on no board, and the count is taken at the click because a deal\n * can be created into the pipeline from another tab meanwhile. The default\n * pipeline cannot go while it is the default — a new deal has to land\n * somewhere — and the last active one cannot go at all, for the same\n * reason from the other side.\n */\nexport function pipelineArchiveRefusal(\n pipeline: Pick<CrmPipeline, 'archivedAt' | 'isDefault'> & { $id: string },\n pipelines: ReadonlyArray<Pick<CrmPipeline, 'archivedAt'> & { $id: string }>,\n openDeals: number,\n): string | null {\n if (isPipelineArchived(pipeline)) return 'This pipeline is already archived.'\n if (activePipelines(pipelines).filter((entry) => entry.$id !== pipeline.$id).length === 0) {\n return 'The last pipeline cannot be archived.'\n }\n if (pipeline.isDefault) {\n return 'This is the default pipeline. Make another one the default first.'\n }\n if (openDeals > 0) {\n return (\n `${openDeals.toLocaleString()} open ${openDeals === 1 ? 'deal is' : 'deals are'} ` +\n 'in this pipeline. Close or move them first, so none is left on a board nobody opens.'\n )\n }\n return null\n}\n\n/** What every pipeline write stamps besides its name. */\nexport interface PipelineWriteContext {\n visibleTo: readonly string[]\n hostId: string\n uid: string\n nowMs: number\n}\n\n/**\n * The document a new pipeline is created as: the name, a COPY of the\n * default stages, not the default, active. A copy for the reason\n * `DEFAULT_DEAL_STAGES` gives — a second pipeline seeded later must start\n * from the original set, whatever the first one's editor did to its own.\n */\nexport function newPipelineDocument(\n name: string,\n context: PipelineWriteContext,\n): Record<string, unknown> {\n return {\n name: String(name ?? '').trim().slice(0, PIPELINE_NAME_MAX),\n stages: DEFAULT_DEAL_STAGES.map((stage) => ({ ...stage })),\n isDefault: false,\n archivedAt: null,\n visibleTo: [...context.visibleTo],\n hostId: context.hostId,\n createdByUid: context.uid,\n createdAt: new Date(context.nowMs),\n updatedAt: new Date(context.nowMs),\n }\n}\n\n/**\n * Currencies the amount picker offers, lowercase ISO 4217 as the model\n * stores them. Short on purpose: a picker of a hundred and eighty currencies\n * is a scroll, and a deal in one outside this list is entered through the\n * API with whatever code it carries — the display below formats any code\n * `Intl` knows.\n */\nexport const DEAL_CURRENCIES = [\n 'usd',\n 'eur',\n 'gbp',\n 'cad',\n 'aud',\n 'nzd',\n 'chf',\n 'jpy',\n 'sek',\n 'nok',\n 'dkk',\n 'mxn',\n 'brl',\n 'inr',\n 'sgd',\n] as const\n\nexport const DEFAULT_DEAL_CURRENCY = 'usd'\n\n/** How a deal's `status` reads on screen. */\nexport const DEAL_STATUS_LABELS: Record<CrmDeal['status'], string> = {\n open: 'Open',\n won: 'Won',\n lost: 'Lost',\n}\n\n/** Cents → the reader's locale, or a plain number when the code is unknown. */\nexport function formatMoney(cents: number, currency: string | undefined): string {\n const code = String(currency || DEFAULT_DEAL_CURRENCY).toUpperCase()\n const amount = (Number(cents) || 0) / 100\n try {\n return new Intl.NumberFormat(undefined, {\n style: 'currency',\n currency: code,\n }).format(amount)\n } catch {\n return `${amount.toFixed(2)} ${code}`\n }\n}\n\n/**\n * A typed amount, as cents, or `null` when it is not a number.\n *\n * Accepts what people paste from an invoice — a thousands separator, a\n * currency symbol, surrounding space — and refuses a negative: a deal is\n * worth zero or more, and a negative amount here is a typo rather than a\n * refund. Rounded rather than truncated so `19.995` does not lose a cent.\n */\nexport function parseAmountInput(input: string): number | null {\n const cleaned = String(input ?? '')\n .replace(/[^0-9.-]/g, '')\n .trim()\n if (!cleaned) return null\n const value = Number(cleaned)\n if (!Number.isFinite(value) || value < 0) return null\n return Math.round(value * 100)\n}\n\n/** Cents → the string the amount field shows, `''` for nothing. */\nexport function amountInputValue(cents: number | undefined | null): string {\n if (typeof cents !== 'number' || !Number.isFinite(cents)) return ''\n return (cents / 100).toFixed(2)\n}\n\n/**\n * Epoch ms from whatever a timestamp field holds after a round trip — a\n * Firestore `Timestamp`, a `Date` a client just wrote, or a number.\n */\nexport function timestampMs(value: unknown): number | null {\n if (!value) return null\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (value instanceof Date) return value.getTime()\n const seconds = (value as { seconds?: unknown }).seconds\n if (typeof seconds === 'number') return seconds * 1000\n const toMillis = (value as { toMillis?: () => number }).toMillis\n if (typeof toMillis === 'function') return toMillis.call(value)\n return null\n}\n\n/** Whole days a deal has sat in its current stage, never negative. */\nexport function daysInStage(\n deal: Pick<CrmDeal, 'stageChangedAtMs' | 'createdAt'>,\n nowMs: number,\n): number {\n const since =\n typeof deal.stageChangedAtMs === 'number'\n ? deal.stageChangedAtMs\n : timestampMs(deal.createdAt)\n if (!since) return 0\n return Math.max(0, Math.floor((nowMs - since) / 86_400_000))\n}\n\n/** A sum per currency, because two currencies never add into one number. */\nexport type AmountByCurrency = Record<string, number>\n\nfunction addAmount(\n totals: AmountByCurrency,\n currency: string | undefined,\n cents: number,\n): void {\n const code = String(currency || DEFAULT_DEAL_CURRENCY).toLowerCase()\n totals[code] = (totals[code] ?? 0) + cents\n}\n\nexport interface BoardSummary {\n /** Deals whose `status` is `open`, whatever stage they sit in. */\n openCount: number\n /** Every open deal's full amount, per currency. */\n valueByCurrency: AmountByCurrency\n /** Every open deal's amount times its stage's probability, per currency. */\n weightedByCurrency: AmountByCurrency\n}\n\n/**\n * The three figures above the board.\n *\n * Closed deals are excluded from both totals rather than counted at full or\n * zero value: the summary describes what is still in play, and a won deal\n * counted in \"pipeline value\" would make the pipeline look larger every time\n * something closed. `weightedDealAmountCents` already answers zero for a\n * stage the pipeline no longer has.\n */\nexport function boardSummary(\n deals: readonly (Pick<CrmDeal, 'status' | 'amountCents' | 'currency' | 'stageId'> &\n Partial<Pick<CrmDeal, 'probability'>>)[],\n pipeline: Pick<CrmPipeline, 'stages'> | null | undefined,\n): BoardSummary {\n const summary: BoardSummary = {\n openCount: 0,\n valueByCurrency: {},\n weightedByCurrency: {},\n }\n for (const deal of deals) {\n if (deal.status !== 'open') continue\n summary.openCount += 1\n const amount = Math.max(0, Math.round(Number(deal.amountCents ?? 0) || 0))\n addAmount(summary.valueByCurrency, deal.currency, amount)\n addAmount(\n summary.weightedByCurrency,\n deal.currency,\n weightedDealAmountCents(deal, dealStageById(pipeline, deal.stageId)),\n )\n }\n return summary\n}\n\n/** `$1,200.00 · €300.00` — one figure per currency, largest first. */\nexport function formatAmountByCurrency(totals: AmountByCurrency): string {\n const entries = Object.entries(totals).sort((a, b) => b[1] - a[1])\n if (!entries.length) return formatMoney(0, DEFAULT_DEAL_CURRENCY)\n return entries.map(([code, cents]) => formatMoney(cents, code)).join(' · ')\n}\n\n/** The pipeline's stages in `order`, ties broken by position. */\nexport function sortedStages(\n pipeline: Pick<CrmPipeline, 'stages'> | null | undefined,\n): CrmDealStage[] {\n return [...(pipeline?.stages ?? [])]\n .map((stage, index) => ({ stage, index }))\n .sort((a, b) => a.stage.order - b.stage.order || a.index - b.index)\n .map(({ stage }) => stage)\n}\n\n/** The stages a deal can be dragged between — everything that is not closed. */\nexport function openStages(\n pipeline: Pick<CrmPipeline, 'stages'> | null | undefined,\n): CrmDealStage[] {\n return sortedStages(pipeline).filter((stage) => stage.kind === 'open')\n}\n\n/** The stage whose `kind` is `won` or `lost`, or null when the pipeline lacks one. */\nexport function closingStage(\n pipeline: Pick<CrmPipeline, 'stages'> | null | undefined,\n kind: 'won' | 'lost',\n): CrmDealStage | null {\n return sortedStages(pipeline).find((stage) => stage.kind === kind) ?? null\n}\n\n/**\n * The id a new stage is stored under: the name as a slug, made unique\n * against the stages already there.\n *\n * A slug rather than a random id because the id appears in every deal that\n * sits in the stage and in every automation filter that names it, so a\n * reader looking at `stageId == \"demo-booked\"` should be able to tell which\n * stage that is without opening the pipeline. Uniqueness is what keeps a\n * second \"Demo\" from silently becoming the first.\n */\nexport function newStageId(\n name: string,\n existing: readonly CrmDealStage[],\n): string {\n const base =\n String(name ?? '')\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 40) || 'stage'\n const taken = new Set(existing.map((stage) => stage.id))\n if (!taken.has(base)) return base\n let suffix = 2\n while (taken.has(`${base}-${suffix}`)) suffix += 1\n return `${base}-${suffix}`\n}\n\n/**\n * Stages re-numbered 0..n in the order they are GIVEN.\n *\n * Positional on purpose: every caller hands this an array it has already\n * arranged — a swap, a splice, a filter over the sorted stages — and the\n * stored `order` on each entry is the arrangement being replaced. Sorting by\n * it here would put a swapped pair straight back where it was.\n */\nexport function renumberStages(\n stages: readonly CrmDealStage[],\n): CrmDealStage[] {\n return stages.map((stage, order) => ({ ...stage, order }))\n}\n\n/** 0–100, whole numbers; anything else becomes the nearest bound. */\nexport function clampProbability(value: unknown): number {\n const number = Math.round(Number(value))\n if (!Number.isFinite(number)) return 0\n return Math.min(100, Math.max(0, number))\n}\n\n/**\n * Add an open stage in front of the closing stages.\n *\n * Won and Lost stay last: a pipeline reads left to right as a progression,\n * and a stage added after \"Won\" would be a step past the end.\n */\nexport function addStage(\n stages: readonly CrmDealStage[],\n name: string,\n probability = 50,\n): CrmDealStage[] {\n const trimmed = String(name ?? '').trim()\n if (!trimmed) return [...stages]\n const sorted = sortedStages({ stages: [...stages] })\n const firstClosed = sorted.findIndex((stage) => stage.kind !== 'open')\n const at = firstClosed === -1 ? sorted.length : firstClosed\n const stage: CrmDealStage = {\n id: newStageId(trimmed, sorted),\n name: trimmed,\n order: at,\n probability: clampProbability(probability),\n kind: 'open',\n forecastCategory: 'pipeline',\n }\n sorted.splice(at, 0, stage)\n return renumberStages(sorted)\n}\n\nexport function renameStage(\n stages: readonly CrmDealStage[],\n stageId: string,\n name: string,\n): CrmDealStage[] {\n const trimmed = String(name ?? '').trim()\n return stages.map((stage) =>\n stage.id === stageId && trimmed ? { ...stage, name: trimmed } : stage,\n )\n}\n\n/**\n * Set an OPEN stage's probability. Won is 100 and Lost is 0 by definition —\n * `weightedDealAmountCents` reads the status before the stage, so a Won\n * stage at 60% would change nothing and only mislead the editor.\n */\nexport function setStageProbability(\n stages: readonly CrmDealStage[],\n stageId: string,\n probability: unknown,\n): CrmDealStage[] {\n return stages.map((stage) =>\n stage.id === stageId && stage.kind === 'open'\n ? { ...stage, probability: clampProbability(probability) }\n : stage,\n )\n}\n\n/**\n * Set an OPEN stage's forecast category (AGL-3516). Won is Closed and Lost\n * is Omitted by definition, for the reason {@link setStageProbability}\n * gives; a deal's own category is changed on the deal.\n */\nexport function setStageForecastCategory(\n stages: readonly CrmDealStage[],\n stageId: string,\n category: CrmForecastCategory | string,\n): CrmDealStage[] {\n if (!isCrmForecastCategory(category)) return [...stages]\n return stages.map((stage) =>\n stage.id === stageId && stage.kind === 'open'\n ? { ...stage, forecastCategory: category }\n : stage,\n )\n}\n\n/**\n * Swap an open stage with its open neighbor. The closing stages are not in\n * the reorderable range, so an open stage can never be moved past Won.\n */\nexport function moveStage(\n stages: readonly CrmDealStage[],\n stageId: string,\n direction: 'up' | 'down',\n): CrmDealStage[] {\n const sorted = sortedStages({ stages: [...stages] })\n const index = sorted.findIndex((stage) => stage.id === stageId)\n if (index === -1 || sorted[index].kind !== 'open') return sorted\n const target = direction === 'up' ? index - 1 : index + 1\n if (target < 0 || target >= sorted.length) return sorted\n if (sorted[target].kind !== 'open') return sorted\n const swapped = [...sorted]\n const moving = swapped[index]\n swapped[index] = swapped[target]\n swapped[target] = moving\n return renumberStages(swapped)\n}\n\n/**\n * Why a stage cannot be removed, or `null` when it can.\n *\n * `dealsInStage` is the server's count, asked for before this is called: a\n * stage with deals in it cannot go, because every one of those deals would\n * be left pointing at a stage that no longer exists and worth nothing to the\n * forecast. The closing stages cannot go at all — a deal has to be able to\n * be won and lost.\n */\nexport function stageRemovalRefusal(\n stages: readonly CrmDealStage[],\n stageId: string,\n dealsInStage: number,\n): string | null {\n const stage = stages.find((entry) => entry.id === stageId)\n if (!stage) return 'This stage is no longer in the pipeline.'\n if (stage.kind !== 'open') {\n return `Every pipeline keeps its ${stage.kind === 'won' ? 'Won' : 'Lost'} stage.`\n }\n if (openStages({ stages: [...stages] }).length <= 1) {\n return 'A pipeline needs at least one open stage.'\n }\n if (dealsInStage > 0) {\n return (\n `${dealsInStage.toLocaleString()} ${dealsInStage === 1 ? 'deal is' : 'deals are'} ` +\n 'in this stage. Move them first, so none is left in a stage that no longer exists.'\n )\n }\n return null\n}\n\nexport function removeStage(\n stages: readonly CrmDealStage[],\n stageId: string,\n): CrmDealStage[] {\n return renumberStages(stages.filter((stage) => stage.id !== stageId))\n}\n\n/**\n * What a stored set of stages has to satisfy before it is written back:\n * unique ids, a name on every stage, exactly one Won and one Lost, at least\n * one open stage. The editor refuses a save that fails this rather than\n * writing a pipeline the board cannot draw.\n */\nexport function stagesProblem(stages: readonly CrmDealStage[]): string | null {\n if (!stages.length) return 'A pipeline needs stages.'\n const ids = new Set<string>()\n for (const stage of stages) {\n if (!stage.id) return 'Every stage needs an id.'\n if (ids.has(stage.id)) return `Two stages share the id \"${stage.id}\".`\n ids.add(stage.id)\n if (!String(stage.name ?? '').trim()) return 'Every stage needs a name.'\n }\n const won = stages.filter((stage) => stage.kind === 'won').length\n const lost = stages.filter((stage) => stage.kind === 'lost').length\n if (won !== 1) return 'A pipeline has exactly one Won stage.'\n if (lost !== 1) return 'A pipeline has exactly one Lost stage.'\n if (!stages.some((stage) => stage.kind === 'open')) {\n return 'A pipeline needs at least one open stage.'\n }\n return null\n}\n\n/**\n * The event a stage change emits, and the flat payload a workflow filter\n * reads (AGL-2598). Strings and numbers only — a workflow expression\n * evaluates over scalars, and an absent link is the empty string rather than\n * a missing key so `contactId == \"\"` is a filter somebody can write.\n */\nexport type DealEventName = 'dealStageChanged' | 'dealWon' | 'dealLost'\n\nexport function dealEventName(status: CrmDeal['status']): DealEventName {\n if (status === 'won') return 'dealWon'\n if (status === 'lost') return 'dealLost'\n return 'dealStageChanged'\n}\n\nexport function dealEventPayload(\n dealId: string,\n deal: Pick<\n CrmDeal,\n | 'title'\n | 'amountCents'\n | 'currency'\n | 'stageId'\n | 'ownerUid'\n | 'contactId'\n | 'companyId'\n | 'lostReason'\n >,\n previousStageId: string,\n): Record<string, string | number> {\n return {\n dealId,\n title: String(deal.title ?? ''),\n amountCents: Math.max(0, Math.round(Number(deal.amountCents ?? 0) || 0)),\n currency: String(deal.currency || DEFAULT_DEAL_CURRENCY).toLowerCase(),\n stageId: String(deal.stageId ?? ''),\n previousStageId: String(previousStageId ?? ''),\n ownerUid: String(deal.ownerUid ?? ''),\n contactId: String(deal.contactId ?? ''),\n companyId: String(deal.companyId ?? ''),\n ...(deal.lostReason ? { lostReason: String(deal.lostReason) } : {}),\n }\n}\n\n/** Initials for an avatar: the first letters of the first two words. */\nexport function initialsFor(label: string): string {\n const words = String(label ?? '')\n .trim()\n .split(/[\\s@._-]+/)\n .filter(Boolean)\n const letters = words\n .slice(0, 2)\n .map((word) => word[0])\n .join('')\n return (letters || '?').toUpperCase()\n}\n\n/**\n * A stable hue for one person, so an owner keeps one color across cards and\n * sessions. Hashing the label rather than picking from a palette is what\n * makes the same uid the same color on every machine.\n */\nexport function hueFor(label: string): number {\n let hash = 0\n for (let index = 0; index < label.length; index += 1) {\n hash = (hash * 31 + label.charCodeAt(index)) % 360\n }\n return hash\n}\n\n/** `2026-09-05` for a date input, from epoch ms; `''` for none. */\nexport function dateInputValue(ms: number | null | undefined): string {\n if (typeof ms !== 'number' || !Number.isFinite(ms)) return ''\n const at = new Date(ms)\n const offset = at.getTimezoneOffset() * 60_000\n return new Date(ms - offset).toISOString().slice(0, 10)\n}\n\n/**\n * Epoch ms at local noon of a typed `YYYY-MM-DD`, or `null`.\n *\n * Noon rather than midnight so the day survives a timezone round trip: a\n * close date stored at local midnight and read back one timezone west shows\n * the day before.\n */\nexport function dateInputMs(value: string): number | null {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(String(value ?? '').trim())\n if (!match) return null\n const at = new Date(\n Number(match[1]),\n Number(match[2]) - 1,\n Number(match[3]),\n 12,\n 0,\n 0,\n 0,\n )\n return Number.isNaN(at.getTime()) ? null : at.getTime()\n}\n"],"names":["DEFAULT_DEAL_STAGES","dealStageById","isCrmForecastCategory","isPipelineArchived","weightedDealAmountCents","DEFAULT_PIPELINE_ID","DEFAULT_PIPELINE_NAME","PIPELINE_NAME_MAX","activePipelines","pipelines","filter","pipeline","defaultPipelineOf","active","find","isDefault","pipelineNameProblem","name","ownId","trimmed","String","trim","taken","$id","toLowerCase","pipelineArchiveRefusal","openDeals","entry","length","toLocaleString","newPipelineDocument","context","slice","stages","map","stage","archivedAt","visibleTo","hostId","createdByUid","uid","createdAt","Date","nowMs","updatedAt","DEAL_CURRENCIES","DEFAULT_DEAL_CURRENCY","DEAL_STATUS_LABELS","open","won","lost","formatMoney","cents","currency","code","toUpperCase","amount","Number","Intl","NumberFormat","undefined","style","format","toFixed","parseAmountInput","input","cleaned","replace","value","isFinite","Math","round","amountInputValue","timestampMs","getTime","seconds","toMillis","call","daysInStage","deal","since","stageChangedAtMs","max","floor","addAmount","totals","boardSummary","deals","summary","openCount","valueByCurrency","weightedByCurrency","status","amountCents","stageId","formatAmountByCurrency","entries","Object","sort","a","b","join","sortedStages","index","order","openStages","kind","closingStage","newStageId","existing","base","Set","id","has","suffix","renumberStages","clampProbability","number","min","addStage","probability","sorted","firstClosed","findIndex","at","forecastCategory","splice","renameStage","setStageProbability","setStageForecastCategory","category","moveStage","direction","target","swapped","moving","stageRemovalRefusal","dealsInStage","removeStage","stagesProblem","ids","add","some","dealEventName","dealEventPayload","dealId","previousStageId","title","ownerUid","contactId","companyId","lostReason","initialsFor","label","words","split","Boolean","letters","word","hueFor","hash","charCodeAt","dateInputValue","ms","offset","getTimezoneOffset","toISOString","dateInputMs","match","exec","isNaN"],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type CrmDealContactRole, type CrmPicklist } from '@aglyn/aglyn';
18
+ /** Why a contact cannot be added to the deal, or `null` when it can. */
19
+ export declare function dealContactRoleAddProblem(roles: readonly CrmDealContactRole[], contactId: string): string | null;
20
+ /** `roles` with one contact added — as the Primary, when asked, which every other row then is not. */
21
+ export declare function dealContactRolesAdded(roles: readonly CrmDealContactRole[], added: {
22
+ contactId: string;
23
+ role?: string;
24
+ primary: boolean;
25
+ }): CrmDealContactRole[];
26
+ /** `roles` with one contact's role set — `''` for none. */
27
+ export declare function dealContactRolesWithRole(roles: readonly CrmDealContactRole[], contactId: string, role: string): CrmDealContactRole[];
28
+ /**
29
+ * What a save of the card writes: the roles judged against the org's list
30
+ * (a contact's current role kept), `contactId` as their Primary — `null`
31
+ * for a field the caller deletes — and whether the Primary changed, which
32
+ * is when the name the drawer copied beside `contactId` goes stale.
33
+ */
34
+ export declare function dealContactRolesSave(deal: {
35
+ contactId?: unknown;
36
+ contactRoles?: unknown;
37
+ }, next: readonly CrmDealContactRole[], picklist: CrmPicklist): {
38
+ ok: true;
39
+ contactRoles: CrmDealContactRole[];
40
+ contactId: string | null;
41
+ primaryChanged: boolean;
42
+ } | {
43
+ ok: false;
44
+ error: string;
45
+ };
@@ -0,0 +1,73 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { DEAL_CONTACT_ROLES_MAX, dealContactRoleFields, dealContactRolesOf, judgeDealContactRoles } from "@aglyn/aglyn";
18
+ /*
19
+ * THE DEAL PAGE'S CONTACT ROLES CARD (AGL-3521), as pure functions: what
20
+ * each edit makes of the list, and the fields one save writes.
21
+ */ /** Why a contact cannot be added to the deal, or `null` when it can. */ export function dealContactRoleAddProblem(roles, contactId) {
22
+ if (!contactId) return 'Pick a contact.';
23
+ if (roles.some((row)=>row.contactId === contactId)) return 'That contact is already on this deal.';
24
+ if (roles.length >= DEAL_CONTACT_ROLES_MAX) {
25
+ return `A deal names at most ${DEAL_CONTACT_ROLES_MAX} contacts.`;
26
+ }
27
+ return null;
28
+ }
29
+ /** `roles` with one contact added — as the Primary, when asked, which every other row then is not. */ export function dealContactRolesAdded(roles, added) {
30
+ const row = _extends({
31
+ contactId: added.contactId
32
+ }, added.role ? {
33
+ role: added.role
34
+ } : {}, {
35
+ primary: added.primary
36
+ });
37
+ return [
38
+ ...roles.map((entry)=>added.primary ? _extends({}, entry, {
39
+ primary: false
40
+ }) : entry),
41
+ row
42
+ ];
43
+ }
44
+ /** `roles` with one contact's role set — `''` for none. */ export function dealContactRolesWithRole(roles, contactId, role) {
45
+ return roles.map((row)=>{
46
+ if (row.contactId !== contactId) return row;
47
+ return _extends({
48
+ contactId: row.contactId
49
+ }, role ? {
50
+ role
51
+ } : {}, {
52
+ primary: row.primary
53
+ });
54
+ });
55
+ }
56
+ /**
57
+ * What a save of the card writes: the roles judged against the org's list
58
+ * (a contact's current role kept), `contactId` as their Primary — `null`
59
+ * for a field the caller deletes — and whether the Primary changed, which
60
+ * is when the name the drawer copied beside `contactId` goes stale.
61
+ */ export function dealContactRolesSave(deal, next, picklist) {
62
+ const judged = judgeDealContactRoles(picklist, next, dealContactRolesOf(deal));
63
+ if (judged.ok === false) return judged;
64
+ const fields = dealContactRoleFields(judged.roles);
65
+ const before = typeof deal.contactId === 'string' && deal.contactId ? deal.contactId : null;
66
+ return _extends({
67
+ ok: true
68
+ }, fields, {
69
+ primaryChanged: fields.contactId !== before
70
+ });
71
+ }
72
+
73
+ //# sourceMappingURL=deal-contact-roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/deal-contact-roles.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 {\n type CrmDealContactRole,\n type CrmPicklist,\n DEAL_CONTACT_ROLES_MAX,\n dealContactRoleFields,\n dealContactRolesOf,\n judgeDealContactRoles,\n} from '@aglyn/aglyn'\n\n/*\n * THE DEAL PAGE'S CONTACT ROLES CARD (AGL-3521), as pure functions: what\n * each edit makes of the list, and the fields one save writes.\n */\n\n/** Why a contact cannot be added to the deal, or `null` when it can. */\nexport function dealContactRoleAddProblem(\n roles: readonly CrmDealContactRole[],\n contactId: string,\n): string | null {\n if (!contactId) return 'Pick a contact.'\n if (roles.some((row) => row.contactId === contactId)) return 'That contact is already on this deal.'\n if (roles.length >= DEAL_CONTACT_ROLES_MAX) {\n return `A deal names at most ${DEAL_CONTACT_ROLES_MAX} contacts.`\n }\n return null\n}\n\n/** `roles` with one contact added — as the Primary, when asked, which every other row then is not. */\nexport function dealContactRolesAdded(\n roles: readonly CrmDealContactRole[],\n added: { contactId: string; role?: string; primary: boolean },\n): CrmDealContactRole[] {\n const row: CrmDealContactRole = {\n contactId: added.contactId,\n ...(added.role ? { role: added.role } : {}),\n primary: added.primary,\n }\n return [...roles.map((entry) => (added.primary ? { ...entry, primary: false } : entry)), row]\n}\n\n/** `roles` with one contact's role set — `''` for none. */\nexport function dealContactRolesWithRole(\n roles: readonly CrmDealContactRole[],\n contactId: string,\n role: string,\n): CrmDealContactRole[] {\n return roles.map((row) => {\n if (row.contactId !== contactId) return row\n return { contactId: row.contactId, ...(role ? { role } : {}), primary: row.primary }\n })\n}\n\n/**\n * What a save of the card writes: the roles judged against the org's list\n * (a contact's current role kept), `contactId` as their Primary — `null`\n * for a field the caller deletes — and whether the Primary changed, which\n * is when the name the drawer copied beside `contactId` goes stale.\n */\nexport function dealContactRolesSave(\n deal: { contactId?: unknown; contactRoles?: unknown },\n next: readonly CrmDealContactRole[],\n picklist: CrmPicklist,\n):\n | {\n ok: true\n contactRoles: CrmDealContactRole[]\n contactId: string | null\n primaryChanged: boolean\n }\n | { ok: false; error: string } {\n const judged = judgeDealContactRoles(picklist, next, dealContactRolesOf(deal))\n if (judged.ok === false) return judged\n const fields = dealContactRoleFields(judged.roles)\n const before = typeof deal.contactId === 'string' && deal.contactId ? deal.contactId : null\n return { ok: true, ...fields, primaryChanged: fields.contactId !== before }\n}\n"],"names":["DEAL_CONTACT_ROLES_MAX","dealContactRoleFields","dealContactRolesOf","judgeDealContactRoles","dealContactRoleAddProblem","roles","contactId","some","row","length","dealContactRolesAdded","added","role","primary","map","entry","dealContactRolesWithRole","dealContactRolesSave","deal","next","picklist","judged","ok","fields","before","primaryChanged"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAGEA,sBAAsB,EACtBC,qBAAqB,EACrBC,kBAAkB,EAClBC,qBAAqB,QAChB,eAAc;AAErB;;;CAGC,GAED,sEAAsE,GACtE,OAAO,SAASC,0BACdC,KAAoC,EACpCC,SAAiB;IAEjB,IAAI,CAACA,WAAW,OAAO;IACvB,IAAID,MAAME,IAAI,CAAC,CAACC,MAAQA,IAAIF,SAAS,KAAKA,YAAY,OAAO;IAC7D,IAAID,MAAMI,MAAM,IAAIT,wBAAwB;QAC1C,OAAO,CAAC,qBAAqB,EAAEA,uBAAuB,UAAU,CAAC;IACnE;IACA,OAAO;AACT;AAEA,oGAAoG,GACpG,OAAO,SAASU,sBACdL,KAAoC,EACpCM,KAA6D;IAE7D,MAAMH,MAA0B;QAC9BF,WAAWK,MAAML,SAAS;OACtBK,MAAMC,IAAI,GAAG;QAAEA,MAAMD,MAAMC,IAAI;IAAC,IAAI,CAAC;QACzCC,SAASF,MAAME,OAAO;;IAExB,OAAO;WAAIR,MAAMS,GAAG,CAAC,CAACC,QAAWJ,MAAME,OAAO,GAAG,aAAKE;gBAAOF,SAAS;iBAAUE;QAASP;KAAI;AAC/F;AAEA,yDAAyD,GACzD,OAAO,SAASQ,yBACdX,KAAoC,EACpCC,SAAiB,EACjBM,IAAY;IAEZ,OAAOP,MAAMS,GAAG,CAAC,CAACN;QAChB,IAAIA,IAAIF,SAAS,KAAKA,WAAW,OAAOE;QACxC,OAAO;YAAEF,WAAWE,IAAIF,SAAS;WAAMM,OAAO;YAAEA;QAAK,IAAI,CAAC;YAAIC,SAASL,IAAIK,OAAO;;IACpF;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASI,qBACdC,IAAqD,EACrDC,IAAmC,EACnCC,QAAqB;IASrB,MAAMC,SAASlB,sBAAsBiB,UAAUD,MAAMjB,mBAAmBgB;IACxE,IAAIG,OAAOC,EAAE,KAAK,OAAO,OAAOD;IAChC,MAAME,SAAStB,sBAAsBoB,OAAOhB,KAAK;IACjD,MAAMmB,SAAS,OAAON,KAAKZ,SAAS,KAAK,YAAYY,KAAKZ,SAAS,GAAGY,KAAKZ,SAAS,GAAG;IACvF,OAAO;QAAEgB,IAAI;OAASC;QAAQE,gBAAgBF,OAAOjB,SAAS,KAAKkB;;AACrE"}
@@ -14,6 +14,15 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
+ /**
18
+ * The deal drawer's values and the documents they become (AGL-2598), as pure
19
+ * functions — the drawer holds strings the way inputs do, and this module is
20
+ * the one place those strings turn into the typed fields a `CrmDeal` stores.
21
+ * Kept out of the component so the translation can be tested without a
22
+ * Firestore or a DOM, and so the contact card and the company card, which
23
+ * open the same drawer with a link preselected, build a deal the same way.
24
+ */
25
+ import { type CrmDeal, type CrmDealStage, type CrmForecastCategory, type CrmPicklist } from '@aglyn/aglyn';
17
26
  import { type DealDoc, type PipelineDoc } from './deal-board-model';
18
27
  /** What the drawer edits — strings as the inputs hold them. */
19
28
  export interface DealFormValues {
@@ -31,6 +40,17 @@ export interface DealFormValues {
31
40
  companyId: string;
32
41
  companyName: string;
33
42
  notes: string;
43
+ /** The `opportunityType` label, or empty. */
44
+ type: string;
45
+ /** The `leadSource` label, or empty. */
46
+ leadSource: string;
47
+ nextStep: string;
48
+ /** The override as typed, `35`; empty means the stage's. */
49
+ probability: string;
50
+ /** Empty means the stage's — what a save stamps when nobody picked one. */
51
+ forecastCategory: '' | CrmForecastCategory;
52
+ /** One of the org's campaigns, or empty. */
53
+ campaignId: string;
34
54
  }
35
55
  export declare const DEAL_TITLE_MAX = 120;
36
56
  export declare const DEAL_NOTES_MAX = 4000;
@@ -62,7 +82,34 @@ export interface DealWriteContext {
62
82
  hostId: string;
63
83
  uid: string;
64
84
  nowMs: number;
85
+ /**
86
+ * The stage the deal is in, whose forecast category a form left on
87
+ * "from the stage" stamps (AGL-3516). Absent, the field is left unset and
88
+ * every reader derives it from the stage.
89
+ */
90
+ stage?: Pick<CrmDealStage, 'kind' | 'forecastCategory'> | null;
65
91
  }
92
+ /**
93
+ * The form's two picklist fields judged against the org's lists (AGL-3516),
94
+ * the way every server door judges them: an active value is stored as the
95
+ * list spells it, a blank clears, the deal's own current value is kept, and
96
+ * anything else is refused naming what the list allows. A NEW deal with no
97
+ * Type takes the list's default, as Salesforce's does; the lead source is
98
+ * never defaulted — it records where a deal came from.
99
+ */
100
+ export declare function judgeDealFormPicklists(values: DealFormValues, lists: {
101
+ type: CrmPicklist;
102
+ leadSource: CrmPicklist;
103
+ }, options: {
104
+ current?: Partial<Pick<CrmDeal, 'type' | 'leadSource'>> | null;
105
+ created: boolean;
106
+ }): {
107
+ ok: true;
108
+ values: DealFormValues;
109
+ } | {
110
+ ok: false;
111
+ error: string;
112
+ };
66
113
  /**
67
114
  * The document a new deal is created as. Open, in the chosen stage, with
68
115
  * the stage clock started now so "days in stage" counts from creation.
@@ -81,6 +128,15 @@ export declare function dealDocumentFromForm(values: DealFormValues, context: De
81
128
  */
82
129
  export declare function dealPatchFromForm(values: DealFormValues, nowMs: number, options?: {
83
130
  amountDerived?: boolean;
131
+ stage?: DealWriteContext['stage'];
132
+ /**
133
+ * The deal as stored, whose contact roles a changed contact moves
134
+ * (AGL-3521): the contact picked becomes the Primary — added with no
135
+ * role when the deal did not name them — and a cleared contact leaves
136
+ * the deal with no Primary. An unchanged contact writes no roles, so a
137
+ * role edited on the deal's page meanwhile is not overwritten.
138
+ */
139
+ current?: Pick<CrmDeal, 'contactId' | 'contactRoles'> | null;
84
140
  }): {
85
141
  set: Record<string, unknown>;
86
142
  clear: string[];