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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/package.json +14 -13
  2. package/src/index.d.ts +1 -0
  3. package/src/index.js.map +1 -1
  4. package/src/lib/components/companies-bulk-bar.js +12 -4
  5. package/src/lib/components/companies-bulk-bar.js.map +1 -1
  6. package/src/lib/components/company-detail-page.js +11 -0
  7. package/src/lib/components/company-detail-page.js.map +1 -1
  8. package/src/lib/components/company-edit-drawer.js +12 -2
  9. package/src/lib/components/company-edit-drawer.js.map +1 -1
  10. package/src/lib/components/company-import-drawer.d.ts +1 -1
  11. package/src/lib/components/company-picker.js +10 -3
  12. package/src/lib/components/company-picker.js.map +1 -1
  13. package/src/lib/components/contact-detail-page.d.ts +4 -4
  14. package/src/lib/components/contact-detail-page.js +50 -53
  15. package/src/lib/components/contact-detail-page.js.map +1 -1
  16. package/src/lib/components/contact-list-columns.d.ts +3 -0
  17. package/src/lib/components/contact-list-columns.js +21 -5
  18. package/src/lib/components/contact-list-columns.js.map +1 -1
  19. package/src/lib/components/contact-merge-dialog.js +2 -1
  20. package/src/lib/components/contact-merge-dialog.js.map +1 -1
  21. package/src/lib/components/contact-timeline-card.js +4 -3
  22. package/src/lib/components/contact-timeline-card.js.map +1 -1
  23. package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
  24. package/src/lib/components/contacts-bulk-bar.js +24 -51
  25. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  26. package/src/lib/components/contacts-section.js +9 -3
  27. package/src/lib/components/contacts-section.js.map +1 -1
  28. package/src/lib/components/crm-export-all-button.d.ts +1 -1
  29. package/src/lib/components/crm-export-all-button.js.map +1 -1
  30. package/src/lib/components/crm-send-email-dialog.js +9 -6
  31. package/src/lib/components/crm-send-email-dialog.js.map +1 -1
  32. package/src/lib/components/csv-import-drawer.js +2 -2
  33. package/src/lib/components/csv-import-drawer.js.map +1 -1
  34. package/src/lib/components/deal-detail-page.js +18 -2
  35. package/src/lib/components/deal-detail-page.js.map +1 -1
  36. package/src/lib/components/deal-edit-drawer.js +10 -0
  37. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  38. package/src/lib/components/deals-bulk-bar.js +13 -5
  39. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  40. package/src/lib/components/lead-campaigns-card.js +6 -0
  41. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  42. package/src/lib/components/lead-detail-page.js +25 -5
  43. package/src/lib/components/lead-detail-page.js.map +1 -1
  44. package/src/lib/components/lead-properties-card.js +6 -0
  45. package/src/lib/components/lead-properties-card.js.map +1 -1
  46. package/src/lib/components/lead-surfaces-note.js +4 -5
  47. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  48. package/src/lib/components/lead-unqualify-dialog.js +6 -0
  49. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  50. package/src/lib/components/leads-bulk-bar.js +28 -3
  51. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  52. package/src/lib/components/leads-section.js +36 -7
  53. package/src/lib/components/leads-section.js.map +1 -1
  54. package/src/lib/components/org-lead-surfaces-note.js +6 -2
  55. package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
  56. package/src/lib/components/recipes-card.js +9 -4
  57. package/src/lib/components/recipes-card.js.map +1 -1
  58. package/src/lib/components/record-sharing-card.d.ts +57 -0
  59. package/src/lib/components/record-sharing-card.js +244 -0
  60. package/src/lib/components/record-sharing-card.js.map +1 -0
  61. package/src/lib/components/settings-section.js +7 -0
  62. package/src/lib/components/settings-section.js.map +1 -1
  63. package/src/lib/components/share-records-dialog.d.ts +28 -0
  64. package/src/lib/components/share-records-dialog.js +184 -0
  65. package/src/lib/components/share-records-dialog.js.map +1 -0
  66. package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
  67. package/src/lib/components/sharing-rule-drawer.js +374 -0
  68. package/src/lib/components/sharing-rule-drawer.js.map +1 -0
  69. package/src/lib/components/sharing-rules-card.d.ts +24 -0
  70. package/src/lib/components/sharing-rules-card.js +287 -0
  71. package/src/lib/components/sharing-rules-card.js.map +1 -0
  72. package/src/lib/components/use-crm-api.d.ts +1 -1
  73. package/src/lib/components/use-crm-api.js.map +1 -1
  74. package/src/lib/components/use-site-route-context.d.ts +25 -0
  75. package/src/lib/components/use-site-route-context.js +58 -0
  76. package/src/lib/components/use-site-route-context.js.map +1 -0
  77. package/src/lib/constants/api-routes.d.ts +31 -0
  78. package/src/lib/constants/api-routes.js +27 -1
  79. package/src/lib/constants/api-routes.js.map +1 -1
  80. package/src/lib/declarations.console-server.d.ts +37 -0
  81. package/src/lib/declarations.console-server.js +103 -0
  82. package/src/lib/declarations.console-server.js.map +1 -0
  83. package/src/lib/declarations.server.d.ts +6 -0
  84. package/src/lib/declarations.server.js +33 -0
  85. package/src/lib/declarations.server.js.map +1 -1
  86. package/src/lib/hooks/use-contact-remove.d.ts +35 -0
  87. package/src/lib/hooks/use-contact-remove.js +77 -0
  88. package/src/lib/hooks/use-contact-remove.js.map +1 -0
  89. package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
  90. package/src/lib/hooks/use-crm-sharing.js +109 -0
  91. package/src/lib/hooks/use-crm-sharing.js.map +1 -0
  92. package/src/lib/model/companies-csv.d.ts +1 -1
  93. package/src/lib/model/companies-csv.js +2 -2
  94. package/src/lib/model/companies-csv.js.map +1 -1
  95. package/src/lib/model/contact-holder.d.ts +68 -0
  96. package/src/lib/model/contact-holder.js +129 -0
  97. package/src/lib/model/contact-holder.js.map +1 -0
  98. package/src/lib/model/contact-interaction-links.d.ts +46 -0
  99. package/src/lib/model/contact-interaction-links.js +65 -0
  100. package/src/lib/model/contact-interaction-links.js.map +1 -0
  101. package/src/lib/model/contact-merge.d.ts +136 -0
  102. package/src/lib/model/contact-merge.js +412 -0
  103. package/src/lib/model/contact-merge.js.map +1 -0
  104. package/src/lib/model/contact-record.d.ts +7 -1
  105. package/src/lib/model/contact-record.js +6 -5
  106. package/src/lib/model/contact-record.js.map +1 -1
  107. package/src/lib/model/contact-remove.d.ts +48 -0
  108. package/src/lib/model/contact-remove.js +22 -0
  109. package/src/lib/model/contact-remove.js.map +1 -0
  110. package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
  111. package/src/lib/model/contacts-bulk-writes.js +2 -82
  112. package/src/lib/model/contacts-bulk-writes.js.map +1 -1
  113. package/src/lib/model/contacts-csv.d.ts +2 -2
  114. package/src/lib/model/contacts-csv.js +2 -2
  115. package/src/lib/model/contacts-csv.js.map +1 -1
  116. package/src/lib/model/crm-csv.d.ts +239 -0
  117. package/src/lib/model/crm-csv.js +467 -0
  118. package/src/lib/model/crm-csv.js.map +1 -0
  119. package/src/lib/model/crm-digest.d.ts +185 -0
  120. package/src/lib/model/crm-digest.js +272 -0
  121. package/src/lib/model/crm-digest.js.map +1 -0
  122. package/src/lib/model/crm-import.d.ts +7 -1
  123. package/src/lib/model/crm-import.js +2 -1
  124. package/src/lib/model/crm-import.js.map +1 -1
  125. package/src/lib/model/crm-record-routes.d.ts +5 -0
  126. package/src/lib/model/crm-record-routes.js +7 -0
  127. package/src/lib/model/crm-record-routes.js.map +1 -1
  128. package/src/lib/model/crm-routes.d.ts +6 -5
  129. package/src/lib/model/crm-routes.js +7 -7
  130. package/src/lib/model/crm-routes.js.map +1 -1
  131. package/src/lib/model/crm-search-sources.d.ts +15 -0
  132. package/src/lib/model/crm-search-sources.js +162 -0
  133. package/src/lib/model/crm-search-sources.js.map +1 -0
  134. package/src/lib/model/crm-sharing-api.d.ts +32 -0
  135. package/src/lib/model/crm-sharing-api.js +59 -0
  136. package/src/lib/model/crm-sharing-api.js.map +1 -0
  137. package/src/lib/model/crm-sharing.d.ts +333 -0
  138. package/src/lib/model/crm-sharing.js +603 -0
  139. package/src/lib/model/crm-sharing.js.map +1 -0
  140. package/src/lib/model/deals-csv.d.ts +1 -1
  141. package/src/lib/model/deals-csv.js +4 -5
  142. package/src/lib/model/deals-csv.js.map +1 -1
  143. package/src/lib/model/lead-filters.d.ts +1 -1
  144. package/src/lib/model/leads-csv.d.ts +1 -1
  145. package/src/lib/model/leads-csv.js +1 -1
  146. package/src/lib/model/leads-csv.js.map +1 -1
  147. package/src/lib/model/tasks-csv.d.ts +1 -1
  148. package/src/lib/model/tasks-csv.js +1 -1
  149. package/src/lib/model/tasks-csv.js.map +1 -1
  150. package/src/lib/plan-entitlements.d.ts +75 -0
  151. package/src/lib/plan-entitlements.js +56 -0
  152. package/src/lib/plan-entitlements.js.map +1 -0
  153. package/src/lib/plugin.js +3 -0
  154. package/src/lib/plugin.js.map +1 -1
  155. package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
  156. package/src/lib/server/api-v1/contacts-merge.js +120 -0
  157. package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
  158. package/src/lib/server/api-v1/contacts.d.ts +18 -0
  159. package/src/lib/server/api-v1/contacts.js +1076 -0
  160. package/src/lib/server/api-v1/contacts.js.map +1 -0
  161. package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
  162. package/src/lib/server/api-v1/crm-activities.js +269 -0
  163. package/src/lib/server/api-v1/crm-activities.js.map +1 -0
  164. package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
  165. package/src/lib/server/api-v1/crm-companies.js +391 -0
  166. package/src/lib/server/api-v1/crm-companies.js.map +1 -0
  167. package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
  168. package/src/lib/server/api-v1/crm-deals.js +631 -0
  169. package/src/lib/server/api-v1/crm-deals.js.map +1 -0
  170. package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
  171. package/src/lib/server/api-v1/crm-email-templates.js +336 -0
  172. package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
  173. package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
  174. package/src/lib/server/api-v1/crm-leads.js +832 -0
  175. package/src/lib/server/api-v1/crm-leads.js.map +1 -0
  176. package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
  177. package/src/lib/server/api-v1/crm-pipelines.js +146 -0
  178. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
  179. package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
  180. package/src/lib/server/api-v1/crm-shared.js +436 -0
  181. package/src/lib/server/api-v1/crm-shared.js.map +1 -0
  182. package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
  183. package/src/lib/server/api-v1/crm-tasks.js +398 -0
  184. package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
  185. package/src/lib/server/api-v1/openapi.d.ts +29 -0
  186. package/src/lib/server/api-v1/openapi.js +985 -0
  187. package/src/lib/server/api-v1/openapi.js.map +1 -0
  188. package/src/lib/server/api-v1/usage.d.ts +26 -0
  189. package/src/lib/server/api-v1/usage.js +73 -0
  190. package/src/lib/server/api-v1/usage.js.map +1 -0
  191. package/src/lib/server/consent-group-participant.js +1 -1
  192. package/src/lib/server/consent-group-participant.js.map +1 -1
  193. package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
  194. package/src/lib/server/contact-lifecycle-floor.js +91 -0
  195. package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
  196. package/src/lib/server/contact-remove.d.ts +75 -0
  197. package/src/lib/server/contact-remove.js +244 -0
  198. package/src/lib/server/contact-remove.js.map +1 -0
  199. package/src/lib/server/contact-update.js +2 -1
  200. package/src/lib/server/contact-update.js.map +1 -1
  201. package/src/lib/server/contacts-import.d.ts +47 -11
  202. package/src/lib/server/contacts-import.js +171 -71
  203. package/src/lib/server/contacts-import.js.map +1 -1
  204. package/src/lib/server/contacts-merge.d.ts +1 -1
  205. package/src/lib/server/contacts-merge.js +3 -2
  206. package/src/lib/server/contacts-merge.js.map +1 -1
  207. package/src/lib/server/convert-host-lead.d.ts +189 -0
  208. package/src/lib/server/convert-host-lead.js +518 -0
  209. package/src/lib/server/convert-host-lead.js.map +1 -0
  210. package/src/lib/server/crm-sharing.d.ts +108 -0
  211. package/src/lib/server/crm-sharing.js +790 -0
  212. package/src/lib/server/crm-sharing.js.map +1 -0
  213. package/src/lib/server/daily-digest-route.d.ts +107 -0
  214. package/src/lib/server/daily-digest-route.js +588 -0
  215. package/src/lib/server/daily-digest-route.js.map +1 -0
  216. package/src/lib/server/export-route.d.ts +60 -0
  217. package/src/lib/server/export-route.js +447 -0
  218. package/src/lib/server/export-route.js.map +1 -0
  219. package/src/lib/server/inbound-route.d.ts +74 -0
  220. package/src/lib/server/inbound-route.js +257 -0
  221. package/src/lib/server/inbound-route.js.map +1 -0
  222. package/src/lib/server/lead-convert.d.ts +1 -1
  223. package/src/lib/server/lead-convert.js +2 -2
  224. package/src/lib/server/lead-convert.js.map +1 -1
  225. package/src/lib/server/merge-contacts.d.ts +55 -0
  226. package/src/lib/server/merge-contacts.js +274 -0
  227. package/src/lib/server/merge-contacts.js.map +1 -0
  228. package/src/lib/server/pipeline-record-index.d.ts +36 -0
  229. package/src/lib/server/pipeline-record-index.js +87 -0
  230. package/src/lib/server/pipeline-record-index.js.map +1 -0
  231. package/src/lib/server/record-facts.js +2 -1
  232. package/src/lib/server/record-facts.js.map +1 -1
  233. package/src/lib/server/task-reminders-route.d.ts +82 -0
  234. package/src/lib/server/task-reminders-route.js +497 -0
  235. package/src/lib/server/task-reminders-route.js.map +1 -0
  236. package/src/lib/server-deal-stage.js +2 -1
  237. package/src/lib/server-deal-stage.js.map +1 -1
  238. package/src/lib/server.d.ts +5 -2
  239. package/src/lib/server.js +47 -2
  240. package/src/lib/server.js.map +1 -1
  241. package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
  242. package/src/lib/testing/stand-in-site-record-routes.js +55 -0
  243. package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
  244. package/src/lib/usage-axes.d.ts +35 -0
  245. package/src/lib/usage-axes.js +68 -0
  246. package/src/lib/usage-axes.js.map +1 -0
  247. package/src/lib/components/use-emails-hub-path.d.ts +0 -24
  248. package/src/lib/components/use-emails-hub-path.js +0 -59
  249. package/src/lib/components/use-emails-hub-path.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-surfaces-note.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\n'use client'\n\nimport { buildRoute, Route } from '@aglyn/aglyn'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n usePluginApiPost,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack, Tooltip, Typography } from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n updateDoc,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n LEAD_SURFACE_FORMS_WINDOW,\n type LeadSurfaceForm,\n leadSurfaceForms,\n} from '../model/lead-surfaces'\n\nexport interface LeadSurfacesNoteProps {\n hostId: string\n}\n\n/** The always-on surfaces, named once wherever the note is drawn. */\nexport const LEAD_SURFACES_INTRO =\n 'Leads are created by bookings, forms with lead routing on, imports and New lead'\n/**\n * The rule that keeps one person one record (AGL-3232): a lead is the\n * whole record until it converts, and a person the workspace already holds\n * as a contact is not filed as a lead again.\n */\nexport const LEAD_SURFACES_CONTACTS_TOO =\n 'A lead is its own record until it converts into a contact. Someone who is already a contact is not filed as a lead again — their submission lands on the contact.'\n\n/** One site's forms, sorted into the two answers the note gives. */\nexport interface LeadSurfaceFormsResult {\n /** `routing.lead` is on: every submission with an address files a lead. */\n routed: LeadSurfaceForm[]\n /** The rest, each with its verdict on whether the switch may be offered. */\n unrouted: LeadSurfaceForm[]\n /** The site has more forms than the window; the lists are not the whole answer. */\n truncated: boolean\n status: ReturnType<typeof useFirestoreCollection>['status']\n}\n\n/**\n * The site's forms, ordered by id and bounded at one past the window — the\n * same read the Inbox's form picker makes, ordered by `__name__` because a\n * form saved without a display name would otherwise fall out of an ordered\n * query and vanish from a list about routing. Host-scoped by path, so any\n * member of the site may read it and no scope predicate is needed.\n *\n * One listener per call, so a note that groups several sites mounts one\n * reader per site it shows and nothing for a site it has not opened.\n */\nexport function useLeadSurfaceForms(hostId: string): LeadSurfaceFormsResult {\n const firestore = useFirestore()\n const { data: formDocs, status } = useFirestoreCollection<\n Record<string, unknown> & { $id: string }\n >(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'forms'),\n orderBy('__name__'),\n limit(LEAD_SURFACE_FORMS_WINDOW + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n return useMemo(() => {\n const forms = leadSurfaceForms(formDocs.slice(0, LEAD_SURFACE_FORMS_WINDOW))\n return {\n routed: forms.filter((form) => form.routed),\n unrouted: forms.filter((form) => !form.routed),\n truncated: formDocs.length > LEAD_SURFACE_FORMS_WINDOW,\n status,\n }\n }, [formDocs, status])\n}\n\n/** The form a switch is mid-flight on — under which site, since a form id is only unique within one. */\nexport interface LeadRoutingTarget {\n hostId: string\n formId: string\n}\n\n/**\n * The switch: one dotted `updateDoc` on `routing.lead`, so a dataset binding\n * beside it is untouched, under the same host-content write the form's page\n * makes. One at a time, whichever site the form is on — two switches in\n * flight would be two toasts racing for the same reader.\n *\n * `siteName` rides into the toast when the note shows more than one site,\n * because \"Contact now files a lead\" is an incomplete sentence to a reader\n * who can see three forms called Contact.\n */\nexport function useTurnOnLeadRouting(): {\n turningOn: LeadRoutingTarget | null\n turnOn: (target: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => Promise<void>\n} {\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const postPluginApi = usePluginApiPost()\n const [turningOn, setTurningOn] = useState<LeadRoutingTarget | null>(null)\n const turnOn = useCallback(\n async ({\n hostId,\n form,\n siteName,\n }: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => {\n if (turningOn) return\n setTurningOn({ hostId, formId: form.$id })\n const where = siteName ? ` on ${siteName}` : ''\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', form.$id), {\n 'routing.lead': true,\n updatedAt: serverTimestamp(),\n })\n // A lead-routing form holds `0` leads until its first (AGL-3330); the\n // counters are the forms plugin's to write, so it recounts them —\n // by address, since this plugin may not import that one.\n void postPluginApi('/api/forms/stats', { hostId, formIds: [form.$id] })\n enqueueSnackbar(\n `\"${form.displayName}\"${where} now files a lead from every submission ` +\n 'that carries an email address.',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar(`Lead routing could not be turned on${where}.`, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setTurningOn(null)\n }\n },\n [turningOn, firestore, enqueueSnackbar, postPluginApi],\n )\n return { turningOn, turnOn }\n}\n\n/** A form by name, linked to its own page when the console can address it. */\nexport function LeadSurfaceFormName(props: {\n form: LeadSurfaceForm\n href: string | null\n}) {\n const { form, href } = props\n return href ? (\n <AppLink href={href}>{form.displayName}</AppLink>\n ) : (\n <span>{form.displayName}</span>\n )\n}\nLeadSurfaceFormName.displayName = 'LeadSurfaceFormName'\n\n/** The routed forms as one clause — \"A, B, and possibly more\" — for a sentence to end. */\nexport function LeadSurfaceFormList(props: {\n forms: LeadSurfaceForm[]\n truncated: boolean\n formHref: (formId: string) => string | null\n}) {\n const { forms, truncated, formHref } = props\n return (\n <>\n {forms.map((form, index) => (\n <span key={form.$id}>\n {index ? ', ' : ''}\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </span>\n ))}\n {truncated ? ', and possibly more' : ''}\n </>\n )\n}\nLeadSurfaceFormList.displayName = 'LeadSurfaceFormList'\n\n/**\n * \"Not routing leads:\" — every form that does not route, with the switch\n * beside the ones that could and the reason beside the ones that cannot.\n *\n * Offered only where `leadSurfaceForms` says the publish check would honor\n * it — a form with no email field, or one that records no consent, is\n * refused with the reason as the tooltip, because a switch that flips and\n * then fails at publish is the exact confusion this note exists to remove.\n */\nexport function UnroutedLeadSurfaces(props: {\n hostId: string\n forms: LeadSurfaceForm[]\n formHref: (formId: string) => string | null\n turningOn: LeadRoutingTarget | null\n onTurnOn: (form: LeadSurfaceForm) => void\n}) {\n const { hostId, forms, formHref, turningOn, onTurnOn } = props\n return (\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Not routing leads:'}\n </Typography>\n {forms.map((form) => {\n const pending =\n turningOn?.hostId === hostId && turningOn.formId === form.$id\n return (\n <Stack\n key={form.$id}\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\" component=\"span\">\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </Typography>\n {/*\n Disabled with the reason as its tooltip rather than absent:\n an absent control and an inapplicable one look alike, and\n only one of them says what to do about it.\n */}\n <Tooltip title={form.blocker ?? ''}>\n <span>\n <Button\n size=\"small\"\n variant=\"text\"\n disabled={!form.canRoute || turningOn !== null}\n onClick={() => onTurnOn(form)}\n >\n {pending ? 'Turning on…' : 'Turn on lead routing'}\n </Button>\n </span>\n </Tooltip>\n </Stack>\n )\n })}\n </Stack>\n )\n}\nUnroutedLeadSurfaces.displayName = 'UnroutedLeadSurfaces'\n\n/**\n * WHAT CREATES A LEAD ON THIS SITE (AGL-2612).\n *\n * Every capture lands in Contacts; a LEAD is filed only by a lead surface —\n * a member sign-up, a booking, or a form whose author declared\n * `routing.lead`. The first two are always on and the third is a switch on\n * each form's own page, which left the Leads list unable to say why one\n * form's people were in it and another's were not. This says so, by name,\n * and offers the switch here for the forms that could carry it.\n *\n * The reader, the switch and the two rows are shared with the\n * organization-level note (`OrgLeadSurfacesNote`), which draws them once\n * per site; this is the one-site composition. The form links resolve the\n * site's console route from the host index, because under a site that is\n * the only address the note is handed.\n */\nexport function LeadSurfacesNote(props: LeadSurfacesNoteProps) {\n const { hostId } = props\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const { routed, unrouted, truncated, status } = useLeadSurfaceForms(hostId)\n const { turningOn, turnOn } = useTurnOnLeadRouting()\n\n const formHref = useCallback(\n (formId: string) =>\n orgSlug && host\n ? buildRoute(Route.FORM_DETAILS, { orgSlug, host, formId })\n : null,\n [orgSlug, host],\n )\n\n return (\n <Stack spacing={0.5}>\n <Typography variant=\"body2\" color=\"text.secondary\" component=\"div\">\n {LEAD_SURFACES_INTRO}\n {status === 'loading' ? (\n '…'\n ) : routed.length ? (\n <>\n {': '}\n <LeadSurfaceFormList forms={routed} truncated={truncated} formHref={formHref} />\n {'.'}\n </>\n ) : (\n ' — no form routes leads yet.'\n )}\n {` ${LEAD_SURFACES_CONTACTS_TOO}`}\n </Typography>\n {unrouted.length ? (\n <UnroutedLeadSurfaces\n hostId={hostId}\n forms={unrouted}\n formHref={formHref}\n turningOn={turningOn}\n onTurnOn={(form) => void turnOn({ hostId, form })}\n />\n ) : null}\n </Stack>\n )\n}\nLeadSurfacesNote.displayName = 'LeadSurfacesNote'\n\nexport default LeadSurfacesNote\n"],"names":["buildRoute","Route","AppLink","useSnackbar","useConsoleHostRoute","useFirestore","useFirestoreCollection","usePluginApiPost","Button","Stack","Tooltip","Typography","collection","doc","limit","orderBy","query","serverTimestamp","updateDoc","useCallback","useMemo","useState","LEAD_SURFACE_FORMS_WINDOW","leadSurfaceForms","LEAD_SURFACES_INTRO","LEAD_SURFACES_CONTACTS_TOO","useLeadSurfaceForms","hostId","firestore","data","formDocs","status","idField","forms","slice","routed","filter","form","unrouted","truncated","length","useTurnOnLeadRouting","enqueueSnackbar","postPluginApi","turningOn","setTurningOn","turnOn","siteName","formId","$id","where","updatedAt","formIds","displayName","variant","persist","error","console","allowDuplicate","LeadSurfaceFormName","props","href","span","LeadSurfaceFormList","formHref","map","index","UnroutedLeadSurfaces","onTurnOn","direction","spacing","sx","alignItems","flexWrap","rowGap","color","pending","component","title","blocker","size","disabled","canRoute","onClick","LeadSurfacesNote","orgSlug","subdomain","host","FORM_DETAILS"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-surfaces-note.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\n'use client'\n\nimport { pluginRecordHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n usePluginApiPost,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack, Tooltip, Typography } from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n updateDoc,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n LEAD_SURFACE_FORMS_WINDOW,\n type LeadSurfaceForm,\n leadSurfaceForms,\n} from '../model/lead-surfaces'\n\nexport interface LeadSurfacesNoteProps {\n hostId: string\n}\n\n/** The always-on surfaces, named once wherever the note is drawn. */\nexport const LEAD_SURFACES_INTRO =\n 'Leads are created by bookings, forms with lead routing on, imports and New lead'\n/**\n * The rule that keeps one person one record (AGL-3232): a lead is the\n * whole record until it converts, and a person the workspace already holds\n * as a contact is not filed as a lead again.\n */\nexport const LEAD_SURFACES_CONTACTS_TOO =\n 'A lead is its own record until it converts into a contact. Someone who is already a contact is not filed as a lead again — their submission lands on the contact.'\n\n/** One site's forms, sorted into the two answers the note gives. */\nexport interface LeadSurfaceFormsResult {\n /** `routing.lead` is on: every submission with an address files a lead. */\n routed: LeadSurfaceForm[]\n /** The rest, each with its verdict on whether the switch may be offered. */\n unrouted: LeadSurfaceForm[]\n /** The site has more forms than the window; the lists are not the whole answer. */\n truncated: boolean\n status: ReturnType<typeof useFirestoreCollection>['status']\n}\n\n/**\n * The site's forms, ordered by id and bounded at one past the window — the\n * same read the Inbox's form picker makes, ordered by `__name__` because a\n * form saved without a display name would otherwise fall out of an ordered\n * query and vanish from a list about routing. Host-scoped by path, so any\n * member of the site may read it and no scope predicate is needed.\n *\n * One listener per call, so a note that groups several sites mounts one\n * reader per site it shows and nothing for a site it has not opened.\n */\nexport function useLeadSurfaceForms(hostId: string): LeadSurfaceFormsResult {\n const firestore = useFirestore()\n const { data: formDocs, status } = useFirestoreCollection<\n Record<string, unknown> & { $id: string }\n >(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'forms'),\n orderBy('__name__'),\n limit(LEAD_SURFACE_FORMS_WINDOW + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n return useMemo(() => {\n const forms = leadSurfaceForms(formDocs.slice(0, LEAD_SURFACE_FORMS_WINDOW))\n return {\n routed: forms.filter((form) => form.routed),\n unrouted: forms.filter((form) => !form.routed),\n truncated: formDocs.length > LEAD_SURFACE_FORMS_WINDOW,\n status,\n }\n }, [formDocs, status])\n}\n\n/** The form a switch is mid-flight on — under which site, since a form id is only unique within one. */\nexport interface LeadRoutingTarget {\n hostId: string\n formId: string\n}\n\n/**\n * The switch: one dotted `updateDoc` on `routing.lead`, so a dataset binding\n * beside it is untouched, under the same host-content write the form's page\n * makes. One at a time, whichever site the form is on — two switches in\n * flight would be two toasts racing for the same reader.\n *\n * `siteName` rides into the toast when the note shows more than one site,\n * because \"Contact now files a lead\" is an incomplete sentence to a reader\n * who can see three forms called Contact.\n */\nexport function useTurnOnLeadRouting(): {\n turningOn: LeadRoutingTarget | null\n turnOn: (target: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => Promise<void>\n} {\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const postPluginApi = usePluginApiPost()\n const [turningOn, setTurningOn] = useState<LeadRoutingTarget | null>(null)\n const turnOn = useCallback(\n async ({\n hostId,\n form,\n siteName,\n }: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => {\n if (turningOn) return\n setTurningOn({ hostId, formId: form.$id })\n const where = siteName ? ` on ${siteName}` : ''\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', form.$id), {\n 'routing.lead': true,\n updatedAt: serverTimestamp(),\n })\n // A lead-routing form holds `0` leads until its first (AGL-3330); the\n // counters are the forms plugin's to write, so it recounts them —\n // by address, since this plugin may not import that one.\n void postPluginApi('/api/forms/stats', { hostId, formIds: [form.$id] })\n enqueueSnackbar(\n `\"${form.displayName}\"${where} now files a lead from every submission ` +\n 'that carries an email address.',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar(`Lead routing could not be turned on${where}.`, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setTurningOn(null)\n }\n },\n [turningOn, firestore, enqueueSnackbar, postPluginApi],\n )\n return { turningOn, turnOn }\n}\n\n/** A form by name, linked to its own page when the console can address it. */\nexport function LeadSurfaceFormName(props: {\n form: LeadSurfaceForm\n href: string | null\n}) {\n const { form, href } = props\n return href ? (\n <AppLink href={href}>{form.displayName}</AppLink>\n ) : (\n <span>{form.displayName}</span>\n )\n}\nLeadSurfaceFormName.displayName = 'LeadSurfaceFormName'\n\n/** The routed forms as one clause — \"A, B, and possibly more\" — for a sentence to end. */\nexport function LeadSurfaceFormList(props: {\n forms: LeadSurfaceForm[]\n truncated: boolean\n formHref: (formId: string) => string | null\n}) {\n const { forms, truncated, formHref } = props\n return (\n <>\n {forms.map((form, index) => (\n <span key={form.$id}>\n {index ? ', ' : ''}\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </span>\n ))}\n {truncated ? ', and possibly more' : ''}\n </>\n )\n}\nLeadSurfaceFormList.displayName = 'LeadSurfaceFormList'\n\n/**\n * \"Not routing leads:\" — every form that does not route, with the switch\n * beside the ones that could and the reason beside the ones that cannot.\n *\n * Offered only where `leadSurfaceForms` says the publish check would honor\n * it — a form with no email field, or one that records no consent, is\n * refused with the reason as the tooltip, because a switch that flips and\n * then fails at publish is the exact confusion this note exists to remove.\n */\nexport function UnroutedLeadSurfaces(props: {\n hostId: string\n forms: LeadSurfaceForm[]\n formHref: (formId: string) => string | null\n turningOn: LeadRoutingTarget | null\n onTurnOn: (form: LeadSurfaceForm) => void\n}) {\n const { hostId, forms, formHref, turningOn, onTurnOn } = props\n return (\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Not routing leads:'}\n </Typography>\n {forms.map((form) => {\n const pending =\n turningOn?.hostId === hostId && turningOn.formId === form.$id\n return (\n <Stack\n key={form.$id}\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\" component=\"span\">\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </Typography>\n {/*\n Disabled with the reason as its tooltip rather than absent:\n an absent control and an inapplicable one look alike, and\n only one of them says what to do about it.\n */}\n <Tooltip title={form.blocker ?? ''}>\n <span>\n <Button\n size=\"small\"\n variant=\"text\"\n disabled={!form.canRoute || turningOn !== null}\n onClick={() => onTurnOn(form)}\n >\n {pending ? 'Turning on…' : 'Turn on lead routing'}\n </Button>\n </span>\n </Tooltip>\n </Stack>\n )\n })}\n </Stack>\n )\n}\nUnroutedLeadSurfaces.displayName = 'UnroutedLeadSurfaces'\n\n/**\n * WHAT CREATES A LEAD ON THIS SITE (AGL-2612).\n *\n * Every capture lands in Contacts; a LEAD is filed only by a lead surface —\n * a member sign-up, a booking, or a form whose author declared\n * `routing.lead`. The first two are always on and the third is a switch on\n * each form's own page, which left the Leads list unable to say why one\n * form's people were in it and another's were not. This says so, by name,\n * and offers the switch here for the forms that could carry it.\n *\n * The reader, the switch and the two rows are shared with the\n * organization-level note (`OrgLeadSurfacesNote`), which draws them once\n * per site; this is the one-site composition. The form links resolve the\n * site's console route from the host index, because under a site that is\n * the only address the note is handed.\n */\nexport function LeadSurfacesNote(props: LeadSurfacesNoteProps) {\n const { hostId } = props\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const { routed, unrouted, truncated, status } = useLeadSurfaceForms(hostId)\n const { turningOn, turnOn } = useTurnOnLeadRouting()\n\n const formHref = useCallback(\n (formId: string) =>\n orgSlug && host\n ? pluginRecordHref('form', { orgSlug, host }, formId)\n : null,\n [orgSlug, host],\n )\n\n return (\n <Stack spacing={0.5}>\n <Typography variant=\"body2\" color=\"text.secondary\" component=\"div\">\n {LEAD_SURFACES_INTRO}\n {status === 'loading' ? (\n '…'\n ) : routed.length ? (\n <>\n {': '}\n <LeadSurfaceFormList forms={routed} truncated={truncated} formHref={formHref} />\n {'.'}\n </>\n ) : (\n ' — no form routes leads yet.'\n )}\n {` ${LEAD_SURFACES_CONTACTS_TOO}`}\n </Typography>\n {unrouted.length ? (\n <UnroutedLeadSurfaces\n hostId={hostId}\n forms={unrouted}\n formHref={formHref}\n turningOn={turningOn}\n onTurnOn={(form) => void turnOn({ hostId, form })}\n />\n ) : null}\n </Stack>\n )\n}\nLeadSurfacesNote.displayName = 'LeadSurfacesNote'\n\nexport default LeadSurfacesNote\n"],"names":["pluginRecordHref","AppLink","useSnackbar","useConsoleHostRoute","useFirestore","useFirestoreCollection","usePluginApiPost","Button","Stack","Tooltip","Typography","collection","doc","limit","orderBy","query","serverTimestamp","updateDoc","useCallback","useMemo","useState","LEAD_SURFACE_FORMS_WINDOW","leadSurfaceForms","LEAD_SURFACES_INTRO","LEAD_SURFACES_CONTACTS_TOO","useLeadSurfaceForms","hostId","firestore","data","formDocs","status","idField","forms","slice","routed","filter","form","unrouted","truncated","length","useTurnOnLeadRouting","enqueueSnackbar","postPluginApi","turningOn","setTurningOn","turnOn","siteName","formId","$id","where","updatedAt","formIds","displayName","variant","persist","error","console","allowDuplicate","LeadSurfaceFormName","props","href","span","LeadSurfaceFormList","formHref","map","index","UnroutedLeadSurfaces","onTurnOn","direction","spacing","sx","alignItems","flexWrap","rowGap","color","pending","component","title","blocker","size","disabled","canRoute","onClick","LeadSurfacesNote","orgSlug","subdomain","host"],"mappings":"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"}
@@ -22,6 +22,7 @@ import { Button, Dialog, DialogActions, DialogContent, DialogContentText, Dialog
22
22
  import { doc, serverTimestamp, updateDoc } from "firebase/firestore";
23
23
  import { useEffect, useState } from "react";
24
24
  import { useCrmScope } from "../hooks/use-crm-scope.js";
25
+ import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
25
26
  /** The most a reason may say — shared with the bulk bar's one-reason-for-all (AGL-2662). */ export const UNQUALIFY_REASON_MAX = 500;
26
27
  const REASON_MAX = UNQUALIFY_REASON_MAX;
27
28
  /**
@@ -39,6 +40,7 @@ const REASON_MAX = UNQUALIFY_REASON_MAX;
39
40
  const { orgId } = useCrmScope({
40
41
  hostId
41
42
  });
43
+ const followUpSharing = useCrmSharingFollowUp(hostId, orgId);
42
44
  const firestore = useFirestore();
43
45
  const { enqueueSnackbar } = useSnackbar();
44
46
  const [reason, setReason] = useState('');
@@ -67,6 +69,10 @@ const REASON_MAX = UNQUALIFY_REASON_MAX;
67
69
  await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), _extends({}, fields, {
68
70
  updatedAt: serverTimestamp()
69
71
  }));
72
+ // A rule may share by status (AGL-3336).
73
+ followUpSharing('leads', [
74
+ leadId
75
+ ]);
70
76
  enqueueSnackbar('Lead marked unqualified', {
71
77
  variant: 'success',
72
78
  persist: false
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-unqualify-dialog.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 type { CrmLeadFields } from '@aglyn/aglyn'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n TextField,\n} from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\n\n/** The most a reason may say — shared with the bulk bar's one-reason-for-all (AGL-2662). */\nexport const UNQUALIFY_REASON_MAX = 500\nconst REASON_MAX = UNQUALIFY_REASON_MAX\n\nexport interface LeadUnqualifyDialogProps {\n open: boolean\n onClose: () => void\n /**\n * The lead's own site, or `null` at the organization level (AGL-3278).\n * The write is an org-scoped one-field update; the site is read for the\n * org root, which the mount answers without one.\n */\n hostId: string | null\n leadId: string\n /** How the lead reads in the title — its name, else its address. */\n leadLabel: string\n}\n\n/**\n * Close a lead without converting it, with the reason why (AGL-2608).\n *\n * The reason is REQUIRED. An unqualified lead with no reason is a row that\n * says only \"somebody gave up\", and the one thing a report on lost leads\n * wants to count is why. A client-direct write: `hosts/{hostId}/leads` is\n * not in the catch-all's update exclusions, so a site admin, editor or\n * author may update it, and nothing here needs the server.\n */\nexport function LeadUnqualifyDialog(props: LeadUnqualifyDialogProps) {\n const { open, onClose, hostId, leadId, leadLabel } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const [reason, setReason] = useState('')\n const [busy, setBusy] = useState(false)\n\n useEffect(() => {\n if (open) setReason('')\n }, [open])\n\n const submit = async () => {\n const trimmed = reason.trim()\n if (!trimmed) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setBusy(true)\n try {\n const fields: Required<Pick<CrmLeadFields, 'status' | 'unqualifiedReason'>> = {\n status: 'unqualified',\n unqualifiedReason: trimmed.slice(0, REASON_MAX),\n }\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n ...fields,\n updatedAt: serverTimestamp(),\n })\n enqueueSnackbar('Lead marked unqualified', { variant: 'success', persist: false })\n onClose()\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <DialogTitle>{`Unqualify ${leadLabel}?`}</DialogTitle>\n <DialogContent>\n <DialogContentText sx={{ mb: 2 }}>\n {'The lead stays on file and drops out of the open list. Say why, ' +\n 'so the reason can be counted later.'}\n </DialogContentText>\n <TextField\n label=\"Reason\"\n value={reason}\n onChange={(event) => setReason(event.target.value)}\n multiline\n minRows={2}\n fullWidth\n autoFocus\n slotProps={{ htmlInput: { maxLength: REASON_MAX } }}\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n color=\"warning\"\n onClick={() => void submit()}\n disabled={busy || !reason.trim()}\n >\n {'Unqualify'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nLeadUnqualifyDialog.displayName = 'LeadUnqualifyDialog'\n\nexport default LeadUnqualifyDialog\n"],"names":["useSnackbar","useFirestore","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","TextField","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","UNQUALIFY_REASON_MAX","REASON_MAX","LeadUnqualifyDialog","props","open","onClose","hostId","leadId","leadLabel","orgId","firestore","enqueueSnackbar","reason","setReason","busy","setBusy","submit","trimmed","trim","variant","persist","fields","status","unqualifiedReason","slice","updatedAt","error","Error","message","maxWidth","fullWidth","sx","mb","label","value","onChange","event","target","multiline","minRows","autoFocus","slotProps","htmlInput","maxLength","onClick","disabled","color","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAGA,SAASA,WAAW,QAAQ,8BAA6B;AACzD,SAASC,YAAY,QAAQ,iCAAgC;AAC7D,SACEC,MAAM,EACNC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,iBAAiB,EACjBC,WAAW,EACXC,SAAS,QACJ,gBAAe;AACtB,SAASC,GAAG,EAAEC,eAAe,EAAEC,SAAS,QAAQ,qBAAoB;AACpE,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,WAAW,QAAQ,4BAAwB;AAEpD,0FAA0F,GAC1F,OAAO,MAAMC,uBAAuB,IAAG;AACvC,MAAMC,aAAaD;AAgBnB;;;;;;;;CAQC,GACD,OAAO,SAASE,oBAAoBC,KAA+B;IACjE,MAAM,EAAEC,IAAI,EAAEC,OAAO,EAAEC,MAAM,EAAEC,MAAM,EAAEC,SAAS,EAAE,GAAGL;IACrD,0EAA0E;IAC1E,4DAA4D;IAC5D,MAAM,EAAEM,KAAK,EAAE,GAAGV,YAAY;QAAEO;IAAO;IACvC,MAAMI,YAAYxB;IAClB,MAAM,EAAEyB,eAAe,EAAE,GAAG1B;IAC5B,MAAM,CAAC2B,QAAQC,UAAU,GAAGf,SAAS;IACrC,MAAM,CAACgB,MAAMC,QAAQ,GAAGjB,SAAS;IAEjCD,UAAU;QACR,IAAIO,MAAMS,UAAU;IACtB,GAAG;QAACT;KAAK;IAET,MAAMY,SAAS;QACb,MAAMC,UAAUL,OAAOM,IAAI;QAC3B,IAAI,CAACD,SAAS;QACd,IAAI,CAACR,OAAO;YACVE,gBAAgB,yDAAyD;gBACvEQ,SAAS;gBACTC,SAAS;YACX;YACA;QACF;QACAL,QAAQ;QACR,IAAI;YACF,MAAMM,SAAwE;gBAC5EC,QAAQ;gBACRC,mBAAmBN,QAAQO,KAAK,CAAC,GAAGvB;YACtC;YACA,MAAML,UAAUF,IAAIgB,WAAW,QAAQD,OAAO,SAASF,SAAS,aAC3Dc;gBACHI,WAAW9B;;YAEbgB,gBAAgB,2BAA2B;gBAAEQ,SAAS;gBAAWC,SAAS;YAAM;YAChFf;QACF,EAAE,OAAOqB,OAAO;YACdf,gBACEe,iBAAiBC,QAAQD,MAAME,OAAO,GAAG,kCACzC;gBAAET,SAAS;YAAQ;QAEvB,SAAU;YACRJ,QAAQ;QACV;IACF;IAEA,qBACE,MAAC3B;QAAOgB,MAAMA;QAAMC,SAASA;QAASwB,UAAS;QAAKC,SAAS;;0BAC3D,KAACtC;0BAAa,CAAC,UAAU,EAAEgB,UAAU,CAAC,CAAC;;0BACvC,MAAClB;;kCACC,KAACC;wBAAkBwC,IAAI;4BAAEC,IAAI;wBAAE;kCAC5B,qEACC;;kCAEJ,KAACvC;wBACCwC,OAAM;wBACNC,OAAOtB;wBACPuB,UAAU,CAACC,QAAUvB,UAAUuB,MAAMC,MAAM,CAACH,KAAK;wBACjDI,SAAS;wBACTC,SAAS;wBACTT,SAAS;wBACTU,SAAS;wBACTC,WAAW;4BAAEC,WAAW;gCAAEC,WAAW1C;4BAAW;wBAAE;;;;0BAGtD,MAACZ;;kCACC,KAACF;wBAAOyD,SAASvC;wBAASwC,UAAU/B;kCACjC;;kCAEH,KAAC3B;wBACCgC,SAAQ;wBACR2B,OAAM;wBACNF,SAAS,IAAM,KAAK5B;wBACpB6B,UAAU/B,QAAQ,CAACF,OAAOM,IAAI;kCAE7B;;;;;;AAKX;AACAhB,oBAAoB6C,WAAW,GAAG;AAElC,eAAe7C,oBAAmB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-unqualify-dialog.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 type { CrmLeadFields } from '@aglyn/aglyn'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n TextField,\n} from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\n/** The most a reason may say — shared with the bulk bar's one-reason-for-all (AGL-2662). */\nexport const UNQUALIFY_REASON_MAX = 500\nconst REASON_MAX = UNQUALIFY_REASON_MAX\n\nexport interface LeadUnqualifyDialogProps {\n open: boolean\n onClose: () => void\n /**\n * The lead's own site, or `null` at the organization level (AGL-3278).\n * The write is an org-scoped one-field update; the site is read for the\n * org root, which the mount answers without one.\n */\n hostId: string | null\n leadId: string\n /** How the lead reads in the title — its name, else its address. */\n leadLabel: string\n}\n\n/**\n * Close a lead without converting it, with the reason why (AGL-2608).\n *\n * The reason is REQUIRED. An unqualified lead with no reason is a row that\n * says only \"somebody gave up\", and the one thing a report on lost leads\n * wants to count is why. A client-direct write: `hosts/{hostId}/leads` is\n * not in the catch-all's update exclusions, so a site admin, editor or\n * author may update it, and nothing here needs the server.\n */\nexport function LeadUnqualifyDialog(props: LeadUnqualifyDialogProps) {\n const { open, onClose, hostId, leadId, leadLabel } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const [reason, setReason] = useState('')\n const [busy, setBusy] = useState(false)\n\n useEffect(() => {\n if (open) setReason('')\n }, [open])\n\n const submit = async () => {\n const trimmed = reason.trim()\n if (!trimmed) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setBusy(true)\n try {\n const fields: Required<Pick<CrmLeadFields, 'status' | 'unqualifiedReason'>> = {\n status: 'unqualified',\n unqualifiedReason: trimmed.slice(0, REASON_MAX),\n }\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n ...fields,\n updatedAt: serverTimestamp(),\n })\n // A rule may share by status (AGL-3336).\n followUpSharing('leads', [leadId])\n enqueueSnackbar('Lead marked unqualified', { variant: 'success', persist: false })\n onClose()\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <DialogTitle>{`Unqualify ${leadLabel}?`}</DialogTitle>\n <DialogContent>\n <DialogContentText sx={{ mb: 2 }}>\n {'The lead stays on file and drops out of the open list. Say why, ' +\n 'so the reason can be counted later.'}\n </DialogContentText>\n <TextField\n label=\"Reason\"\n value={reason}\n onChange={(event) => setReason(event.target.value)}\n multiline\n minRows={2}\n fullWidth\n autoFocus\n slotProps={{ htmlInput: { maxLength: REASON_MAX } }}\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n color=\"warning\"\n onClick={() => void submit()}\n disabled={busy || !reason.trim()}\n >\n {'Unqualify'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nLeadUnqualifyDialog.displayName = 'LeadUnqualifyDialog'\n\nexport default LeadUnqualifyDialog\n"],"names":["useSnackbar","useFirestore","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","TextField","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","useCrmSharingFollowUp","UNQUALIFY_REASON_MAX","REASON_MAX","LeadUnqualifyDialog","props","open","onClose","hostId","leadId","leadLabel","orgId","followUpSharing","firestore","enqueueSnackbar","reason","setReason","busy","setBusy","submit","trimmed","trim","variant","persist","fields","status","unqualifiedReason","slice","updatedAt","error","Error","message","maxWidth","fullWidth","sx","mb","label","value","onChange","event","target","multiline","minRows","autoFocus","slotProps","htmlInput","maxLength","onClick","disabled","color","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAGA,SAASA,WAAW,QAAQ,8BAA6B;AACzD,SAASC,YAAY,QAAQ,iCAAgC;AAC7D,SACEC,MAAM,EACNC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,iBAAiB,EACjBC,WAAW,EACXC,SAAS,QACJ,gBAAe;AACtB,SAASC,GAAG,EAAEC,eAAe,EAAEC,SAAS,QAAQ,qBAAoB;AACpE,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,WAAW,QAAQ,4BAAwB;AACpD,SAASC,qBAAqB,QAAQ,8BAA0B;AAEhE,0FAA0F,GAC1F,OAAO,MAAMC,uBAAuB,IAAG;AACvC,MAAMC,aAAaD;AAgBnB;;;;;;;;CAQC,GACD,OAAO,SAASE,oBAAoBC,KAA+B;IACjE,MAAM,EAAEC,IAAI,EAAEC,OAAO,EAAEC,MAAM,EAAEC,MAAM,EAAEC,SAAS,EAAE,GAAGL;IACrD,0EAA0E;IAC1E,4DAA4D;IAC5D,MAAM,EAAEM,KAAK,EAAE,GAAGX,YAAY;QAAEQ;IAAO;IACvC,MAAMI,kBAAkBX,sBAAsBO,QAAQG;IACtD,MAAME,YAAY1B;IAClB,MAAM,EAAE2B,eAAe,EAAE,GAAG5B;IAC5B,MAAM,CAAC6B,QAAQC,UAAU,GAAGjB,SAAS;IACrC,MAAM,CAACkB,MAAMC,QAAQ,GAAGnB,SAAS;IAEjCD,UAAU;QACR,IAAIQ,MAAMU,UAAU;IACtB,GAAG;QAACV;KAAK;IAET,MAAMa,SAAS;QACb,MAAMC,UAAUL,OAAOM,IAAI;QAC3B,IAAI,CAACD,SAAS;QACd,IAAI,CAACT,OAAO;YACVG,gBAAgB,yDAAyD;gBACvEQ,SAAS;gBACTC,SAAS;YACX;YACA;QACF;QACAL,QAAQ;QACR,IAAI;YACF,MAAMM,SAAwE;gBAC5EC,QAAQ;gBACRC,mBAAmBN,QAAQO,KAAK,CAAC,GAAGxB;YACtC;YACA,MAAMN,UAAUF,IAAIkB,WAAW,QAAQF,OAAO,SAASF,SAAS,aAC3De;gBACHI,WAAWhC;;YAEb,yCAAyC;YACzCgB,gBAAgB,SAAS;gBAACH;aAAO;YACjCK,gBAAgB,2BAA2B;gBAAEQ,SAAS;gBAAWC,SAAS;YAAM;YAChFhB;QACF,EAAE,OAAOsB,OAAO;YACdf,gBACEe,iBAAiBC,QAAQD,MAAME,OAAO,GAAG,kCACzC;gBAAET,SAAS;YAAQ;QAEvB,SAAU;YACRJ,QAAQ;QACV;IACF;IAEA,qBACE,MAAC7B;QAAOiB,MAAMA;QAAMC,SAASA;QAASyB,UAAS;QAAKC,SAAS;;0BAC3D,KAACxC;0BAAa,CAAC,UAAU,EAAEiB,UAAU,CAAC,CAAC;;0BACvC,MAACnB;;kCACC,KAACC;wBAAkB0C,IAAI;4BAAEC,IAAI;wBAAE;kCAC5B,qEACC;;kCAEJ,KAACzC;wBACC0C,OAAM;wBACNC,OAAOtB;wBACPuB,UAAU,CAACC,QAAUvB,UAAUuB,MAAMC,MAAM,CAACH,KAAK;wBACjDI,SAAS;wBACTC,SAAS;wBACTT,SAAS;wBACTU,SAAS;wBACTC,WAAW;4BAAEC,WAAW;gCAAEC,WAAW3C;4BAAW;wBAAE;;;;0BAGtD,MAACb;;kCACC,KAACF;wBAAO2D,SAASxC;wBAASyC,UAAU/B;kCACjC;;kCAEH,KAAC7B;wBACCkC,SAAQ;wBACR2B,OAAM;wBACNF,SAAS,IAAM,KAAK5B;wBACpB6B,UAAU/B,QAAQ,CAACF,OAAOM,IAAI;kCAE7B;;;;;;AAKX;AACAjB,oBAAoB8C,WAAW,GAAG;AAElC,eAAe9C,oBAAmB"}
@@ -40,7 +40,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
40
40
  * menu and the inline select do. A lead already closed is not unqualified
41
41
  * twice. A lead already at the asked status is skipped rather than rewritten
42
42
  * so "Nothing to change" means what it says.
43
- */ import { CRM_LEAD_STATUS_LABELS, crmLeadStatus, isCrmLeadOpen, leadPrimaryGroup, readCampaignIds } from "@aglyn/aglyn";
43
+ */ import { CRM_LEAD_STATUS_LABELS, crmLeadStatus, isCrmLeadOpen, readCampaignIds } from "@aglyn/aglyn";
44
44
  import CampaignPicker from "@aglyn/shared-ui-email-campaigns/components/campaign-picker.component";
45
45
  import { useFirestore } from "@aglyn/tenant-feature-instance";
46
46
  import { Button, MenuItem, TextField } from "@mui/material";
@@ -55,8 +55,11 @@ import { crmBulkWriters, runCrmBulkWrites } from "../model/crm-bulk-writes.js";
55
55
  import { leadsCsv } from "../model/leads-csv.js";
56
56
  import { CrmBulkBarFrame, CrmBulkValueDialog, countNoun } from "./crm-bulk-bar-frame.js";
57
57
  import CrmExportAllButton from "./crm-export-all-button.js";
58
+ import { CrmBulkShareButton } from "./record-sharing-card.js";
58
59
  import { LeadOwnerSelect } from "./lead-owner-select.js";
59
60
  import { UNQUALIFY_REASON_MAX } from "./lead-unqualify-dialog.js";
61
+ import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
62
+ import { leadPrimaryGroup } from "../model/contact-holder.js";
60
63
  const NOUN = {
61
64
  singular: 'lead',
62
65
  plural: 'leads'
@@ -82,6 +85,7 @@ function LeadsBulkBarBody(props) {
82
85
  const { rows, selected, onSelectedChange, roster, csv } = props;
83
86
  const orgId = (_props_orgId = props.orgId) != null ? _props_orgId : null;
84
87
  const hostId = (_props_hostId = props.hostId) != null ? _props_hostId : null;
88
+ const followUpSharing = useCrmSharingFollowUp(hostId, orgId);
85
89
  const firestore = useFirestore();
86
90
  const { busy, report, apply, dismissReport } = useCrmBulkApply({
87
91
  recordKind: 'lead'
@@ -140,11 +144,22 @@ function LeadsBulkBarBody(props) {
140
144
  const runPlan = useCallback((plan, done)=>apply({
141
145
  attempted: plan.writes.length,
142
146
  skipped: plan.skipped,
143
- job: ()=>runCrmBulkWrites(writers, plan.writes, (write)=>write.label),
147
+ job: async ()=>{
148
+ const outcome = await runCrmBulkWrites(writers, plan.writes, (write)=>write.label);
149
+ // A client-direct write owes the sharing rules a re-evaluation (AGL-3336).
150
+ followUpSharing('leads', plan.writes.filter((write)=>write.kind === 'update').map((write)=>{
151
+ var _ref;
152
+ var _rows_find;
153
+ return (_ref = (_rows_find = rows.find((row)=>row.$id === write.id)) == null ? void 0 : _rows_find.leadId) != null ? _ref : write.id;
154
+ }));
155
+ return outcome;
156
+ },
144
157
  done
145
158
  }), [
146
159
  apply,
147
- writers
160
+ writers,
161
+ rows,
162
+ followUpSharing
148
163
  ]);
149
164
  const handleApply = useCallback(async ()=>{
150
165
  if (!pending) return;
@@ -392,6 +407,16 @@ function LeadsBulkBarBody(props) {
392
407
  onClick: ()=>openAction('campaign'),
393
408
  children: 'Add to campaign'
394
409
  }),
410
+ /*#__PURE__*/ _jsx(CrmBulkShareButton, {
411
+ object: "leads",
412
+ ids: selectedRows.map((row)=>{
413
+ var _row_leadId;
414
+ return (_row_leadId = row.leadId) != null ? _row_leadId : row.$id;
415
+ }),
416
+ hostId: hostId,
417
+ orgId: orgId,
418
+ disabled: busy
419
+ }),
395
420
  /*#__PURE__*/ _jsx(Button, {
396
421
  size: "small",
397
422
  disabled: busy,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/leads-bulk-bar.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\n/**\n * The bar over the Leads table, for whatever rows are ticked (AGL-2662).\n *\n * Hand them to an owner, set their status, unqualify them with one reason,\n * or take them into a spreadsheet. Every act here is a document write and\n * nothing more — a lead's status and owner are the team's own notes on a\n * capture, with no event and no notification behind them, which is why the\n * row's inline select writes them client-direct — so the whole bar goes\n * through the shared batched runner under the same rules.\n *\n * ## A row names its own site\n *\n * A lead lives at `orgs/{orgId}/leads/{leadId}` (AGL-3275), and at the organization\n * level the selection spans sites, so the reference for a write is built\n * from the ROW rather than from a scope the bar was handed: the row knows\n * its site, the bar need not. Under a site every row names the same one.\n *\n * ## What is declined, by name\n *\n * A converted lead's status IS its conversion, stamped by the route, and\n * the bar leaves it alone under Set status and Unqualify the way the row\n * menu and the inline select do. A lead already closed is not unqualified\n * twice. A lead already at the asked status is skipped rather than rewritten\n * so \"Nothing to change\" means what it says.\n */\n\nimport {\n CRM_LEAD_STATUS_LABELS,\n type CrmLeadFields,\n type CrmLeadStatus,\n crmLeadStatus,\n isCrmLeadOpen,\n leadPrimaryGroup,\n readCampaignIds,\n} from '@aglyn/aglyn'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { Button, MenuItem, TextField } from '@mui/material'\nimport { arrayUnion, deleteField, doc, serverTimestamp } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useCampaignFilingLog } from '../hooks/use-campaign-filing-log'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport { useCrmCampaigns } from '../hooks/use-crm-campaigns'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmClientListFields } from '../model/crm-list-query'\nimport {\n type CrmBulkPlan,\n type CrmBulkSkip,\n type CrmBulkWrite,\n crmBulkWriters,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport { type LeadCsvOptions, leadsCsv } from '../model/leads-csv'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { UNQUALIFY_REASON_MAX } from './lead-unqualify-dialog'\n\n/**\n * One row of the Leads list as the bar needs it: the document's fields,\n * the grid's key, and which document under which site a write names.\n */\nexport type LeadBulkRow = Record<string, unknown> &\n CrmLeadFields & { $id: string; leadId: string }\n\nexport interface LeadsBulkBarProps {\n rows: readonly LeadBulkRow[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /** The section's roster — already read for the Owner column. */\n roster: OrgMemberOptions\n /** How the export names the owner and, at the org level, the site — the list's own. */\n csv?: LeadCsvOptions\n /** The organization these leads belong to; null while it is unresolved. */\n orgId?: string | null\n /**\n * The site the list is read under, or `null` at the organization level\n * where the complete export spans every site (AGL-2662).\n */\n hostId?: string | null\n /**\n * The org the shell passed, for the scope a filing entry carries\n * (AGL-3274): Add to campaign writes one on each lead's Activity, and\n * the entry is visible to whoever sees a record made on this site.\n */\n org?: Record<string, unknown> | null\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'lead', plural: 'leads' }\n\ntype PendingAction = 'owner' | 'status' | 'unqualify' | 'campaign'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n owner: 'Set the owner',\n status: 'Set the status',\n unqualify: 'Unqualify',\n campaign: 'Add to campaign',\n}\n\n/** The statuses the bar can set — the open ones; closing goes through Unqualify. */\nconst SETTABLE_STATUSES: readonly CrmLeadStatus[] = ['new', 'working']\n\n/** The label a report lists a lead under — its name, else its address. */\nconst labelOf = (lead: LeadBulkRow): string =>\n String(lead['name'] || lead['email'] || lead.leadId)\n\nexport function LeadsBulkBar(props: LeadsBulkBarProps) {\n if (!props.selected.length) return null\n return <LeadsBulkBarBody {...props} />\n}\nLeadsBulkBar.displayName = 'LeadsBulkBar'\n\nfunction LeadsBulkBarBody(props: LeadsBulkBarProps) {\n const { rows, selected, onSelectedChange, roster, csv } = props\n const orgId = props.orgId ?? null\n const hostId = props.hostId ?? null\n const firestore = useFirestore()\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'lead' })\n\n const selectedRows = useMemo(() => {\n const chosen = new Set(selected)\n return rows.filter((row) => chosen.has(row.$id))\n }, [rows, selected])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n /*\n * The campaigns to add the selection to (AGL-3254), and the containers\n * the picker offers — read only while that dialog is open. Under a site,\n * the campaigns placed on it; at the organization level, where a\n * selection spans sites, every campaign in the org.\n */\n const [campaignIds, setCampaignIds] = useState<string[]>([])\n const campaigns = useCrmCampaigns(\n { hostId, orgId },\n { enabled: pending === 'campaign' },\n )\n const logFiling = useCampaignFilingLog({ orgId, hostId, org: props.org })\n\n // The reference a write names is the row's own site and document.\n const writers = useMemo(() => {\n const byId = new Map(rows.map((row) => [row.$id, row]))\n return crmBulkWriters(firestore, (id) => {\n const row = byId.get(id)\n // One org collection (AGL-3275): the row no longer names a site, and\n // the bulk write addresses the same document at either level.\n // `orgs//leads` is a path Firestore accepts the shape of and nothing\n // owns, so an absent org refuses the write rather than composing one.\n if (!orgId) throw new Error('[crm] a lead write needs an organization')\n return doc(firestore, 'orgs', orgId, 'leads', row?.leadId ?? id)\n })\n }, [firestore, rows])\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setCampaignIds([])\n setPending(action)\n }\n\n const runPlan = useCallback(\n (plan: CrmBulkPlan, done: (count: number) => string) =>\n apply({\n attempted: plan.writes.length,\n skipped: plan.skipped,\n job: () => runCrmBulkWrites(writers, plan.writes, (write) => write.label),\n done,\n }),\n [apply, writers],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending) return\n const action = pending\n setPending(null)\n const writes: CrmBulkWrite[] = []\n const skipped: CrmBulkSkip[] = []\n if (action === 'owner') {\n for (const lead of selectedRows) {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: { ownerUid: value ? value : deleteField(), updatedAt: serverTimestamp() },\n })\n }\n await runPlan(\n { writes, skipped },\n (count) => (value ? `Owner set on ${countNoun(count, NOUN)}` : `Owner cleared on ${countNoun(count, NOUN)}`),\n )\n return\n }\n if (action === 'campaign') {\n // Added to what each lead carries — `arrayUnion`, the way a sequence's\n // enroll and the import add one — never in place of it.\n for (const lead of selectedRows) {\n // The union as it will stand, for the list field it moves (AGL-3321).\n const union = [...new Set([...readCampaignIds(lead), ...campaignIds])]\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n campaignIds: arrayUnion(...campaignIds),\n ...crmClientListFields('leads', lead, { campaignIds: union }, ['scopedCampaignIds']),\n updatedAt: serverTimestamp(),\n },\n })\n }\n const outcome = await runPlan(\n { writes, skipped },\n (count) =>\n `Added ${countNoun(count, NOUN)} to ${campaignIds.length === 1 ? 'the campaign' : `${campaignIds.length} campaigns`}`,\n )\n // What landed is written on each lead's Activity (AGL-3274): one\n // \"Filed under\" per campaign the lead was not already in, and none\n // for a lead the store refused — the row's own document is the\n // membership, and the entry only records the act.\n const refused = new Set(outcome.refused.map((entry) => entry.label))\n const named = campaignIds.map((campaignId) => ({\n id: campaignId,\n name: campaigns.options.find((option) => option.value === campaignId)?.label ?? '',\n }))\n for (const lead of selectedRows) {\n if (refused.has(labelOf(lead))) continue\n const already = readCampaignIds(lead)\n const filed = named.filter((campaign) => !already.includes(campaign.id))\n // At the organization level each entry carries its own lead's first\n // capturing site, the one the lead's page files as.\n if (filed.length) {\n await logFiling(\n { leadId: lead.leadId },\n { filed },\n hostId ? null : leadPrimaryGroup(lead, props.org ?? null).hostId || null,\n )\n }\n }\n return\n }\n if (action === 'status') {\n const status = value as CrmLeadStatus\n for (const lead of selectedRows) {\n const current = crmLeadStatus(lead)\n if (lead.convertedContactId) {\n skipped.push({ label: labelOf(lead), reason: 'was converted' })\n } else if (current === status) {\n skipped.push({\n label: labelOf(lead),\n reason: `already ${CRM_LEAD_STATUS_LABELS[status]}`,\n })\n } else {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n status,\n // Reopening an unqualified lead clears its reason, as the row does.\n ...(current === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n updatedAt: serverTimestamp(),\n },\n })\n }\n }\n await runPlan(\n { writes, skipped },\n (count) => `Status set on ${countNoun(count, NOUN)}`,\n )\n return\n }\n const reason = value.trim().slice(0, UNQUALIFY_REASON_MAX)\n for (const lead of selectedRows) {\n if (lead.convertedContactId) {\n skipped.push({ label: labelOf(lead), reason: 'was converted' })\n } else if (!isCrmLeadOpen(lead)) {\n skipped.push({ label: labelOf(lead), reason: 'is already closed' })\n } else {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n status: 'unqualified' satisfies CrmLeadStatus,\n unqualifiedReason: reason,\n updatedAt: serverTimestamp(),\n },\n })\n }\n }\n await runPlan(\n { writes, skipped },\n (count) => `Marked ${countNoun(count, NOUN)} unqualified`,\n )\n }, [pending, value, campaignIds, campaigns.options, selectedRows, runPlan, logFiling, hostId, props.org])\n\n const handleExport = useCallback(() => {\n downloadTextFile('leads-selected.csv', 'text/csv', leadsCsv(selectedRows, csv))\n }, [selectedRows, csv])\n\n const canApply =\n pending === 'owner' ||\n (pending === 'campaign'\n ? campaignIds.length > 0\n : pending === 'status'\n ? Boolean(value)\n : Boolean(value.trim()))\n\n return (\n <CrmBulkBarFrame\n count={selected.length}\n noun={NOUN}\n busy={busy}\n onClear={() => onSelectedChange([])}\n report={report}\n onDismissReport={dismissReport}\n extras={\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={canApply}\n applyLabel={pending === 'unqualify' ? 'Unqualify' : pending === 'campaign' ? 'Add' : undefined}\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'owner' ? (\n <LeadOwnerSelect value={value} onChange={setValue} roster={roster} size=\"medium\" />\n ) : pending === 'status' ? (\n <TextField\n select\n size=\"small\"\n label=\"Status\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText=\"Closing a lead goes through Unqualify, which asks why.\"\n >\n {SETTABLE_STATUSES.map((status) => (\n <MenuItem key={status} value={status}>\n {CRM_LEAD_STATUS_LABELS[status]}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'unqualify' ? (\n <TextField\n label=\"Reason\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n multiline\n minRows={2}\n autoFocus\n helperText=\"One reason for every selected lead, kept on each so it can be counted later.\"\n slotProps={{ htmlInput: { maxLength: UNQUALIFY_REASON_MAX } }}\n />\n ) : pending === 'campaign' ? (\n <CampaignPicker\n options={campaigns.options}\n value={campaignIds}\n onChange={setCampaignIds}\n helperText=\"Added to the campaigns each selected lead is already in. It does not decide who a campaign mails.\"\n empty={campaigns.ready && !campaigns.options.length}\n emptyText={\n hostId\n ? 'This site has no campaigns yet. Create one from Marketing to file leads under it.'\n : 'There are no campaigns yet. Create one from Marketing to file leads under it.'\n }\n />\n ) : null}\n </CrmBulkValueDialog>\n }\n >\n <Button size=\"small\" disabled={busy} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('status')}>\n {'Set status'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('unqualify')}>\n {'Unqualify'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('campaign')}>\n {'Add to campaign'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={handleExport}>\n {'Export CSV'}\n </Button>\n {/*\n The selection's file is the rows on screen; this one is the whole\n collection, streamed by the server (AGL-2662).\n */}\n <CrmExportAllButton\n resource=\"leads\"\n orgId={orgId}\n hostId={hostId}\n disabled={busy}\n />\n </CrmBulkBarFrame>\n )\n}\nLeadsBulkBarBody.displayName = 'LeadsBulkBarBody'\n\nexport default LeadsBulkBar\n"],"names":["CRM_LEAD_STATUS_LABELS","crmLeadStatus","isCrmLeadOpen","leadPrimaryGroup","readCampaignIds","CampaignPicker","useFirestore","Button","MenuItem","TextField","arrayUnion","deleteField","doc","serverTimestamp","useCallback","useMemo","useState","useCampaignFilingLog","useCrmBulkApply","useCrmCampaigns","downloadTextFile","crmClientListFields","crmBulkWriters","runCrmBulkWrites","leadsCsv","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","LeadOwnerSelect","UNQUALIFY_REASON_MAX","NOUN","singular","plural","ACTION_TITLES","owner","status","unqualify","campaign","SETTABLE_STATUSES","labelOf","lead","String","leadId","LeadsBulkBar","props","selected","length","LeadsBulkBarBody","displayName","rows","onSelectedChange","roster","csv","orgId","hostId","firestore","busy","report","apply","dismissReport","recordKind","selectedRows","chosen","Set","filter","row","has","$id","pending","setPending","value","setValue","campaignIds","setCampaignIds","campaigns","enabled","logFiling","org","writers","byId","Map","map","id","get","Error","openAction","action","runPlan","plan","done","attempted","writes","skipped","job","write","label","handleApply","push","kind","data","ownerUid","updatedAt","count","union","outcome","refused","entry","named","campaignId","name","options","find","option","already","filed","includes","current","convertedContactId","reason","unqualifiedReason","trim","slice","handleExport","canApply","Boolean","noun","onClear","onDismissReport","extras","open","title","applyLabel","undefined","onClose","onApply","onChange","size","select","event","target","helperText","multiline","minRows","autoFocus","slotProps","htmlInput","maxLength","empty","ready","emptyText","disabled","onClick","resource"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/leads-bulk-bar.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\n/**\n * The bar over the Leads table, for whatever rows are ticked (AGL-2662).\n *\n * Hand them to an owner, set their status, unqualify them with one reason,\n * or take them into a spreadsheet. Every act here is a document write and\n * nothing more — a lead's status and owner are the team's own notes on a\n * capture, with no event and no notification behind them, which is why the\n * row's inline select writes them client-direct — so the whole bar goes\n * through the shared batched runner under the same rules.\n *\n * ## A row names its own site\n *\n * A lead lives at `orgs/{orgId}/leads/{leadId}` (AGL-3275), and at the organization\n * level the selection spans sites, so the reference for a write is built\n * from the ROW rather than from a scope the bar was handed: the row knows\n * its site, the bar need not. Under a site every row names the same one.\n *\n * ## What is declined, by name\n *\n * A converted lead's status IS its conversion, stamped by the route, and\n * the bar leaves it alone under Set status and Unqualify the way the row\n * menu and the inline select do. A lead already closed is not unqualified\n * twice. A lead already at the asked status is skipped rather than rewritten\n * so \"Nothing to change\" means what it says.\n */\n\nimport {\n CRM_LEAD_STATUS_LABELS,\n type CrmLeadFields,\n type CrmLeadStatus,\n crmLeadStatus,\n isCrmLeadOpen,\n readCampaignIds,\n} from '@aglyn/aglyn'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { Button, MenuItem, TextField } from '@mui/material'\nimport { arrayUnion, deleteField, doc, serverTimestamp } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useCampaignFilingLog } from '../hooks/use-campaign-filing-log'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport { useCrmCampaigns } from '../hooks/use-crm-campaigns'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmClientListFields } from '../model/crm-list-query'\nimport {\n type CrmBulkPlan,\n type CrmBulkSkip,\n type CrmBulkWrite,\n crmBulkWriters,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport { type LeadCsvOptions, leadsCsv } from '../model/leads-csv'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { CrmBulkShareButton } from './record-sharing-card'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { UNQUALIFY_REASON_MAX } from './lead-unqualify-dialog'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\nimport { leadPrimaryGroup } from '../model/contact-holder'\n\n/**\n * One row of the Leads list as the bar needs it: the document's fields,\n * the grid's key, and which document under which site a write names.\n */\nexport type LeadBulkRow = Record<string, unknown> &\n CrmLeadFields & { $id: string; leadId: string }\n\nexport interface LeadsBulkBarProps {\n rows: readonly LeadBulkRow[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /** The section's roster — already read for the Owner column. */\n roster: OrgMemberOptions\n /** How the export names the owner and, at the org level, the site — the list's own. */\n csv?: LeadCsvOptions\n /** The organization these leads belong to; null while it is unresolved. */\n orgId?: string | null\n /**\n * The site the list is read under, or `null` at the organization level\n * where the complete export spans every site (AGL-2662).\n */\n hostId?: string | null\n /**\n * The org the shell passed, for the scope a filing entry carries\n * (AGL-3274): Add to campaign writes one on each lead's Activity, and\n * the entry is visible to whoever sees a record made on this site.\n */\n org?: Record<string, unknown> | null\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'lead', plural: 'leads' }\n\ntype PendingAction = 'owner' | 'status' | 'unqualify' | 'campaign'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n owner: 'Set the owner',\n status: 'Set the status',\n unqualify: 'Unqualify',\n campaign: 'Add to campaign',\n}\n\n/** The statuses the bar can set — the open ones; closing goes through Unqualify. */\nconst SETTABLE_STATUSES: readonly CrmLeadStatus[] = ['new', 'working']\n\n/** The label a report lists a lead under — its name, else its address. */\nconst labelOf = (lead: LeadBulkRow): string =>\n String(lead['name'] || lead['email'] || lead.leadId)\n\nexport function LeadsBulkBar(props: LeadsBulkBarProps) {\n if (!props.selected.length) return null\n return <LeadsBulkBarBody {...props} />\n}\nLeadsBulkBar.displayName = 'LeadsBulkBar'\n\nfunction LeadsBulkBarBody(props: LeadsBulkBarProps) {\n const { rows, selected, onSelectedChange, roster, csv } = props\n const orgId = props.orgId ?? null\n const hostId = props.hostId ?? null\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n const firestore = useFirestore()\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'lead' })\n\n const selectedRows = useMemo(() => {\n const chosen = new Set(selected)\n return rows.filter((row) => chosen.has(row.$id))\n }, [rows, selected])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n /*\n * The campaigns to add the selection to (AGL-3254), and the containers\n * the picker offers — read only while that dialog is open. Under a site,\n * the campaigns placed on it; at the organization level, where a\n * selection spans sites, every campaign in the org.\n */\n const [campaignIds, setCampaignIds] = useState<string[]>([])\n const campaigns = useCrmCampaigns(\n { hostId, orgId },\n { enabled: pending === 'campaign' },\n )\n const logFiling = useCampaignFilingLog({ orgId, hostId, org: props.org })\n\n // The reference a write names is the row's own site and document.\n const writers = useMemo(() => {\n const byId = new Map(rows.map((row) => [row.$id, row]))\n return crmBulkWriters(firestore, (id) => {\n const row = byId.get(id)\n // One org collection (AGL-3275): the row no longer names a site, and\n // the bulk write addresses the same document at either level.\n // `orgs//leads` is a path Firestore accepts the shape of and nothing\n // owns, so an absent org refuses the write rather than composing one.\n if (!orgId) throw new Error('[crm] a lead write needs an organization')\n return doc(firestore, 'orgs', orgId, 'leads', row?.leadId ?? id)\n })\n }, [firestore, rows])\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setCampaignIds([])\n setPending(action)\n }\n\n const runPlan = useCallback(\n (plan: CrmBulkPlan, done: (count: number) => string) =>\n apply({\n attempted: plan.writes.length,\n skipped: plan.skipped,\n job: async () => {\n const outcome = await runCrmBulkWrites(writers, plan.writes, (write) => write.label)\n // A client-direct write owes the sharing rules a re-evaluation (AGL-3336).\n followUpSharing(\n 'leads',\n plan.writes.filter((write) => write.kind === 'update').map((write) => rows.find((row) => row.$id === write.id)?.leadId ?? write.id),\n )\n return outcome\n },\n done,\n }),\n [apply, writers, rows, followUpSharing],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending) return\n const action = pending\n setPending(null)\n const writes: CrmBulkWrite[] = []\n const skipped: CrmBulkSkip[] = []\n if (action === 'owner') {\n for (const lead of selectedRows) {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: { ownerUid: value ? value : deleteField(), updatedAt: serverTimestamp() },\n })\n }\n await runPlan(\n { writes, skipped },\n (count) => (value ? `Owner set on ${countNoun(count, NOUN)}` : `Owner cleared on ${countNoun(count, NOUN)}`),\n )\n return\n }\n if (action === 'campaign') {\n // Added to what each lead carries — `arrayUnion`, the way a sequence's\n // enroll and the import add one — never in place of it.\n for (const lead of selectedRows) {\n // The union as it will stand, for the list field it moves (AGL-3321).\n const union = [...new Set([...readCampaignIds(lead), ...campaignIds])]\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n campaignIds: arrayUnion(...campaignIds),\n ...crmClientListFields('leads', lead, { campaignIds: union }, ['scopedCampaignIds']),\n updatedAt: serverTimestamp(),\n },\n })\n }\n const outcome = await runPlan(\n { writes, skipped },\n (count) =>\n `Added ${countNoun(count, NOUN)} to ${campaignIds.length === 1 ? 'the campaign' : `${campaignIds.length} campaigns`}`,\n )\n // What landed is written on each lead's Activity (AGL-3274): one\n // \"Filed under\" per campaign the lead was not already in, and none\n // for a lead the store refused — the row's own document is the\n // membership, and the entry only records the act.\n const refused = new Set(outcome.refused.map((entry) => entry.label))\n const named = campaignIds.map((campaignId) => ({\n id: campaignId,\n name: campaigns.options.find((option) => option.value === campaignId)?.label ?? '',\n }))\n for (const lead of selectedRows) {\n if (refused.has(labelOf(lead))) continue\n const already = readCampaignIds(lead)\n const filed = named.filter((campaign) => !already.includes(campaign.id))\n // At the organization level each entry carries its own lead's first\n // capturing site, the one the lead's page files as.\n if (filed.length) {\n await logFiling(\n { leadId: lead.leadId },\n { filed },\n hostId ? null : leadPrimaryGroup(lead, props.org ?? null).hostId || null,\n )\n }\n }\n return\n }\n if (action === 'status') {\n const status = value as CrmLeadStatus\n for (const lead of selectedRows) {\n const current = crmLeadStatus(lead)\n if (lead.convertedContactId) {\n skipped.push({ label: labelOf(lead), reason: 'was converted' })\n } else if (current === status) {\n skipped.push({\n label: labelOf(lead),\n reason: `already ${CRM_LEAD_STATUS_LABELS[status]}`,\n })\n } else {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n status,\n // Reopening an unqualified lead clears its reason, as the row does.\n ...(current === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n updatedAt: serverTimestamp(),\n },\n })\n }\n }\n await runPlan(\n { writes, skipped },\n (count) => `Status set on ${countNoun(count, NOUN)}`,\n )\n return\n }\n const reason = value.trim().slice(0, UNQUALIFY_REASON_MAX)\n for (const lead of selectedRows) {\n if (lead.convertedContactId) {\n skipped.push({ label: labelOf(lead), reason: 'was converted' })\n } else if (!isCrmLeadOpen(lead)) {\n skipped.push({ label: labelOf(lead), reason: 'is already closed' })\n } else {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n status: 'unqualified' satisfies CrmLeadStatus,\n unqualifiedReason: reason,\n updatedAt: serverTimestamp(),\n },\n })\n }\n }\n await runPlan(\n { writes, skipped },\n (count) => `Marked ${countNoun(count, NOUN)} unqualified`,\n )\n }, [pending, value, campaignIds, campaigns.options, selectedRows, runPlan, logFiling, hostId, props.org])\n\n const handleExport = useCallback(() => {\n downloadTextFile('leads-selected.csv', 'text/csv', leadsCsv(selectedRows, csv))\n }, [selectedRows, csv])\n\n const canApply =\n pending === 'owner' ||\n (pending === 'campaign'\n ? campaignIds.length > 0\n : pending === 'status'\n ? Boolean(value)\n : Boolean(value.trim()))\n\n return (\n <CrmBulkBarFrame\n count={selected.length}\n noun={NOUN}\n busy={busy}\n onClear={() => onSelectedChange([])}\n report={report}\n onDismissReport={dismissReport}\n extras={\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={canApply}\n applyLabel={pending === 'unqualify' ? 'Unqualify' : pending === 'campaign' ? 'Add' : undefined}\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'owner' ? (\n <LeadOwnerSelect value={value} onChange={setValue} roster={roster} size=\"medium\" />\n ) : pending === 'status' ? (\n <TextField\n select\n size=\"small\"\n label=\"Status\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText=\"Closing a lead goes through Unqualify, which asks why.\"\n >\n {SETTABLE_STATUSES.map((status) => (\n <MenuItem key={status} value={status}>\n {CRM_LEAD_STATUS_LABELS[status]}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'unqualify' ? (\n <TextField\n label=\"Reason\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n multiline\n minRows={2}\n autoFocus\n helperText=\"One reason for every selected lead, kept on each so it can be counted later.\"\n slotProps={{ htmlInput: { maxLength: UNQUALIFY_REASON_MAX } }}\n />\n ) : pending === 'campaign' ? (\n <CampaignPicker\n options={campaigns.options}\n value={campaignIds}\n onChange={setCampaignIds}\n helperText=\"Added to the campaigns each selected lead is already in. It does not decide who a campaign mails.\"\n empty={campaigns.ready && !campaigns.options.length}\n emptyText={\n hostId\n ? 'This site has no campaigns yet. Create one from Marketing to file leads under it.'\n : 'There are no campaigns yet. Create one from Marketing to file leads under it.'\n }\n />\n ) : null}\n </CrmBulkValueDialog>\n }\n >\n <Button size=\"small\" disabled={busy} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('status')}>\n {'Set status'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('unqualify')}>\n {'Unqualify'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('campaign')}>\n {'Add to campaign'}\n </Button>\n <CrmBulkShareButton\n object=\"leads\"\n ids={selectedRows.map((row) => row.leadId ?? row.$id)}\n hostId={hostId}\n orgId={orgId}\n disabled={busy}\n />\n <Button size=\"small\" disabled={busy} onClick={handleExport}>\n {'Export CSV'}\n </Button>\n {/*\n The selection's file is the rows on screen; this one is the whole\n collection, streamed by the server (AGL-2662).\n */}\n <CrmExportAllButton\n resource=\"leads\"\n orgId={orgId}\n hostId={hostId}\n disabled={busy}\n />\n </CrmBulkBarFrame>\n )\n}\nLeadsBulkBarBody.displayName = 'LeadsBulkBarBody'\n\nexport default LeadsBulkBar\n"],"names":["CRM_LEAD_STATUS_LABELS","crmLeadStatus","isCrmLeadOpen","readCampaignIds","CampaignPicker","useFirestore","Button","MenuItem","TextField","arrayUnion","deleteField","doc","serverTimestamp","useCallback","useMemo","useState","useCampaignFilingLog","useCrmBulkApply","useCrmCampaigns","downloadTextFile","crmClientListFields","crmBulkWriters","runCrmBulkWrites","leadsCsv","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","CrmBulkShareButton","LeadOwnerSelect","UNQUALIFY_REASON_MAX","useCrmSharingFollowUp","leadPrimaryGroup","NOUN","singular","plural","ACTION_TITLES","owner","status","unqualify","campaign","SETTABLE_STATUSES","labelOf","lead","String","leadId","LeadsBulkBar","props","selected","length","LeadsBulkBarBody","displayName","rows","onSelectedChange","roster","csv","orgId","hostId","followUpSharing","firestore","busy","report","apply","dismissReport","recordKind","selectedRows","chosen","Set","filter","row","has","$id","pending","setPending","value","setValue","campaignIds","setCampaignIds","campaigns","enabled","logFiling","org","writers","byId","Map","map","id","get","Error","openAction","action","runPlan","plan","done","attempted","writes","skipped","job","outcome","write","label","kind","find","handleApply","push","data","ownerUid","updatedAt","count","union","refused","entry","named","campaignId","name","options","option","already","filed","includes","current","convertedContactId","reason","unqualifiedReason","trim","slice","handleExport","canApply","Boolean","noun","onClear","onDismissReport","extras","open","title","applyLabel","undefined","onClose","onApply","onChange","size","select","event","target","helperText","multiline","minRows","autoFocus","slotProps","htmlInput","maxLength","empty","ready","emptyText","disabled","onClick","object","ids","resource"],"mappings":"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"}
@@ -42,6 +42,7 @@ import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, MenuIte
42
42
  import { deleteField, doc, serverTimestamp, updateDoc } from "firebase/firestore";
43
43
  import { useRouter } from "next/navigation";
44
44
  import { useCallback, useEffect, useMemo, useState } from "react";
45
+ import { leadPrimaryGroup } from "../model/contact-holder.js";
45
46
  import { downloadTextFile } from "../model/contacts-csv.js";
46
47
  import { crmRoutes } from "../model/crm-routes.js";
47
48
  import { LEAD_EMAIL_FILTER_OPTIONS, LEAD_FILTER_CODECS, LEAD_LIST_DECLARATION, LEAD_LIST_FILTER_FIELDS, LEAD_LIST_FILTER_HEADERS, LEAD_SOURCE_FILTER_NONE, LEAD_STATUS_FILTER_OPTIONS, leadClausesForGrid, LEAD_PREFIX_SEARCH, leadClauseImpliesScope, leadClausesToStore, leadQueryClause } from "../model/lead-filters.js";
@@ -60,6 +61,9 @@ import { LeadOwnerSelect } from "./lead-owner-select.js";
60
61
  import { CONVERT_PENDING_ERASURE_REASON } from "./lead-properties-card.js";
61
62
  import { CrmEmailStateChip } from "./crm-email-state-chip.js";
62
63
  import { LeadStatusChip } from "./lead-status-chip.js";
64
+ import { CrmShareChipView } from "./record-sharing-card.js";
65
+ import { crmShareChipFor } from "../model/crm-sharing.js";
66
+ import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
63
67
  import LeadSurfacesNote from "./lead-surfaces-note.js";
64
68
  import { LeadUnqualifyDialog } from "./lead-unqualify-dialog.js";
65
69
  import LeadsBulkBar from "./leads-bulk-bar.js";
@@ -87,10 +91,13 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
87
91
  const firestore = useFirestore();
88
92
  const router = useRouter();
89
93
  const { enqueueSnackbar } = useSnackbar();
90
- const { orgId, createHostId } = useCrmScope({
94
+ const { orgId, createHostId, consentGroup } = useCrmScope({
91
95
  hostId,
92
96
  org
93
97
  });
98
+ // The viewing site's group: a row it sees only through a share is chipped
99
+ // with who shared it (AGL-3336). None at the organization level.
100
+ const viewingHostIds = consentGroup == null ? void 0 : consentGroup.hostIds;
94
101
  const mount = useCrmOrgMount();
95
102
  const roster = useOrgMemberOptions(orgId);
96
103
  // The org's lead fields, for the optional columns below (AGL-3272).
@@ -341,6 +348,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
341
348
  crmApi,
342
349
  enqueueSnackbar
343
350
  ]);
351
+ // A client-direct write owes the org's sharing rules a re-evaluation (AGL-3336).
352
+ const followUpSharing = useCrmSharingFollowUp(hostId, orgId);
344
353
  const writeLead = useCallback(async (lead, fields, done)=>{
345
354
  if (!orgId) {
346
355
  enqueueSnackbar('Still loading this workspace — try again in a moment.', {
@@ -353,6 +362,9 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
353
362
  await updateDoc(doc(firestore, 'orgs', orgId, 'leads', lead.leadId), _extends({}, fields, {
354
363
  updatedAt: serverTimestamp()
355
364
  }));
365
+ followUpSharing('leads', [
366
+ lead.leadId
367
+ ]);
356
368
  enqueueSnackbar(done, {
357
369
  variant: 'success',
358
370
  persist: false
@@ -364,7 +376,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
364
376
  }
365
377
  }, [
366
378
  firestore,
367
- enqueueSnackbar
379
+ enqueueSnackbar,
380
+ followUpSharing
368
381
  ]);
369
382
  const columns = useMemo(()=>[
370
383
  {
@@ -383,10 +396,24 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
383
396
  height: '100%'
384
397
  },
385
398
  children: [
386
- /*#__PURE__*/ _jsx(Typography, {
387
- variant: "body2",
388
- noWrap: true,
389
- children: String(row['name'] || row['email'] || row.$id)
399
+ /*#__PURE__*/ _jsxs(Stack, {
400
+ direction: "row",
401
+ spacing: 1,
402
+ sx: {
403
+ alignItems: 'center',
404
+ minWidth: 0
405
+ },
406
+ children: [
407
+ /*#__PURE__*/ _jsx(Typography, {
408
+ variant: "body2",
409
+ noWrap: true,
410
+ children: String(row['name'] || row['email'] || row.$id)
411
+ }),
412
+ /*#__PURE__*/ _jsx(CrmShareChipView, {
413
+ chip: crmShareChipFor(row, viewingHostIds != null ? viewingHostIds : []),
414
+ org: org
415
+ })
416
+ ]
390
417
  }),
391
418
  row['name'] ? /*#__PURE__*/ _jsx(Typography, {
392
419
  variant: "caption",
@@ -627,6 +654,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
627
654
  routes,
628
655
  writeLead,
629
656
  hostId,
657
+ viewingHostIds,
658
+ org,
630
659
  mount,
631
660
  campaignName,
632
661
  leadFields.active,
@@ -784,7 +813,7 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
784
813
  /*#__PURE__*/ _jsx(LeadConvertDialog, {
785
814
  open: Boolean(converting),
786
815
  onClose: ()=>setConverting(null),
787
- hostId: hostId != null ? hostId : Aglyn.leadPrimaryGroup(converting, org).hostId || null,
816
+ hostId: hostId != null ? hostId : leadPrimaryGroup(converting, org).hostId || null,
788
817
  orgId: orgId,
789
818
  org: org,
790
819
  leadId: (_ref1 = converting == null ? void 0 : converting.leadId) != null ? _ref1 : '',