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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (346) hide show
  1. package/README.md +2 -0
  2. package/package.json +14 -14
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/components/companies-bulk-bar.js +12 -4
  6. package/src/lib/components/companies-bulk-bar.js.map +1 -1
  7. package/src/lib/components/company-detail-page.js +11 -0
  8. package/src/lib/components/company-detail-page.js.map +1 -1
  9. package/src/lib/components/company-edit-drawer.js +12 -2
  10. package/src/lib/components/company-edit-drawer.js.map +1 -1
  11. package/src/lib/components/company-import-drawer.d.ts +1 -1
  12. package/src/lib/components/company-picker.js +10 -3
  13. package/src/lib/components/company-picker.js.map +1 -1
  14. package/src/lib/components/contact-associations-card.js +4 -3
  15. package/src/lib/components/contact-associations-card.js.map +1 -1
  16. package/src/lib/components/contact-detail-page.d.ts +4 -4
  17. package/src/lib/components/contact-detail-page.js +50 -53
  18. package/src/lib/components/contact-detail-page.js.map +1 -1
  19. package/src/lib/components/contact-list-columns.d.ts +3 -0
  20. package/src/lib/components/contact-list-columns.js +21 -5
  21. package/src/lib/components/contact-list-columns.js.map +1 -1
  22. package/src/lib/components/contact-merge-dialog.js +2 -1
  23. package/src/lib/components/contact-merge-dialog.js.map +1 -1
  24. package/src/lib/components/contact-timeline-card.js +4 -3
  25. package/src/lib/components/contact-timeline-card.js.map +1 -1
  26. package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
  27. package/src/lib/components/contacts-bulk-bar.js +24 -51
  28. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  29. package/src/lib/components/contacts-section.js +9 -3
  30. package/src/lib/components/contacts-section.js.map +1 -1
  31. package/src/lib/components/crm-booking-zone.d.ts +8 -2
  32. package/src/lib/components/crm-booking-zone.js.map +1 -1
  33. package/src/lib/components/crm-export-all-button.d.ts +1 -1
  34. package/src/lib/components/crm-export-all-button.js.map +1 -1
  35. package/src/lib/components/crm-glance-card.js +2 -1
  36. package/src/lib/components/crm-glance-card.js.map +1 -1
  37. package/src/lib/components/crm-send-email-dialog.js +9 -6
  38. package/src/lib/components/crm-send-email-dialog.js.map +1 -1
  39. package/src/lib/components/csv-import-drawer.js +2 -2
  40. package/src/lib/components/csv-import-drawer.js.map +1 -1
  41. package/src/lib/components/deal-detail-page.js +18 -2
  42. package/src/lib/components/deal-detail-page.js.map +1 -1
  43. package/src/lib/components/deal-edit-drawer.js +10 -0
  44. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  45. package/src/lib/components/deal-products-card.js +27 -16
  46. package/src/lib/components/deal-products-card.js.map +1 -1
  47. package/src/lib/components/deals-bulk-bar.js +13 -5
  48. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  49. package/src/lib/components/lead-campaigns-card.d.ts +4 -4
  50. package/src/lib/components/lead-campaigns-card.js +15 -8
  51. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  52. package/src/lib/components/lead-detail-page.js +25 -5
  53. package/src/lib/components/lead-detail-page.js.map +1 -1
  54. package/src/lib/components/lead-properties-card.js +10 -0
  55. package/src/lib/components/lead-properties-card.js.map +1 -1
  56. package/src/lib/components/lead-status-chip.js +3 -1
  57. package/src/lib/components/lead-status-chip.js.map +1 -1
  58. package/src/lib/components/lead-surfaces-note.js +4 -5
  59. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  60. package/src/lib/components/lead-unqualify-dialog.js +6 -0
  61. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  62. package/src/lib/components/leads-bulk-bar.js +34 -11
  63. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  64. package/src/lib/components/leads-section.js +41 -8
  65. package/src/lib/components/leads-section.js.map +1 -1
  66. package/src/lib/components/new-lead-drawer.js +5 -4
  67. package/src/lib/components/new-lead-drawer.js.map +1 -1
  68. package/src/lib/components/org-lead-surfaces-note.js +6 -2
  69. package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
  70. package/src/lib/components/recipes-card.js +9 -4
  71. package/src/lib/components/recipes-card.js.map +1 -1
  72. package/src/lib/components/record-sharing-card.d.ts +57 -0
  73. package/src/lib/components/record-sharing-card.js +244 -0
  74. package/src/lib/components/record-sharing-card.js.map +1 -0
  75. package/src/lib/components/reports/activity-card.js +4 -3
  76. package/src/lib/components/reports/activity-card.js.map +1 -1
  77. package/src/lib/components/reports/closed-deals-card.js +4 -3
  78. package/src/lib/components/reports/closed-deals-card.js.map +1 -1
  79. package/src/lib/components/reports/contacts-mix-card.js +3 -2
  80. package/src/lib/components/reports/contacts-mix-card.js.map +1 -1
  81. package/src/lib/components/reports/contacts-trend-card.js +2 -1
  82. package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
  83. package/src/lib/components/reports/forecast-card.js +3 -2
  84. package/src/lib/components/reports/forecast-card.js.map +1 -1
  85. package/src/lib/components/reports/lead-funnel-card.js +8 -6
  86. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  87. package/src/lib/components/reports/pipeline-card.js +4 -3
  88. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  89. package/src/lib/components/reports/report-format.d.ts +1 -1
  90. package/src/lib/components/reports/report-format.js.map +1 -1
  91. package/src/lib/components/reports/report-scope.d.ts +1 -1
  92. package/src/lib/components/reports/report-scope.js.map +1 -1
  93. package/src/lib/components/reports/source-conversion-card.js +2 -1
  94. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  95. package/src/lib/components/reports/tasks-card.js +2 -1
  96. package/src/lib/components/reports/tasks-card.js.map +1 -1
  97. package/src/lib/components/reports/won-lost-by-owner-card.js +3 -2
  98. package/src/lib/components/reports/won-lost-by-owner-card.js.map +1 -1
  99. package/src/lib/components/reports-section.js +4 -4
  100. package/src/lib/components/reports-section.js.map +1 -1
  101. package/src/lib/components/settings-section.js +7 -0
  102. package/src/lib/components/settings-section.js.map +1 -1
  103. package/src/lib/components/share-records-dialog.d.ts +28 -0
  104. package/src/lib/components/share-records-dialog.js +184 -0
  105. package/src/lib/components/share-records-dialog.js.map +1 -0
  106. package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
  107. package/src/lib/components/sharing-rule-drawer.js +374 -0
  108. package/src/lib/components/sharing-rule-drawer.js.map +1 -0
  109. package/src/lib/components/sharing-rules-card.d.ts +24 -0
  110. package/src/lib/components/sharing-rules-card.js +287 -0
  111. package/src/lib/components/sharing-rules-card.js.map +1 -0
  112. package/src/lib/components/use-crm-api.d.ts +1 -1
  113. package/src/lib/components/use-crm-api.js.map +1 -1
  114. package/src/lib/components/use-site-route-context.d.ts +25 -0
  115. package/src/lib/components/use-site-route-context.js +58 -0
  116. package/src/lib/components/use-site-route-context.js.map +1 -0
  117. package/src/lib/constants/api-routes.d.ts +31 -0
  118. package/src/lib/constants/api-routes.js +27 -1
  119. package/src/lib/constants/api-routes.js.map +1 -1
  120. package/src/lib/constants/bundle-common.d.ts +2 -0
  121. package/src/lib/constants/bundle-common.js +1 -0
  122. package/src/lib/constants/bundle-common.js.map +1 -1
  123. package/src/lib/declarations.console-server.d.ts +41 -0
  124. package/src/lib/declarations.console-server.js +115 -0
  125. package/src/lib/declarations.console-server.js.map +1 -0
  126. package/src/lib/declarations.server.d.ts +12 -0
  127. package/src/lib/declarations.server.js +84 -1
  128. package/src/lib/declarations.server.js.map +1 -1
  129. package/src/lib/hooks/use-contact-remove.d.ts +35 -0
  130. package/src/lib/hooks/use-contact-remove.js +77 -0
  131. package/src/lib/hooks/use-contact-remove.js.map +1 -0
  132. package/src/lib/hooks/use-crm-campaigns.d.ts +2 -2
  133. package/src/lib/hooks/use-crm-campaigns.js +3 -3
  134. package/src/lib/hooks/use-crm-campaigns.js.map +1 -1
  135. package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
  136. package/src/lib/hooks/use-crm-sharing.js +109 -0
  137. package/src/lib/hooks/use-crm-sharing.js.map +1 -0
  138. package/src/lib/model/campaign-filing-activity.d.ts +1 -1
  139. package/src/lib/model/campaign-filing-activity.js +1 -1
  140. package/src/lib/model/campaign-filing-activity.js.map +1 -1
  141. package/src/lib/model/companies-csv.d.ts +1 -1
  142. package/src/lib/model/companies-csv.js +2 -2
  143. package/src/lib/model/companies-csv.js.map +1 -1
  144. package/src/lib/model/contact-holder.d.ts +68 -0
  145. package/src/lib/model/contact-holder.js +129 -0
  146. package/src/lib/model/contact-holder.js.map +1 -0
  147. package/src/lib/model/contact-interaction-links.d.ts +46 -0
  148. package/src/lib/model/contact-interaction-links.js +65 -0
  149. package/src/lib/model/contact-interaction-links.js.map +1 -0
  150. package/src/lib/model/contact-merge.d.ts +136 -0
  151. package/src/lib/model/contact-merge.js +412 -0
  152. package/src/lib/model/contact-merge.js.map +1 -0
  153. package/src/lib/model/contact-record.d.ts +7 -1
  154. package/src/lib/model/contact-record.js +7 -6
  155. package/src/lib/model/contact-record.js.map +1 -1
  156. package/src/lib/model/contact-remove.d.ts +48 -0
  157. package/src/lib/model/contact-remove.js +22 -0
  158. package/src/lib/model/contact-remove.js.map +1 -0
  159. package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
  160. package/src/lib/model/contacts-bulk-writes.js +2 -82
  161. package/src/lib/model/contacts-bulk-writes.js.map +1 -1
  162. package/src/lib/model/contacts-csv.d.ts +2 -2
  163. package/src/lib/model/contacts-csv.js +2 -2
  164. package/src/lib/model/contacts-csv.js.map +1 -1
  165. package/src/lib/model/crm-csv.d.ts +239 -0
  166. package/src/lib/model/crm-csv.js +467 -0
  167. package/src/lib/model/crm-csv.js.map +1 -0
  168. package/src/lib/model/crm-digest.d.ts +227 -0
  169. package/src/lib/model/crm-digest.js +336 -0
  170. package/src/lib/model/crm-digest.js.map +1 -0
  171. package/src/lib/model/crm-import.d.ts +7 -1
  172. package/src/lib/model/crm-import.js +2 -1
  173. package/src/lib/model/crm-import.js.map +1 -1
  174. package/src/lib/model/crm-inbound.d.ts +162 -0
  175. package/src/lib/model/crm-inbound.js +208 -0
  176. package/src/lib/model/crm-inbound.js.map +1 -0
  177. package/src/lib/model/crm-lead-import.d.ts +1 -1
  178. package/src/lib/model/crm-lead-import.js +4 -4
  179. package/src/lib/model/crm-lead-import.js.map +1 -1
  180. package/src/lib/model/crm-record-routes.d.ts +5 -0
  181. package/src/lib/model/crm-record-routes.js +7 -0
  182. package/src/lib/model/crm-record-routes.js.map +1 -1
  183. package/src/lib/model/crm-reports.d.ts +467 -0
  184. package/src/lib/model/crm-reports.js +667 -0
  185. package/src/lib/model/crm-reports.js.map +1 -0
  186. package/src/lib/model/crm-routes.d.ts +6 -5
  187. package/src/lib/model/crm-routes.js +7 -7
  188. package/src/lib/model/crm-routes.js.map +1 -1
  189. package/src/lib/model/crm-search-sources.d.ts +15 -0
  190. package/src/lib/model/crm-search-sources.js +162 -0
  191. package/src/lib/model/crm-search-sources.js.map +1 -0
  192. package/src/lib/model/crm-sharing-api.d.ts +32 -0
  193. package/src/lib/model/crm-sharing-api.js +59 -0
  194. package/src/lib/model/crm-sharing-api.js.map +1 -0
  195. package/src/lib/model/crm-sharing.d.ts +333 -0
  196. package/src/lib/model/crm-sharing.js +603 -0
  197. package/src/lib/model/crm-sharing.js.map +1 -0
  198. package/src/lib/model/deals-csv.d.ts +1 -1
  199. package/src/lib/model/deals-csv.js +4 -5
  200. package/src/lib/model/deals-csv.js.map +1 -1
  201. package/src/lib/model/lead-filters.d.ts +5 -4
  202. package/src/lib/model/lead-filters.js +7 -5
  203. package/src/lib/model/lead-filters.js.map +1 -1
  204. package/src/lib/model/leads-csv.d.ts +1 -1
  205. package/src/lib/model/leads-csv.js +1 -1
  206. package/src/lib/model/leads-csv.js.map +1 -1
  207. package/src/lib/model/tasks-csv.d.ts +1 -1
  208. package/src/lib/model/tasks-csv.js +1 -1
  209. package/src/lib/model/tasks-csv.js.map +1 -1
  210. package/src/lib/plan-entitlements.d.ts +75 -0
  211. package/src/lib/plan-entitlements.js +56 -0
  212. package/src/lib/plan-entitlements.js.map +1 -0
  213. package/src/lib/plugin.js +39 -0
  214. package/src/lib/plugin.js.map +1 -1
  215. package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
  216. package/src/lib/server/api-v1/contacts-merge.js +120 -0
  217. package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
  218. package/src/lib/server/api-v1/contacts.d.ts +18 -0
  219. package/src/lib/server/api-v1/contacts.js +1076 -0
  220. package/src/lib/server/api-v1/contacts.js.map +1 -0
  221. package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
  222. package/src/lib/server/api-v1/crm-activities.js +269 -0
  223. package/src/lib/server/api-v1/crm-activities.js.map +1 -0
  224. package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
  225. package/src/lib/server/api-v1/crm-companies.js +391 -0
  226. package/src/lib/server/api-v1/crm-companies.js.map +1 -0
  227. package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
  228. package/src/lib/server/api-v1/crm-deals.js +631 -0
  229. package/src/lib/server/api-v1/crm-deals.js.map +1 -0
  230. package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
  231. package/src/lib/server/api-v1/crm-email-templates.js +336 -0
  232. package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
  233. package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
  234. package/src/lib/server/api-v1/crm-leads.js +833 -0
  235. package/src/lib/server/api-v1/crm-leads.js.map +1 -0
  236. package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
  237. package/src/lib/server/api-v1/crm-pipelines.js +146 -0
  238. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
  239. package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
  240. package/src/lib/server/api-v1/crm-shared.js +436 -0
  241. package/src/lib/server/api-v1/crm-shared.js.map +1 -0
  242. package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
  243. package/src/lib/server/api-v1/crm-tasks.js +398 -0
  244. package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
  245. package/src/lib/server/api-v1/openapi.d.ts +29 -0
  246. package/src/lib/server/api-v1/openapi.js +985 -0
  247. package/src/lib/server/api-v1/openapi.js.map +1 -0
  248. package/src/lib/server/api-v1/usage.d.ts +26 -0
  249. package/src/lib/server/api-v1/usage.js +73 -0
  250. package/src/lib/server/api-v1/usage.js.map +1 -0
  251. package/src/lib/server/campaign-filing-activity.d.ts +1 -1
  252. package/src/lib/server/campaign-filing-activity.js +7 -11
  253. package/src/lib/server/campaign-filing-activity.js.map +1 -1
  254. package/src/lib/server/capture-contact.js +15 -4
  255. package/src/lib/server/capture-contact.js.map +1 -1
  256. package/src/lib/server/consent-group-participant.js +1 -1
  257. package/src/lib/server/consent-group-participant.js.map +1 -1
  258. package/src/lib/server/contact-email-engagement.d.ts +33 -0
  259. package/src/lib/server/contact-email-engagement.js +146 -0
  260. package/src/lib/server/contact-email-engagement.js.map +1 -0
  261. package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
  262. package/src/lib/server/contact-lifecycle-floor.js +91 -0
  263. package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
  264. package/src/lib/server/contact-remove.d.ts +75 -0
  265. package/src/lib/server/contact-remove.js +244 -0
  266. package/src/lib/server/contact-remove.js.map +1 -0
  267. package/src/lib/server/contact-update.js +4 -3
  268. package/src/lib/server/contact-update.js.map +1 -1
  269. package/src/lib/server/contacts-import.d.ts +47 -11
  270. package/src/lib/server/contacts-import.js +171 -71
  271. package/src/lib/server/contacts-import.js.map +1 -1
  272. package/src/lib/server/contacts-merge.d.ts +1 -1
  273. package/src/lib/server/contacts-merge.js +3 -2
  274. package/src/lib/server/contacts-merge.js.map +1 -1
  275. package/src/lib/server/container-detach.d.ts +31 -0
  276. package/src/lib/server/container-detach.js +115 -0
  277. package/src/lib/server/container-detach.js.map +1 -0
  278. package/src/lib/server/convert-host-lead.d.ts +189 -0
  279. package/src/lib/server/convert-host-lead.js +518 -0
  280. package/src/lib/server/convert-host-lead.js.map +1 -0
  281. package/src/lib/server/crm-inbound-email.d.ts +162 -0
  282. package/src/lib/server/crm-inbound-email.js +259 -0
  283. package/src/lib/server/crm-inbound-email.js.map +1 -0
  284. package/src/lib/server/crm-records-meter.d.ts +50 -0
  285. package/src/lib/server/crm-records-meter.js +126 -0
  286. package/src/lib/server/crm-records-meter.js.map +1 -0
  287. package/src/lib/server/crm-sharing.d.ts +108 -0
  288. package/src/lib/server/crm-sharing.js +790 -0
  289. package/src/lib/server/crm-sharing.js.map +1 -0
  290. package/src/lib/server/daily-digest-route.d.ts +101 -0
  291. package/src/lib/server/daily-digest-route.js +578 -0
  292. package/src/lib/server/daily-digest-route.js.map +1 -0
  293. package/src/lib/server/export-route.d.ts +60 -0
  294. package/src/lib/server/export-route.js +447 -0
  295. package/src/lib/server/export-route.js.map +1 -0
  296. package/src/lib/server/inbound-address.js +5 -3
  297. package/src/lib/server/inbound-address.js.map +1 -1
  298. package/src/lib/server/inbound-route.d.ts +74 -0
  299. package/src/lib/server/inbound-route.js +260 -0
  300. package/src/lib/server/inbound-route.js.map +1 -0
  301. package/src/lib/server/lead-campaign-carry.js +3 -3
  302. package/src/lib/server/lead-campaign-carry.js.map +1 -1
  303. package/src/lib/server/lead-convert.d.ts +1 -1
  304. package/src/lib/server/lead-convert.js +2 -2
  305. package/src/lib/server/lead-convert.js.map +1 -1
  306. package/src/lib/server/lead-create.d.ts +1 -1
  307. package/src/lib/server/lead-create.js +10 -15
  308. package/src/lib/server/lead-create.js.map +1 -1
  309. package/src/lib/server/lead-nurturing.d.ts +23 -0
  310. package/src/lib/server/lead-nurturing.js +82 -0
  311. package/src/lib/server/lead-nurturing.js.map +1 -0
  312. package/src/lib/server/leads-import.js +7 -7
  313. package/src/lib/server/leads-import.js.map +1 -1
  314. package/src/lib/server/merge-contacts.d.ts +55 -0
  315. package/src/lib/server/merge-contacts.js +274 -0
  316. package/src/lib/server/merge-contacts.js.map +1 -0
  317. package/src/lib/server/pipeline-record-index.d.ts +36 -0
  318. package/src/lib/server/pipeline-record-index.js +87 -0
  319. package/src/lib/server/pipeline-record-index.js.map +1 -0
  320. package/src/lib/server/record-email-state.js +23 -0
  321. package/src/lib/server/record-email-state.js.map +1 -1
  322. package/src/lib/server/record-facts.js +2 -1
  323. package/src/lib/server/record-facts.js.map +1 -1
  324. package/src/lib/server/record-timeline.d.ts +12 -0
  325. package/src/lib/server/record-timeline.js +127 -15
  326. package/src/lib/server/record-timeline.js.map +1 -1
  327. package/src/lib/server/task-reminders-route.d.ts +82 -0
  328. package/src/lib/server/task-reminders-route.js +499 -0
  329. package/src/lib/server/task-reminders-route.js.map +1 -0
  330. package/src/lib/server/task-routes.d.ts +2 -0
  331. package/src/lib/server/task-routes.js +22 -3
  332. package/src/lib/server/task-routes.js.map +1 -1
  333. package/src/lib/server-deal-stage.js +2 -1
  334. package/src/lib/server-deal-stage.js.map +1 -1
  335. package/src/lib/server.d.ts +5 -2
  336. package/src/lib/server.js +47 -2
  337. package/src/lib/server.js.map +1 -1
  338. package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
  339. package/src/lib/testing/stand-in-site-record-routes.js +55 -0
  340. package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
  341. package/src/lib/usage-axes.d.ts +35 -0
  342. package/src/lib/usage-axes.js +75 -0
  343. package/src/lib/usage-axes.js.map +1 -0
  344. package/src/lib/components/use-emails-hub-path.d.ts +0 -24
  345. package/src/lib/components/use-emails-hub-path.js +0 -59
  346. package/src/lib/components/use-emails-hub-path.js.map +0 -1
@@ -0,0 +1,244 @@
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
+ */ 'use client';
17
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
+ import { pluginDocsHelp } from "@aglyn/aglyn";
19
+ import { mdiClose } from "@aglyn/shared-data-mdi";
20
+ import { CardDisplay, MdiIcon, SrOnly } from "@aglyn/shared-ui-jsx";
21
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
22
+ import { useUser } from "@aglyn/tenant-feature-instance";
23
+ import { Button, Chip, IconButton, Stack, Typography } from "@mui/material";
24
+ import { useMemo, useState } from "react";
25
+ import { useCrmSharingRules, useCrmSharingSites } from "../hooks/use-crm-sharing.js";
26
+ import { CRM_SHARING_OBJECT_LABELS, crmShareChipFor, crmShareChipLabel, describeRecordSharing, describeShareTargets } from "../model/crm-sharing.js";
27
+ import { callCrmSharing } from "../model/crm-sharing-api.js";
28
+ import { crmTaskCallScope } from "../model/task-routes.js";
29
+ import { useCanManageCrmSettings } from "./settings-section.js";
30
+ import { ShareRecordsDialog } from "./share-records-dialog.js";
31
+ /**
32
+ * The chip a list row or a record header shows when this site sees the
33
+ * record only because it was shared with it: "Shared by Dana".
34
+ */ export function CrmShareChipView(props) {
35
+ const rules = useCrmSharingRules(props.org);
36
+ if (!props.chip) return null;
37
+ return /*#__PURE__*/ _jsx(Chip, {
38
+ size: "small",
39
+ variant: "outlined",
40
+ color: "info",
41
+ label: crmShareChipLabel(props.chip, rules),
42
+ sx: {
43
+ maxWidth: 220
44
+ }
45
+ });
46
+ }
47
+ CrmShareChipView.displayName = 'CrmShareChipView';
48
+ const when = (atMs)=>atMs > 0 ? new Date(atMs).toLocaleDateString() : '';
49
+ /**
50
+ * SHARING — where this record is visible, and why (AGL-3336).
51
+ *
52
+ * One line for the sites that hold it (its consent group, or the site that
53
+ * created it), then one per hand share, naming who shared it and when, with
54
+ * the one-click unshare, and one per sharing rule. The held line has no
55
+ * unshare: visibility a site holds is not the sharing's to take away. A
56
+ * rule's line is removed by editing the rule, in Settings.
57
+ *
58
+ * The header's "Share with sites…" is an org manager's, and so is the
59
+ * unshare; every other reader sees the lines and nothing to press. For a
60
+ * reader on a site that sees the record only through a read-only share,
61
+ * the card says so, since the page's edits will be refused.
62
+ */ export function RecordSharingCard(props) {
63
+ const { object, id, record, hostId, orgId, org, viewingHostIds = [] } = props;
64
+ const { data: user } = useUser();
65
+ const { enqueueSnackbar } = useSnackbar();
66
+ const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId != null ? orgId : undefined);
67
+ const sites = useCrmSharingSites({
68
+ hostId,
69
+ orgId,
70
+ enabled: canManage
71
+ });
72
+ const rules = useCrmSharingRules(org);
73
+ const lines = useMemo(()=>describeRecordSharing(record), [
74
+ record
75
+ ]);
76
+ const chip = useMemo(()=>crmShareChipFor(record, viewingHostIds), [
77
+ record,
78
+ viewingHostIds
79
+ ]);
80
+ const [sharing, setSharing] = useState(false);
81
+ const [busyKey, setBusyKey] = useState(null);
82
+ const scope = crmTaskCallScope(hostId, orgId);
83
+ const label = CRM_SHARING_OBJECT_LABELS[object];
84
+ const shared = lines.some((line)=>line.kind !== 'held');
85
+ if (!record || !shared && !(roleReady && canManage)) return null;
86
+ const unshare = async (key, token)=>{
87
+ if (!scope) return;
88
+ setBusyKey(key);
89
+ try {
90
+ await callCrmSharing(user, scope, 'unshare', {
91
+ object,
92
+ ids: [
93
+ id
94
+ ],
95
+ targets: token === 'org' ? 'all' : [
96
+ token.slice('host:'.length)
97
+ ]
98
+ });
99
+ enqueueSnackbar('No longer shared', {
100
+ variant: 'success',
101
+ persist: false
102
+ });
103
+ } catch (error) {
104
+ enqueueSnackbar(error.message, {
105
+ variant: 'error',
106
+ allowDuplicate: true
107
+ });
108
+ } finally{
109
+ setBusyKey(null);
110
+ }
111
+ };
112
+ const heldLine = lines.find((line)=>line.kind === 'held');
113
+ const heldHostIds = (heldLine == null ? void 0 : heldLine.kind) === 'held' ? heldLine.hostIds : [];
114
+ return /*#__PURE__*/ _jsxs(CardDisplay, {
115
+ header: 'Sharing',
116
+ help: pluginDocsHelp('crmSharing'),
117
+ contentGutterX: true,
118
+ contentGutterY: true,
119
+ HeaderProps: {
120
+ action: roleReady && canManage ? /*#__PURE__*/ _jsx(Button, {
121
+ variant: "contained",
122
+ size: "small",
123
+ onClick: ()=>setSharing(true),
124
+ children: 'Share with sites…'
125
+ }) : null
126
+ },
127
+ children: [
128
+ /*#__PURE__*/ _jsxs(Stack, {
129
+ spacing: 1.5,
130
+ children: [
131
+ chip ? /*#__PURE__*/ _jsx(Typography, {
132
+ variant: "body2",
133
+ children: chip.access === 'edit' ? `Shared with this site to read and edit. ${crmShareChipLabel(chip, rules)}.` : `Shared with this site read-only: you can see this ${label.one} and log activity ` + `and tasks on it, but not change it. ${crmShareChipLabel(chip, rules)}.`
134
+ }) : null,
135
+ lines.map((line)=>{
136
+ if (line.kind === 'held') {
137
+ return /*#__PURE__*/ _jsx(Typography, {
138
+ variant: "body2",
139
+ color: "text.secondary",
140
+ children: line.allSites ? 'Visible to every site in the organization.' : line.hostIds.length ? `Held by ${line.hostIds.map(sites.siteName).join(', ')} — where it was captured.` : 'Held by no site.'
141
+ }, "held");
142
+ }
143
+ if (line.kind === 'manual') {
144
+ const target = describeShareTargets([
145
+ line.token
146
+ ], sites.siteName);
147
+ return /*#__PURE__*/ _jsxs(Stack, {
148
+ direction: "row",
149
+ spacing: 1,
150
+ sx: {
151
+ alignItems: 'center'
152
+ },
153
+ children: [
154
+ /*#__PURE__*/ _jsx(Typography, {
155
+ variant: "body2",
156
+ sx: {
157
+ flex: 1,
158
+ minWidth: 0
159
+ },
160
+ children: `${target} — shared by ${line.byName || 'a manager'}${line.atMs ? ` on ${when(line.atMs)}` : ''}, ${line.access === 'edit' ? 'read and edit' : 'read-only'}`
161
+ }),
162
+ canManage ? /*#__PURE__*/ _jsxs(IconButton, {
163
+ size: "small",
164
+ disabled: busyKey !== null,
165
+ onClick: ()=>void unshare(line.key, line.token),
166
+ children: [
167
+ /*#__PURE__*/ _jsx(MdiIcon, {
168
+ path: mdiClose.path,
169
+ size: 0.7
170
+ }),
171
+ /*#__PURE__*/ _jsx(SrOnly, {
172
+ children: `Stop sharing with ${target}`
173
+ })
174
+ ]
175
+ }) : null
176
+ ]
177
+ }, line.key);
178
+ }
179
+ const rule = rules.find((entry)=>entry.id === line.ruleId);
180
+ return /*#__PURE__*/ _jsx(Typography, {
181
+ variant: "body2",
182
+ children: `${describeShareTargets(line.tokens, sites.siteName)} — by the rule ${rule ? `“${rule.name}”` : 'that shares it'}, ${line.access === 'edit' ? 'read and edit' : 'read-only'}`
183
+ }, line.key);
184
+ }),
185
+ !shared ? /*#__PURE__*/ _jsx(Typography, {
186
+ variant: "caption",
187
+ color: "text.secondary",
188
+ children: `Not shared with any other site. Sharing lets another site see this ${label.one}; it does not share the person's consent.`
189
+ }) : null
190
+ ]
191
+ }),
192
+ /*#__PURE__*/ _jsx(ShareRecordsDialog, {
193
+ open: sharing,
194
+ onClose: ()=>setSharing(false),
195
+ object: object,
196
+ ids: [
197
+ id
198
+ ],
199
+ scope: scope,
200
+ sites: sites.sites,
201
+ sitesReady: sites.ready,
202
+ heldHostIds: heldHostIds
203
+ })
204
+ ]
205
+ });
206
+ }
207
+ RecordSharingCard.displayName = 'RecordSharingCard';
208
+ /**
209
+ * "Share with sites…" on a list's bulk bar (AGL-3336): the same dialog as
210
+ * the record page, over the selection. Drawn for an org manager only.
211
+ */ export function CrmBulkShareButton(props) {
212
+ const { object, ids, hostId, orgId, disabled } = props;
213
+ const { canManage, ready } = useCanManageCrmSettings(orgId != null ? orgId : undefined);
214
+ const sites = useCrmSharingSites({
215
+ hostId,
216
+ orgId,
217
+ enabled: canManage
218
+ });
219
+ const [open, setOpen] = useState(false);
220
+ if (!ready || !canManage) return null;
221
+ return /*#__PURE__*/ _jsxs(_Fragment, {
222
+ children: [
223
+ /*#__PURE__*/ _jsx(Button, {
224
+ size: "small",
225
+ disabled: disabled || !ids.length,
226
+ onClick: ()=>setOpen(true),
227
+ children: 'Share with sites…'
228
+ }),
229
+ /*#__PURE__*/ _jsx(ShareRecordsDialog, {
230
+ open: open,
231
+ onClose: ()=>setOpen(false),
232
+ object: object,
233
+ ids: ids,
234
+ scope: crmTaskCallScope(hostId, orgId),
235
+ sites: sites.sites,
236
+ sitesReady: sites.ready
237
+ })
238
+ ]
239
+ });
240
+ }
241
+ CrmBulkShareButton.displayName = 'CrmBulkShareButton';
242
+ export default RecordSharingCard;
243
+
244
+ //# sourceMappingURL=record-sharing-card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/record-sharing-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiClose } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, IconButton, Stack, Typography } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport { useCrmSharingRules, useCrmSharingSites } from '../hooks/use-crm-sharing'\nimport type { CrmOrgDoc } from '../hooks/use-crm-scope'\nimport {\n CRM_SHARING_OBJECT_LABELS,\n type CrmShareChip,\n crmShareChipFor,\n crmShareChipLabel,\n type CrmSharingObject,\n describeRecordSharing,\n describeShareTargets,\n} from '../model/crm-sharing'\nimport { callCrmSharing } from '../model/crm-sharing-api'\nimport { crmTaskCallScope } from '../model/task-routes'\nimport { useCanManageCrmSettings } from './settings-section'\nimport { ShareRecordsDialog } from './share-records-dialog'\n\n/**\n * The chip a list row or a record header shows when this site sees the\n * record only because it was shared with it: \"Shared by Dana\".\n */\nexport function CrmShareChipView(props: {\n chip: CrmShareChip | null\n org: CrmOrgDoc\n}) {\n const rules = useCrmSharingRules(props.org)\n if (!props.chip) return null\n return (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color=\"info\"\n label={crmShareChipLabel(props.chip, rules)}\n sx={{ maxWidth: 220 }}\n />\n )\n}\nCrmShareChipView.displayName = 'CrmShareChipView'\n\nexport interface RecordSharingCardProps {\n object: CrmSharingObject\n id: string\n record: Readonly<Record<string, unknown>> | null | undefined\n /** The mounted site, or `null` at the organization level. */\n hostId: string | null\n orgId: string | null\n org: CrmOrgDoc\n /** The viewing site's consent-group sites, for \"shared with this site\". */\n viewingHostIds?: readonly string[]\n}\n\nconst when = (atMs: number) => (atMs > 0 ? new Date(atMs).toLocaleDateString() : '')\n\n/**\n * SHARING — where this record is visible, and why (AGL-3336).\n *\n * One line for the sites that hold it (its consent group, or the site that\n * created it), then one per hand share, naming who shared it and when, with\n * the one-click unshare, and one per sharing rule. The held line has no\n * unshare: visibility a site holds is not the sharing's to take away. A\n * rule's line is removed by editing the rule, in Settings.\n *\n * The header's \"Share with sites…\" is an org manager's, and so is the\n * unshare; every other reader sees the lines and nothing to press. For a\n * reader on a site that sees the record only through a read-only share,\n * the card says so, since the page's edits will be refused.\n */\nexport function RecordSharingCard(props: RecordSharingCardProps) {\n const { object, id, record, hostId, orgId, org, viewingHostIds = [] } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId ?? undefined)\n const sites = useCrmSharingSites({ hostId, orgId, enabled: canManage })\n const rules = useCrmSharingRules(org)\n const lines = useMemo(() => describeRecordSharing(record), [record])\n const chip = useMemo(() => crmShareChipFor(record, viewingHostIds), [record, viewingHostIds])\n const [sharing, setSharing] = useState(false)\n const [busyKey, setBusyKey] = useState<string | null>(null)\n const scope = crmTaskCallScope(hostId, orgId)\n const label = CRM_SHARING_OBJECT_LABELS[object]\n const shared = lines.some((line) => line.kind !== 'held')\n if (!record || (!shared && !(roleReady && canManage))) return null\n\n const unshare = async (key: string, token: string) => {\n if (!scope) return\n setBusyKey(key)\n try {\n await callCrmSharing(user, scope, 'unshare', {\n object,\n ids: [id],\n targets: token === 'org' ? 'all' : [token.slice('host:'.length)],\n })\n enqueueSnackbar('No longer shared', { variant: 'success', persist: false })\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyKey(null)\n }\n }\n\n const heldLine = lines.find((line) => line.kind === 'held')\n const heldHostIds = heldLine?.kind === 'held' ? heldLine.hostIds : []\n return (\n <CardDisplay\n header={'Sharing'}\n help={pluginDocsHelp('crmSharing')}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action:\n roleReady && canManage ? (\n <Button variant=\"contained\" size=\"small\" onClick={() => setSharing(true)}>\n {'Share with sites…'}\n </Button>\n ) : null,\n }}\n >\n <Stack spacing={1.5}>\n {chip ? (\n <Typography variant=\"body2\">\n {chip.access === 'edit'\n ? `Shared with this site to read and edit. ${crmShareChipLabel(chip, rules)}.`\n : `Shared with this site read-only: you can see this ${label.one} and log activity ` +\n `and tasks on it, but not change it. ${crmShareChipLabel(chip, rules)}.`}\n </Typography>\n ) : null}\n {lines.map((line) => {\n if (line.kind === 'held') {\n return (\n <Typography key=\"held\" variant=\"body2\" color=\"text.secondary\">\n {line.allSites\n ? 'Visible to every site in the organization.'\n : line.hostIds.length\n ? `Held by ${line.hostIds.map(sites.siteName).join(', ')} — where it was captured.`\n : 'Held by no site.'}\n </Typography>\n )\n }\n if (line.kind === 'manual') {\n const target = describeShareTargets([line.token], sites.siteName)\n return (\n <Stack key={line.key} direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\" sx={{ flex: 1, minWidth: 0 }}>\n {`${target} — shared by ${line.byName || 'a manager'}${\n line.atMs ? ` on ${when(line.atMs)}` : ''\n }, ${line.access === 'edit' ? 'read and edit' : 'read-only'}`}\n </Typography>\n {canManage ? (\n <IconButton\n size=\"small\"\n disabled={busyKey !== null}\n onClick={() => void unshare(line.key, line.token)}\n >\n <MdiIcon path={mdiClose.path} size={0.7} />\n <SrOnly>{`Stop sharing with ${target}`}</SrOnly>\n </IconButton>\n ) : null}\n </Stack>\n )\n }\n const rule = rules.find((entry) => entry.id === line.ruleId)\n return (\n <Typography key={line.key} variant=\"body2\">\n {`${describeShareTargets(line.tokens, sites.siteName)} — by the rule ${\n rule ? `“${rule.name}”` : 'that shares it'\n }, ${line.access === 'edit' ? 'read and edit' : 'read-only'}`}\n </Typography>\n )\n })}\n {!shared ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Not shared with any other site. Sharing lets another site see this ${label.one}; it does not share the person's consent.`}\n </Typography>\n ) : null}\n </Stack>\n <ShareRecordsDialog\n open={sharing}\n onClose={() => setSharing(false)}\n object={object}\n ids={[id]}\n scope={scope}\n sites={sites.sites}\n sitesReady={sites.ready}\n heldHostIds={heldHostIds}\n />\n </CardDisplay>\n )\n}\nRecordSharingCard.displayName = 'RecordSharingCard'\n\n/**\n * \"Share with sites…\" on a list's bulk bar (AGL-3336): the same dialog as\n * the record page, over the selection. Drawn for an org manager only.\n */\nexport function CrmBulkShareButton(props: {\n object: CrmSharingObject\n ids: readonly string[]\n hostId: string | null\n orgId: string | null\n disabled?: boolean\n}) {\n const { object, ids, hostId, orgId, disabled } = props\n const { canManage, ready } = useCanManageCrmSettings(orgId ?? undefined)\n const sites = useCrmSharingSites({ hostId, orgId, enabled: canManage })\n const [open, setOpen] = useState(false)\n if (!ready || !canManage) return null\n return (\n <>\n <Button size=\"small\" disabled={disabled || !ids.length} onClick={() => setOpen(true)}>\n {'Share with sites…'}\n </Button>\n <ShareRecordsDialog\n open={open}\n onClose={() => setOpen(false)}\n object={object}\n ids={ids}\n scope={crmTaskCallScope(hostId, orgId)}\n sites={sites.sites}\n sitesReady={sites.ready}\n />\n </>\n )\n}\nCrmBulkShareButton.displayName = 'CrmBulkShareButton'\n\nexport default RecordSharingCard\n"],"names":["pluginDocsHelp","mdiClose","CardDisplay","MdiIcon","SrOnly","useSnackbar","useUser","Button","Chip","IconButton","Stack","Typography","useMemo","useState","useCrmSharingRules","useCrmSharingSites","CRM_SHARING_OBJECT_LABELS","crmShareChipFor","crmShareChipLabel","describeRecordSharing","describeShareTargets","callCrmSharing","crmTaskCallScope","useCanManageCrmSettings","ShareRecordsDialog","CrmShareChipView","props","rules","org","chip","size","variant","color","label","sx","maxWidth","displayName","when","atMs","Date","toLocaleDateString","RecordSharingCard","object","id","record","hostId","orgId","viewingHostIds","data","user","enqueueSnackbar","canManage","ready","roleReady","undefined","sites","enabled","lines","sharing","setSharing","busyKey","setBusyKey","scope","shared","some","line","kind","unshare","key","token","ids","targets","slice","length","persist","error","message","allowDuplicate","heldLine","find","heldHostIds","hostIds","header","help","contentGutterX","contentGutterY","HeaderProps","action","onClick","spacing","access","one","map","allSites","siteName","join","target","direction","alignItems","flex","minWidth","byName","disabled","path","rule","entry","ruleId","tokens","name","open","onClose","sitesReady","CrmBulkShareButton","setOpen"],"mappings":"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"}
@@ -29,6 +29,7 @@ import { reportFilename } from "./report-format.js";
29
29
  import { reportCacheKey, scopedCollection, visibleToClause } from "./report-scope.js";
30
30
  import { ReportStatTile } from "./report-stat-tile.js";
31
31
  import { useAggregateRead, useWindowRead } from "./use-aggregate-read.js";
32
+ import { activityLeaderboard, deltaPercent } from "../../model/crm-reports.js";
32
33
  /**
33
34
  * How many activities and how many completed tasks the leaderboard is
34
35
  * grouped from.
@@ -144,7 +145,7 @@ const TASK_DONE_CEILING = 1000;
144
145
  ], {
145
146
  cacheKey: reportCacheKey(report, 'activity:tasks-window')
146
147
  });
147
- const rows = useMemo(()=>Aglyn.activityLeaderboard(activityWindow.rows, taskWindow.rows), [
148
+ const rows = useMemo(()=>activityLeaderboard(activityWindow.rows, taskWindow.rows), [
148
149
  activityWindow,
149
150
  taskWindow
150
151
  ]);
@@ -198,14 +199,14 @@ const TASK_DONE_CEILING = 1000;
198
199
  /*#__PURE__*/ _jsx(ReportStatTile, {
199
200
  label: 'Activities logged',
200
201
  value: activityFigures ? activityFigures.current.toLocaleString() : null,
201
- deltaPct: activityFigures ? Aglyn.deltaPercent(activityFigures.current, activityFigures.previous) : null,
202
+ deltaPct: activityFigures ? deltaPercent(activityFigures.current, activityFigures.previous) : null,
202
203
  note: 'counted on the server',
203
204
  href: routes.section('contacts')
204
205
  }),
205
206
  /*#__PURE__*/ _jsx(ReportStatTile, {
206
207
  label: 'Tasks completed',
207
208
  value: taskFigures ? taskFigures.current.toLocaleString() : null,
208
- deltaPct: taskFigures ? Aglyn.deltaPercent(taskFigures.current, taskFigures.previous) : null,
209
+ deltaPct: taskFigures ? deltaPercent(taskFigures.current, taskFigures.previous) : null,
209
210
  note: 'ticked off in the period',
210
211
  href: routes.section('tasks')
211
212
  }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/activity-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n getCountFromServer,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useOrgMemberOptions } from '../../hooks/use-org-member-options'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\n\n/**\n * How many activities and how many completed tasks the leaderboard is\n * grouped from.\n *\n * The tiles are server counts and never meet these bounds; the table needs\n * each activity's kind and author and each task's completer, so it reads\n * the period's newest thousand of each and says when there were more —\n * the contacts list's bound, and the one every window on this page shares.\n */\nconst ACTIVITY_CEILING = 1000\nconst TASK_DONE_CEILING = 1000\n\ntype ActivityRow = Aglyn.CrmActivityRow\ntype TaskRow = Aglyn.CrmTask & { $id: string }\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = [\n 'Teammate',\n ...Aglyn.CRM_ACTIVITY_KINDS.map((kind) => Aglyn.CRM_ACTIVITY_KIND_LABELS[kind]),\n 'Activities',\n 'Tasks done',\n] as const\n\nexport interface ActivityCardProps {\n report: CrmReportScope\n}\n\n/**\n * Activity by teammate (AGL-2624): the calls, emails, meetings and notes\n * each member logged in the period, and the tasks they ticked off,\n * busiest first.\n *\n * ## Two windows, two indexes, one table\n *\n * Activities are counted within the period on `atMs`; completed tasks are\n * counted the same way on `completedAtMs`, under `status == 'done'`. Each\n * count is also drawn as a window, newest first, for the leaderboard.\n *\n * ## Why that is four indexes and not one\n *\n * The counts and the windows do not share an index, and neither do the two\n * audiences.\n *\n * A COUNT here names no `orderBy`. Firestore then orders implicitly by the\n * range field, ASCENDING, so a count wants `completedAtMs` ascending — while\n * the window beside it asks for `desc` explicitly and wants the descending\n * one. A composite index serves one direction or its exact reverse, never\n * both, so declaring only the direction the window reads leaves the count\n * with nothing: `FAILED_PRECONDITION`, one column of dashes, and a card that\n * otherwise looks fine.\n *\n * Then the visibility clause doubles it. `visibleToClause` contributes\n * nothing when the reader's tokens are null, which is every organization-level\n * read, so the shape arriving at Firestore has no leading array field — a\n * different index, not a prefix of the one with it. `(visibleTo, status,\n * completedAtMs)` serves a site and `(status, completedAtMs)` the org above\n * it; one without the other is a card that works for exactly one audience.\n *\n * The two reads are independent so that a missing task index degrades ONE\n * column to a dash and a notice rather than the whole card: the activities\n * were read, and the leaderboard draws them.\n *\n * A task ordered by `completedAtMs` is a task the complete route stamped;\n * a task marked done some other way, without the stamp, is not in the\n * window and the caption says what the column is grouped from.\n *\n * ## Names\n *\n * Every activity carries the name it was signed with, for the reason\n * `CrmActivity.byName` gives — a scoped reader cannot resolve a colleague's\n * uid. The roster is read too, through the members route any member may\n * ask, and wins when it knows the uid, so a renamed teammate reads by\n * their current name; the signed name covers a uid the roster no longer\n * has, and the uid itself covers the rest.\n */\nexport function ActivityCard(props: ActivityCardProps) {\n const { report } = props\n const { scope, tokens, range, period, routes } = report\n const firestore = useFirestore()\n const roster = useOrgMemberOptions(scope[1])\n\n const activitiesBetween = (from: number, to: number) =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.activities),\n ...visibleToClause(tokens),\n where('atMs', '>=', from),\n where('atMs', '<', to),\n )\n const tasksDoneBetween = (from: number, to: number) =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks),\n ...visibleToClause(tokens),\n where('status', '==', 'done'),\n where('completedAtMs', '>=', from),\n where('completedAtMs', '<', to),\n )\n const countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n const activityCounts = useAggregateRead(\n () =>\n Promise.all([\n countOf(activitiesBetween(range.from, range.to)),\n countOf(activitiesBetween(range.previousFrom, range.previousTo)),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:counts') },\n )\n const taskCounts = useAggregateRead(\n () =>\n Promise.all([\n countOf(tasksDoneBetween(range.from, range.to)),\n countOf(tasksDoneBetween(range.previousFrom, range.previousTo)),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:tasks-counts') },\n )\n\n const activityWindow = useWindowRead<ActivityRow>(\n () =>\n query(\n activitiesBetween(range.from, range.to),\n orderBy('atMs', 'desc'),\n limit(ACTIVITY_CEILING + 1),\n ),\n ACTIVITY_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:window') },\n )\n const taskWindow = useWindowRead<TaskRow>(\n () =>\n query(\n tasksDoneBetween(range.from, range.to),\n orderBy('completedAtMs', 'desc'),\n limit(TASK_DONE_CEILING + 1),\n ),\n TASK_DONE_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:tasks-window') },\n )\n\n const rows = useMemo(\n () => Aglyn.activityLeaderboard(activityWindow.rows, taskWindow.rows),\n [activityWindow, taskWindow],\n )\n const nameOf = (row: Aglyn.ActivityLeaderboardRow): string => {\n if (!row.uid) return 'No teammate'\n return Aglyn.findOrgMember(roster.options, row.uid)?.label ?? row.name ?? row.uid\n }\n const teammates = rows.filter((row) => row.uid).length\n\n const activitiesRead = activityWindow.status === 'success'\n const tasksRead = taskWindow.status === 'success'\n const settled = activityWindow.status !== 'loading' && taskWindow.status !== 'loading'\n const caption = [\n activityWindow.truncated\n ? `Grouped from the ${ACTIVITY_CEILING.toLocaleString()} most recent activities in the period`\n : null,\n taskWindow.truncated\n ? `the ${TASK_DONE_CEILING.toLocaleString()} most recently completed tasks`\n : null,\n ]\n .filter(Boolean)\n .join(' and ')\n const activityFigures = activityCounts.value\n const taskFigures = taskCounts.value\n\n return (\n <CardDisplay\n header={'Activity by teammate'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#activity-by-teammate',\n excerpt:\n 'The calls, emails, meetings and notes each teammate logged in ' +\n 'the period, and the tasks they completed, busiest first. ' +\n 'Grouped from the period’s newest thousand of each.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('activity', period)}\n columns={COLUMNS}\n rows={() =>\n rows.map((row) => [\n nameOf(row),\n ...Aglyn.CRM_ACTIVITY_KINDS.map((kind) => row.kinds[kind]),\n row.activities,\n tasksRead ? row.tasksDone : '',\n ])\n }\n disabled={!activitiesRead || !rows.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Activities logged'}\n value={activityFigures ? activityFigures.current.toLocaleString() : null}\n deltaPct={\n activityFigures\n ? Aglyn.deltaPercent(activityFigures.current, activityFigures.previous)\n : null\n }\n note={'counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Tasks completed'}\n value={taskFigures ? taskFigures.current.toLocaleString() : null}\n deltaPct={\n taskFigures\n ? Aglyn.deltaPercent(taskFigures.current, taskFigures.previous)\n : null\n }\n note={'ticked off in the period'}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Teammates active'}\n value={settled ? teammates.toLocaleString() : null}\n note={'logged or completed something'}\n />\n </Stack>\n {activityCounts.status === 'error' || activityWindow.status === 'error' ? (\n <Alert severity=\"warning\">{'The activities could not be read.'}</Alert>\n ) : null}\n {taskCounts.status === 'error' || taskWindow.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The completed-task counts could not be read; the Tasks done column is left blank.'}\n </Alert>\n ) : null}\n <Section title={'Who did what'}>\n {rows.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {rows.map((row) => (\n <TableRow key={row.uid || '$nobody'}>\n <TableCell>{nameOf(row)}</TableCell>\n {Aglyn.CRM_ACTIVITY_KINDS.map((kind) => (\n <TableCell key={kind} align=\"right\">\n {row.kinds[kind].toLocaleString()}\n </TableCell>\n ))}\n <TableCell align=\"right\">{row.activities.toLocaleString()}</TableCell>\n <TableCell align=\"right\">\n {tasksRead ? row.tasksDone.toLocaleString() : '—'}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled ? 'Nothing logged or completed in this period.' : 'Reading…'}\n </Typography>\n )}\n {rows.some((row) => !row.uid) ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No teammate — activities an automation logged, and tasks completed with nobody assigned.'}\n </Typography>\n ) : null}\n {/* What the table, and so the export, is grouped from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${caption}; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nActivityCard.displayName = 'ActivityCard'\n\nexport default ActivityCard\n"],"names":["Aglyn","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","where","useMemo","useFirestore","useOrgMemberOptions","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","ACTIVITY_CEILING","TASK_DONE_CEILING","COLUMNS","CRM_ACTIVITY_KINDS","map","kind","CRM_ACTIVITY_KIND_LABELS","ActivityCard","props","report","scope","tokens","range","period","routes","firestore","roster","activitiesBetween","from","to","CRM_COLLECTIONS","activities","tasksDoneBetween","tasks","countOf","target","then","snapshot","data","count","activityCounts","Promise","all","previousFrom","previousTo","current","previous","cacheKey","taskCounts","activityWindow","taskWindow","rows","activityLeaderboard","nameOf","row","uid","findOrgMember","options","label","name","teammates","filter","length","activitiesRead","status","tasksRead","settled","caption","truncated","toLocaleString","Boolean","join","activityFigures","value","taskFigures","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","kinds","tasksDone","disabled","spacing","direction","sx","flexWrap","deltaPct","deltaPercent","note","href","section","severity","title","size","column","index","align","variant","color","some","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/activity-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Section } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport {\n Alert,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Typography,\n} from '@mui/material'\nimport {\n getCountFromServer,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { useOrgMemberOptions } from '../../hooks/use-org-member-options'\nimport { ReportExport } from './report-export'\nimport { reportFilename } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport {\n activityLeaderboard,\n type ActivityLeaderboardRow,\n deltaPercent,\n} from '../../model/crm-reports'\n\n/**\n * How many activities and how many completed tasks the leaderboard is\n * grouped from.\n *\n * The tiles are server counts and never meet these bounds; the table needs\n * each activity's kind and author and each task's completer, so it reads\n * the period's newest thousand of each and says when there were more —\n * the contacts list's bound, and the one every window on this page shares.\n */\nconst ACTIVITY_CEILING = 1000\nconst TASK_DONE_CEILING = 1000\n\ntype ActivityRow = Aglyn.CrmActivityRow\ntype TaskRow = Aglyn.CrmTask & { $id: string }\n\n/** The table's columns, which are also the CSV's. */\nconst COLUMNS = [\n 'Teammate',\n ...Aglyn.CRM_ACTIVITY_KINDS.map((kind) => Aglyn.CRM_ACTIVITY_KIND_LABELS[kind]),\n 'Activities',\n 'Tasks done',\n] as const\n\nexport interface ActivityCardProps {\n report: CrmReportScope\n}\n\n/**\n * Activity by teammate (AGL-2624): the calls, emails, meetings and notes\n * each member logged in the period, and the tasks they ticked off,\n * busiest first.\n *\n * ## Two windows, two indexes, one table\n *\n * Activities are counted within the period on `atMs`; completed tasks are\n * counted the same way on `completedAtMs`, under `status == 'done'`. Each\n * count is also drawn as a window, newest first, for the leaderboard.\n *\n * ## Why that is four indexes and not one\n *\n * The counts and the windows do not share an index, and neither do the two\n * audiences.\n *\n * A COUNT here names no `orderBy`. Firestore then orders implicitly by the\n * range field, ASCENDING, so a count wants `completedAtMs` ascending — while\n * the window beside it asks for `desc` explicitly and wants the descending\n * one. A composite index serves one direction or its exact reverse, never\n * both, so declaring only the direction the window reads leaves the count\n * with nothing: `FAILED_PRECONDITION`, one column of dashes, and a card that\n * otherwise looks fine.\n *\n * Then the visibility clause doubles it. `visibleToClause` contributes\n * nothing when the reader's tokens are null, which is every organization-level\n * read, so the shape arriving at Firestore has no leading array field — a\n * different index, not a prefix of the one with it. `(visibleTo, status,\n * completedAtMs)` serves a site and `(status, completedAtMs)` the org above\n * it; one without the other is a card that works for exactly one audience.\n *\n * The two reads are independent so that a missing task index degrades ONE\n * column to a dash and a notice rather than the whole card: the activities\n * were read, and the leaderboard draws them.\n *\n * A task ordered by `completedAtMs` is a task the complete route stamped;\n * a task marked done some other way, without the stamp, is not in the\n * window and the caption says what the column is grouped from.\n *\n * ## Names\n *\n * Every activity carries the name it was signed with, for the reason\n * `CrmActivity.byName` gives — a scoped reader cannot resolve a colleague's\n * uid. The roster is read too, through the members route any member may\n * ask, and wins when it knows the uid, so a renamed teammate reads by\n * their current name; the signed name covers a uid the roster no longer\n * has, and the uid itself covers the rest.\n */\nexport function ActivityCard(props: ActivityCardProps) {\n const { report } = props\n const { scope, tokens, range, period, routes } = report\n const firestore = useFirestore()\n const roster = useOrgMemberOptions(scope[1])\n\n const activitiesBetween = (from: number, to: number) =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.activities),\n ...visibleToClause(tokens),\n where('atMs', '>=', from),\n where('atMs', '<', to),\n )\n const tasksDoneBetween = (from: number, to: number) =>\n query(\n scopedCollection(firestore, scope, Aglyn.CRM_COLLECTIONS.tasks),\n ...visibleToClause(tokens),\n where('status', '==', 'done'),\n where('completedAtMs', '>=', from),\n where('completedAtMs', '<', to),\n )\n const countOf = (target: ReturnType<typeof query>) =>\n getCountFromServer(target).then((snapshot) => snapshot.data().count)\n\n const activityCounts = useAggregateRead(\n () =>\n Promise.all([\n countOf(activitiesBetween(range.from, range.to)),\n countOf(activitiesBetween(range.previousFrom, range.previousTo)),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:counts') },\n )\n const taskCounts = useAggregateRead(\n () =>\n Promise.all([\n countOf(tasksDoneBetween(range.from, range.to)),\n countOf(tasksDoneBetween(range.previousFrom, range.previousTo)),\n ]).then(([current, previous]) => ({ current, previous })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:tasks-counts') },\n )\n\n const activityWindow = useWindowRead<ActivityRow>(\n () =>\n query(\n activitiesBetween(range.from, range.to),\n orderBy('atMs', 'desc'),\n limit(ACTIVITY_CEILING + 1),\n ),\n ACTIVITY_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:window') },\n )\n const taskWindow = useWindowRead<TaskRow>(\n () =>\n query(\n tasksDoneBetween(range.from, range.to),\n orderBy('completedAtMs', 'desc'),\n limit(TASK_DONE_CEILING + 1),\n ),\n TASK_DONE_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'activity:tasks-window') },\n )\n\n const rows = useMemo(\n () => activityLeaderboard(activityWindow.rows, taskWindow.rows),\n [activityWindow, taskWindow],\n )\n const nameOf = (row: ActivityLeaderboardRow): string => {\n if (!row.uid) return 'No teammate'\n return Aglyn.findOrgMember(roster.options, row.uid)?.label ?? row.name ?? row.uid\n }\n const teammates = rows.filter((row) => row.uid).length\n\n const activitiesRead = activityWindow.status === 'success'\n const tasksRead = taskWindow.status === 'success'\n const settled = activityWindow.status !== 'loading' && taskWindow.status !== 'loading'\n const caption = [\n activityWindow.truncated\n ? `Grouped from the ${ACTIVITY_CEILING.toLocaleString()} most recent activities in the period`\n : null,\n taskWindow.truncated\n ? `the ${TASK_DONE_CEILING.toLocaleString()} most recently completed tasks`\n : null,\n ]\n .filter(Boolean)\n .join(' and ')\n const activityFigures = activityCounts.value\n const taskFigures = taskCounts.value\n\n return (\n <CardDisplay\n header={'Activity by teammate'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#activity-by-teammate',\n excerpt:\n 'The calls, emails, meetings and notes each teammate logged in ' +\n 'the period, and the tasks they completed, busiest first. ' +\n 'Grouped from the period’s newest thousand of each.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <ReportExport\n filename={reportFilename('activity', period)}\n columns={COLUMNS}\n rows={() =>\n rows.map((row) => [\n nameOf(row),\n ...Aglyn.CRM_ACTIVITY_KINDS.map((kind) => row.kinds[kind]),\n row.activities,\n tasksRead ? row.tasksDone : '',\n ])\n }\n disabled={!activitiesRead || !rows.length}\n />\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Activities logged'}\n value={activityFigures ? activityFigures.current.toLocaleString() : null}\n deltaPct={\n activityFigures\n ? deltaPercent(activityFigures.current, activityFigures.previous)\n : null\n }\n note={'counted on the server'}\n href={routes.section('contacts')}\n />\n <ReportStatTile\n label={'Tasks completed'}\n value={taskFigures ? taskFigures.current.toLocaleString() : null}\n deltaPct={\n taskFigures\n ? deltaPercent(taskFigures.current, taskFigures.previous)\n : null\n }\n note={'ticked off in the period'}\n href={routes.section('tasks')}\n />\n <ReportStatTile\n label={'Teammates active'}\n value={settled ? teammates.toLocaleString() : null}\n note={'logged or completed something'}\n />\n </Stack>\n {activityCounts.status === 'error' || activityWindow.status === 'error' ? (\n <Alert severity=\"warning\">{'The activities could not be read.'}</Alert>\n ) : null}\n {taskCounts.status === 'error' || taskWindow.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The completed-task counts could not be read; the Tasks done column is left blank.'}\n </Alert>\n ) : null}\n <Section title={'Who did what'}>\n {rows.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {COLUMNS.map((column, index) => (\n <TableCell key={column} align={index ? 'right' : 'left'}>\n {column}\n </TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {rows.map((row) => (\n <TableRow key={row.uid || '$nobody'}>\n <TableCell>{nameOf(row)}</TableCell>\n {Aglyn.CRM_ACTIVITY_KINDS.map((kind) => (\n <TableCell key={kind} align=\"right\">\n {row.kinds[kind].toLocaleString()}\n </TableCell>\n ))}\n <TableCell align=\"right\">{row.activities.toLocaleString()}</TableCell>\n <TableCell align=\"right\">\n {tasksRead ? row.tasksDone.toLocaleString() : '—'}\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {settled ? 'Nothing logged or completed in this period.' : 'Reading…'}\n </Typography>\n )}\n {rows.some((row) => !row.uid) ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'No teammate — activities an automation logged, and tasks completed with nobody assigned.'}\n </Typography>\n ) : null}\n {/* What the table, and so the export, is grouped from when that is not everything. */}\n {caption ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`${caption}; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nActivityCard.displayName = 'ActivityCard'\n\nexport default ActivityCard\n"],"names":["Aglyn","CardDisplay","Section","ScrollTable","Alert","Stack","TableBody","TableCell","TableHead","TableRow","Typography","getCountFromServer","limit","orderBy","query","where","useMemo","useFirestore","useOrgMemberOptions","ReportExport","reportFilename","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","activityLeaderboard","deltaPercent","ACTIVITY_CEILING","TASK_DONE_CEILING","COLUMNS","CRM_ACTIVITY_KINDS","map","kind","CRM_ACTIVITY_KIND_LABELS","ActivityCard","props","report","scope","tokens","range","period","routes","firestore","roster","activitiesBetween","from","to","CRM_COLLECTIONS","activities","tasksDoneBetween","tasks","countOf","target","then","snapshot","data","count","activityCounts","Promise","all","previousFrom","previousTo","current","previous","cacheKey","taskCounts","activityWindow","taskWindow","rows","nameOf","row","uid","findOrgMember","options","label","name","teammates","filter","length","activitiesRead","status","tasksRead","settled","caption","truncated","toLocaleString","Boolean","join","activityFigures","value","taskFigures","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","filename","columns","kinds","tasksDone","disabled","spacing","direction","sx","flexWrap","deltaPct","note","href","section","severity","title","size","column","index","align","variant","color","some","displayName"],"mappings":"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"}
@@ -28,6 +28,7 @@ import { weekLabel } from "./report-format.js";
28
28
  import { reportCacheKey, scopedCollection, visibleToClause } from "./report-scope.js";
29
29
  import { ReportStatTile } from "./report-stat-tile.js";
30
30
  import { useAggregateRead, useWindowRead } from "./use-aggregate-read.js";
31
+ import { bucketByWeek, currencyOfDeals } from "../../model/crm-reports.js";
31
32
  /**
32
33
  * How many closed deals of each outcome the weekly chart reads.
33
34
  *
@@ -92,8 +93,8 @@ import { useAggregateRead, useWindowRead } from "./use-aggregate-read.js";
92
93
  const wonStatus = wonWindow.status;
93
94
  const lostStatus = lostWindow.status;
94
95
  const chart = useMemo(()=>{
95
- const wonWeeks = Aglyn.bucketByWeek(wonWindow.rows, (deal)=>deal.closedAtMs, range.from, range.to);
96
- const lostWeeks = Aglyn.bucketByWeek(lostWindow.rows, (deal)=>deal.closedAtMs, range.from, range.to);
96
+ const wonWeeks = bucketByWeek(wonWindow.rows, (deal)=>deal.closedAtMs, range.from, range.to);
97
+ const lostWeeks = bucketByWeek(lostWindow.rows, (deal)=>deal.closedAtMs, range.from, range.to);
97
98
  return {
98
99
  labels: wonWeeks.map((week)=>weekLabel(week.start)),
99
100
  won: wonWeeks.map((week)=>week.items.length),
@@ -102,7 +103,7 @@ import { useAggregateRead, useWindowRead } from "./use-aggregate-read.js";
102
103
  return total + Math.max(0, Number((_deal_amountCents = deal.amountCents) != null ? _deal_amountCents : 0));
103
104
  }, 0)),
104
105
  lost: lostWeeks.map((week)=>week.items.length),
105
- currency: Aglyn.currencyOfDeals(wonWindow.rows).currency
106
+ currency: currencyOfDeals(wonWindow.rows).currency
106
107
  };
107
108
  }, [
108
109
  wonWindow,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/closed-deals-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n getCountFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportBarChart } from './report-bar-chart'\nimport { weekLabel } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\n\n/**\n * How many closed deals of each outcome the weekly chart reads.\n *\n * The tiles are server aggregates and never meet this bound; the chart\n * needs each deal's `closedAtMs` to place it in a week, so it reads them,\n * most recent first, and says when the period held more.\n */\nconst CLOSED_DEAL_CEILING = 500\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\n\nexport interface ClosedDealsCardProps {\n report: CrmReportScope\n}\n\n/**\n * Deals won and lost in the period, and the win rate over what closed\n * (AGL-2604).\n *\n * Won and lost are TWO queries rather than one over `status in [won,\n * lost]`: each is an equality the `(visibleTo, status, closedAtMs)` index\n * serves directly, and a card that drew one series from a merged read\n * would have to split it again by hand. The range is on `closedAtMs`, the\n * moment the deal was closed — not `updatedAt`, which a note added last\n * week would move.\n */\nexport function ClosedDealsCard(props: ClosedDealsCardProps) {\n const { report } = props\n const { scope, tokens, range, routes } = report\n const firestore = useFirestore()\n\n const closedInPeriod = (status: Aglyn.CrmDealStatus) =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', status),\n where('closedAtMs', '>=', range.from),\n where('closedAtMs', '<', range.to),\n )\n\n const closed = useAggregateRead(\n () =>\n Promise.all([\n getAggregateFromServer(closedInPeriod('won'), {\n count: count(),\n amountCents: sum('amountCents'),\n }).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n getCountFromServer(closedInPeriod('lost')).then(\n (snapshot) => snapshot.data().count,\n ),\n ]).then(([won, lost]) => ({ won, lost })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:totals') },\n )\n\n const wonWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('won'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:won') },\n )\n const lostWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('lost'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:lost') },\n )\n const wonStatus = wonWindow.status\n const lostStatus = lostWindow.status\n\n const chart = useMemo(() => {\n const wonWeeks = Aglyn.bucketByWeek(\n wonWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n const lostWeeks = Aglyn.bucketByWeek(\n lostWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n return {\n labels: wonWeeks.map((week) => weekLabel(week.start)),\n won: wonWeeks.map((week) => week.items.length),\n wonCents: wonWeeks.map((week) =>\n week.items.reduce(\n (total, deal) => total + Math.max(0, Number(deal.amountCents ?? 0)),\n 0,\n ),\n ),\n lost: lostWeeks.map((week) => week.items.length),\n currency: Aglyn.currencyOfDeals(wonWindow.rows).currency,\n }\n }, [wonWindow, lostWindow, range])\n\n const figures = closed.value\n const winRate = figures\n ? measuredRate(figures.won.count, figures.won.count + figures.lost, 'closed')\n : null\n const chartRead = wonStatus === 'success' && lostStatus === 'success'\n\n return (\n <CardDisplay\n header={'Won and lost'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#won-and-lost',\n excerpt:\n 'Deals closed in the period, by outcome and by week, with the ' +\n 'value of what was won and the win rate over everything that ' +\n 'closed. Placed by the moment each deal was closed.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Won'}\n value={figures ? figures.won.count.toLocaleString() : null}\n note={\n figures\n ? `${money(figures.won.amountCents, chart.currency)} closed won`\n : undefined\n }\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Lost'}\n value={figures ? figures.lost.toLocaleString() : null}\n note={'in the period'}\n riseIsGood={false}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Win rate'}\n value={winRate ? percent(winRate.value) : null}\n note={\n winRate\n ? `${winRate.numerator.toLocaleString()} of ${winRate.denominator.toLocaleString()} ${winRate.denominatorLabel}`\n : figures\n ? 'nothing closed in the period'\n : undefined\n }\n />\n </Stack>\n {closed.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The closed-deal totals could not be read.'}\n </Alert>\n ) : null}\n <Section title={'Won vs lost per week'}>\n <ReportBarChart\n labels={chart.labels}\n series={[\n { name: 'Won', color: 'success.main', values: chart.won },\n { name: 'Lost', color: 'error.main', values: chart.lost },\n ]}\n format={(value, seriesIndex, barIndex) =>\n seriesIndex === 0\n ? `Won: ${value.toLocaleString()} · ${money(chart.wonCents[barIndex] ?? 0, chart.currency)}`\n : `Lost: ${value.toLocaleString()}`\n }\n emptyText={chartRead ? 'Nothing closed in this period.' : 'Reading…'}\n />\n {wonWindow.truncated || lostWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`The chart places the ${CLOSED_DEAL_CEILING} most recently closed of each outcome; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nClosedDealsCard.displayName = 'ClosedDealsCard'\n\nexport default ClosedDealsCard\n"],"names":["Aglyn","money","CardDisplay","measuredRate","percent","Section","Alert","Stack","Typography","count","getAggregateFromServer","getCountFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","ReportBarChart","weekLabel","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","CLOSED_DEAL_CEILING","ClosedDealsCard","props","report","scope","tokens","range","routes","firestore","closedInPeriod","status","from","to","closed","Promise","all","amountCents","then","snapshot","Number","data","won","lost","cacheKey","wonWindow","lostWindow","wonStatus","lostStatus","chart","wonWeeks","bucketByWeek","rows","deal","closedAtMs","lostWeeks","labels","map","week","start","items","length","wonCents","reduce","total","Math","max","currency","currencyOfDeals","figures","value","winRate","chartRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","direction","sx","flexWrap","label","toLocaleString","note","undefined","href","section","riseIsGood","numerator","denominator","denominatorLabel","severity","title","series","name","color","values","format","seriesIndex","barIndex","emptyText","truncated","variant","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/components/reports/closed-deals-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { money } from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport {\n measuredRate,\n percent,\n Section,\n} from '@aglyn/shared-ui-jsx/components/measured-figures.component'\nimport { Alert, Stack, Typography } from '@mui/material'\nimport {\n count,\n getAggregateFromServer,\n getCountFromServer,\n limit,\n orderBy,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { ReportBarChart } from './report-bar-chart'\nimport { weekLabel } from './report-format'\nimport {\n type CrmReportScope,\n reportCacheKey,\n scopedCollection,\n visibleToClause,\n} from './report-scope'\nimport { ReportStatTile } from './report-stat-tile'\nimport { useAggregateRead, useWindowRead } from './use-aggregate-read'\nimport { bucketByWeek, currencyOfDeals } from '../../model/crm-reports'\n\n/**\n * How many closed deals of each outcome the weekly chart reads.\n *\n * The tiles are server aggregates and never meet this bound; the chart\n * needs each deal's `closedAtMs` to place it in a week, so it reads them,\n * most recent first, and says when the period held more.\n */\nconst CLOSED_DEAL_CEILING = 500\n\ntype DealRow = Aglyn.CrmDeal & { $id: string }\n\nexport interface ClosedDealsCardProps {\n report: CrmReportScope\n}\n\n/**\n * Deals won and lost in the period, and the win rate over what closed\n * (AGL-2604).\n *\n * Won and lost are TWO queries rather than one over `status in [won,\n * lost]`: each is an equality the `(visibleTo, status, closedAtMs)` index\n * serves directly, and a card that drew one series from a merged read\n * would have to split it again by hand. The range is on `closedAtMs`, the\n * moment the deal was closed — not `updatedAt`, which a note added last\n * week would move.\n */\nexport function ClosedDealsCard(props: ClosedDealsCardProps) {\n const { report } = props\n const { scope, tokens, range, routes } = report\n const firestore = useFirestore()\n\n const closedInPeriod = (status: Aglyn.CrmDealStatus) =>\n query(\n scopedCollection(firestore, scope, 'deals'),\n ...visibleToClause(tokens),\n where('status', '==', status),\n where('closedAtMs', '>=', range.from),\n where('closedAtMs', '<', range.to),\n )\n\n const closed = useAggregateRead(\n () =>\n Promise.all([\n getAggregateFromServer(closedInPeriod('won'), {\n count: count(),\n amountCents: sum('amountCents'),\n }).then((snapshot) => ({\n count: Number(snapshot.data().count ?? 0),\n amountCents: Number(snapshot.data().amountCents ?? 0),\n })),\n getCountFromServer(closedInPeriod('lost')).then(\n (snapshot) => snapshot.data().count,\n ),\n ]).then(([won, lost]) => ({ won, lost })),\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:totals') },\n )\n\n const wonWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('won'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:won') },\n )\n const lostWindow = useWindowRead<DealRow>(\n () =>\n query(\n closedInPeriod('lost'),\n orderBy('closedAtMs', 'desc'),\n limit(CLOSED_DEAL_CEILING + 1),\n ),\n CLOSED_DEAL_CEILING,\n [firestore, scope, tokens, range],\n { cacheKey: reportCacheKey(report, 'closed:lost') },\n )\n const wonStatus = wonWindow.status\n const lostStatus = lostWindow.status\n\n const chart = useMemo(() => {\n const wonWeeks = bucketByWeek(\n wonWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n const lostWeeks = bucketByWeek(\n lostWindow.rows,\n (deal) => deal.closedAtMs,\n range.from,\n range.to,\n )\n return {\n labels: wonWeeks.map((week) => weekLabel(week.start)),\n won: wonWeeks.map((week) => week.items.length),\n wonCents: wonWeeks.map((week) =>\n week.items.reduce(\n (total, deal) => total + Math.max(0, Number(deal.amountCents ?? 0)),\n 0,\n ),\n ),\n lost: lostWeeks.map((week) => week.items.length),\n currency: currencyOfDeals(wonWindow.rows).currency,\n }\n }, [wonWindow, lostWindow, range])\n\n const figures = closed.value\n const winRate = figures\n ? measuredRate(figures.won.count, figures.won.count + figures.lost, 'closed')\n : null\n const chartRead = wonStatus === 'success' && lostStatus === 'success'\n\n return (\n <CardDisplay\n header={'Won and lost'}\n help={Aglyn.pluginDocsHelp('crmReports', {\n anchor: '#won-and-lost',\n excerpt:\n 'Deals closed in the period, by outcome and by week, with the ' +\n 'value of what was won and the win rate over everything that ' +\n 'closed. Placed by the moment each deal was closed.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={3} sx={{ flexWrap: 'wrap' }}>\n <ReportStatTile\n label={'Won'}\n value={figures ? figures.won.count.toLocaleString() : null}\n note={\n figures\n ? `${money(figures.won.amountCents, chart.currency)} closed won`\n : undefined\n }\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Lost'}\n value={figures ? figures.lost.toLocaleString() : null}\n note={'in the period'}\n riseIsGood={false}\n href={routes.section('deals')}\n />\n <ReportStatTile\n label={'Win rate'}\n value={winRate ? percent(winRate.value) : null}\n note={\n winRate\n ? `${winRate.numerator.toLocaleString()} of ${winRate.denominator.toLocaleString()} ${winRate.denominatorLabel}`\n : figures\n ? 'nothing closed in the period'\n : undefined\n }\n />\n </Stack>\n {closed.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The closed-deal totals could not be read.'}\n </Alert>\n ) : null}\n <Section title={'Won vs lost per week'}>\n <ReportBarChart\n labels={chart.labels}\n series={[\n { name: 'Won', color: 'success.main', values: chart.won },\n { name: 'Lost', color: 'error.main', values: chart.lost },\n ]}\n format={(value, seriesIndex, barIndex) =>\n seriesIndex === 0\n ? `Won: ${value.toLocaleString()} · ${money(chart.wonCents[barIndex] ?? 0, chart.currency)}`\n : `Lost: ${value.toLocaleString()}`\n }\n emptyText={chartRead ? 'Nothing closed in this period.' : 'Reading…'}\n />\n {wonWindow.truncated || lostWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`The chart places the ${CLOSED_DEAL_CEILING} most recently closed of each outcome; the tiles are counted on the server.`}\n </Typography>\n ) : null}\n </Section>\n </Stack>\n </CardDisplay>\n )\n}\nClosedDealsCard.displayName = 'ClosedDealsCard'\n\nexport default ClosedDealsCard\n"],"names":["Aglyn","money","CardDisplay","measuredRate","percent","Section","Alert","Stack","Typography","count","getAggregateFromServer","getCountFromServer","limit","orderBy","query","sum","where","useMemo","useFirestore","ReportBarChart","weekLabel","reportCacheKey","scopedCollection","visibleToClause","ReportStatTile","useAggregateRead","useWindowRead","bucketByWeek","currencyOfDeals","CLOSED_DEAL_CEILING","ClosedDealsCard","props","report","scope","tokens","range","routes","firestore","closedInPeriod","status","from","to","closed","Promise","all","amountCents","then","snapshot","Number","data","won","lost","cacheKey","wonWindow","lostWindow","wonStatus","lostStatus","chart","wonWeeks","rows","deal","closedAtMs","lostWeeks","labels","map","week","start","items","length","wonCents","reduce","total","Math","max","currency","figures","value","winRate","chartRead","header","help","pluginDocsHelp","anchor","excerpt","contentGutterX","contentGutterY","spacing","direction","sx","flexWrap","label","toLocaleString","note","undefined","href","section","riseIsGood","numerator","denominator","denominatorLabel","severity","title","series","name","color","values","format","seriesIndex","barIndex","emptyText","truncated","variant","displayName"],"mappings":"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"}
@@ -27,6 +27,7 @@ import { ReportBreakdown } from "./report-breakdown.js";
27
27
  import { plural } from "./report-format.js";
28
28
  import { reportCacheKey, scopedCollection, visibleToClause } from "./report-scope.js";
29
29
  import { useWindowRead } from "./use-aggregate-read.js";
30
+ import { funnelFromStages, tally } from "../../model/crm-reports.js";
30
31
  /**
31
32
  * How many contacts the source mix and the funnel are read from.
32
33
  *
@@ -71,7 +72,7 @@ import { useWindowRead } from "./use-aggregate-read.js";
71
72
  // A person captured two ways counts under both sources: the chart asks
72
73
  // "how many people came through each door", and a person who came
73
74
  // through two did.
74
- const sources = Aglyn.tally(facets.flatMap((facet)=>Object.keys(facet.sources).filter((source)=>facet.sources[source])), (source)=>source);
75
+ const sources = tally(facets.flatMap((facet)=>Object.keys(facet.sources).filter((source)=>facet.sources[source])), (source)=>source);
75
76
  const stageCounts = {};
76
77
  let unstaged = 0;
77
78
  for (const facet of facets){
@@ -85,7 +86,7 @@ import { useWindowRead } from "./use-aggregate-read.js";
85
86
  }
86
87
  return {
87
88
  sources,
88
- funnel: Aglyn.funnelFromStages(stageCounts),
89
+ funnel: funnelFromStages(stageCounts),
89
90
  unstaged
90
91
  };
91
92
  }, [