@aglyn/plugins-crm 1.0.0-beta.218 → 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/deal-detail-page.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 { CRM_COLLECTIONS, dealStageById, pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPencilOutline } from '@aglyn/shared-data-mdi'\nimport { MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { Button, Stack, Typography } from '@mui/material'\nimport { deleteDoc, doc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { CrmCreateSiteDefault } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useDealStageApi } from '../hooks/use-deal-stage-api'\nimport { useDeal } from '../hooks/use-deals'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { usePipeline } from '../hooks/use-pipeline'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport { DEAL_STATUS_LABELS, formatMoney } from '../model/deal-board-model'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { RecordActivityCard } from './record-activity-card'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport { DealEditDrawer } from './deal-edit-drawer'\nimport { DealProductsCard } from './deal-products-card'\nimport { DealPropertiesCard } from './deal-properties-card'\nimport { DealStageCard } from './deal-stage-card'\nimport RecordFilesCard from './record-files-card'\nimport { RecordTasksCard } from './record-tasks-card'\n\n/**\n * `/crm/deals/{dealId}` — one deal (AGL-2598).\n *\n * The record behind a card: its stage and the controls that move it, what\n * it is worth and who it is with, the products behind the amount\n * (AGL-2620), and the tasks and activity filed against it. One live\n * document read; the pipeline and the roster are the same bounded reads\n * the board makes. Editing opens the same drawer the board creates with,\n * and deleting is the one destructive act here — confirmed, then a\n * client-direct delete the rules allow the same people who could have\n * created it.\n */\nexport function DealDetailPage(props: CrmDetailPageProps) {\n const { id, basePath, hostId, org } = props\n const routes = crmRoutes(basePath)\n const router = useRouter()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const scope = useCrmScope({ hostId, org })\n const { data: deal, status, fromCache } = useDeal(scope.orgId, id)\n // The feed the act is logged in, decided by the level it was performed\n // at (AGL-2738): this site's under a site, the organization's at the org\n // hub, where a client-direct append to the deal's own site would face a\n // gate the record write never had to pass.\n const logActivity = useCrmActivityLogger(hostId)\n const pipelineState = usePipeline(scope.orgId, {\n hostId,\n org: (org ?? null) as Record<string, unknown> | null,\n })\n const pipeline = pipelineState.pipelineById(deal?.pipelineId)\n const roster = useOrgMemberDirectory(scope.orgId)\n const api = useDealStageApi(hostId)\n const nowMs = useMemo(() => Date.now(), [])\n const [editing, setEditing] = useState(false)\n const [deleting, setDeleting] = useState(false)\n // The org's deal fields (AGL-2661), for the properties card's rows.\n const dealFields = useContactFieldDefinitions(scope.orgId, 'deal')\n\n const notFound = scope.ready && (!scope.orgId || (status !== 'loading' && !deal))\n\n const handleDelete = useCallback(async () => {\n if (!deal || !scope.orgId) return\n const agreed = await confirm({\n title: 'Delete this deal?',\n description:\n `\"${deal.title}\" is removed from the pipeline along with its notes. ` +\n 'The contact and the company it names are not touched.',\n confirmationText: 'Delete deal',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n setDeleting(true)\n try {\n await deleteDoc(doc(firestore, 'orgs', scope.orgId, CRM_COLLECTIONS.deals, deal.$id))\n // Setup → Activity shows CRM work (AGL-2622): the deal is org data,\n // but the act happened in this site's console and belongs in its feed.\n logActivity('Deleted deal', { type: 'deal', id: deal.$id, name: deal.title })\n enqueueSnackbar('Deal deleted', { variant: 'success', persist: false })\n router.push(routes.section('deals'))\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n setDeleting(false)\n }\n }, [deal, scope.orgId, confirm, firestore, logActivity, enqueueSnackbar, router, routes])\n\n const stage = deal ? dealStageById(pipeline, deal.stageId) : undefined\n\n return (\n // A create opened from this page — a task, an activity — files under\n // the deal's own site unless the reader says otherwise (AGL-2630).\n <CrmCreateSiteDefault hostId={deal?.hostId ?? null}>\n <Stack spacing={2}>\n <CrmRecordHeader\n kind=\"Deal\"\n title={deal?.title}\n subtitle={\n deal\n ? [pipeline?.name, stage?.name ?? deal.stageId].filter(Boolean).join(' · ')\n : undefined\n }\n help={pluginDocsHelp('deals', { anchor: '#a-deals-page' })}\n backHref={routes.section('deals')}\n backLabel=\"Back to deals\"\n loading={!deal && !notFound}\n // The booking door (AGL-2660): the deal's own site's services.\n booking={\n deal\n ? { hostId: hostId ?? deal.hostId ?? null, org, kind: 'deal', recordId: deal.$id }\n : undefined\n }\n actions={\n deal ? (\n <>\n <Button\n size=\"small\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPencilOutline.path} size={0.8} />}\n onClick={() => setEditing(true)}\n >\n {'Edit'}\n </Button>\n <CrmSendEmailButton\n hostId={hostId ?? deal.hostId ?? null}\n org={org}\n dealId={deal.$id}\n contactId={deal.contactId}\n name={deal.contactName}\n />\n </>\n ) : null\n }\n menuItems={\n deal\n ? [\n {\n key: 'delete',\n label: 'Delete deal',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: deleting,\n disabledReason: 'The deal is being deleted',\n onClick: () => void handleDelete(),\n },\n ]\n : undefined\n }\n chips={\n deal ? (\n <>\n <CrmRecordChip\n label=\"Status\"\n value={DEAL_STATUS_LABELS[deal.status] ?? deal.status}\n color={deal.status === 'won' ? 'success' : deal.status === 'lost' ? 'default' : 'primary'}\n />\n <CrmRecordChip\n label=\"Amount\"\n value={\n typeof deal.amountCents === 'number'\n ? formatMoney(deal.amountCents, deal.currency)\n : undefined\n }\n />\n <CrmRecordChip label=\"Owner\" value={roster.nameOf(deal.ownerUid) || undefined} />\n </>\n ) : null\n }\n >\n {notFound ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This deal does not exist, or is not visible to this site.'}\n </Typography>\n ) : null}\n </CrmRecordHeader>\n {deal ? (\n <>\n {/* What an assistant proposes about the deal, its stage included (AGL-2917). */}\n <CrmRecordInsightsZone\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"deal\"\n recordId={deal.$id}\n name={deal.title}\n taskLink={{ dealId: deal.$id }}\n deal={{ pipeline, ref: deal, stageId: deal.stageId, status: deal.status, api }}\n />\n <DealStageCard deal={deal} pipeline={pipeline} api={api} nowMs={nowMs} />\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2} sx={{ alignItems: 'stretch' }}>\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <DealPropertiesCard\n deal={deal}\n pipeline={pipeline}\n ownerLabel={roster.nameOf(deal.ownerUid)}\n routes={routes}\n customFields={dealFields.active}\n />\n </Stack>\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <RecordTasksCard hostId={hostId} org={org} basePath={basePath} dealId={deal.$id} />\n </Stack>\n </Stack>\n {scope.orgId ? (\n <DealProductsCard\n deal={deal}\n orgId={scope.orgId}\n hostId={hostId ?? deal.hostId ?? null}\n org={org}\n fromCache={fromCache}\n unreadable={status === 'error'}\n />\n ) : null}\n <RecordFilesCard\n scope={scope.scope}\n collection={CRM_COLLECTIONS.deals}\n recordId={deal.$id}\n mediaIds={deal.mediaIds}\n topic=\"deals\"\n />\n <RecordActivityCard hostId={hostId} org={org} dealId={deal.$id} />\n </>\n ) : null}\n </Stack>\n <DealEditDrawer\n open={editing}\n onClose={() => setEditing(false)}\n hostId={hostId}\n org={org}\n deal={deal ?? null}\n pipelines={pipelineState.activePipelines}\n defaultPipeline={pipeline ?? pipelineState.pipeline}\n unreadable={status === 'error'}\n fromCache={fromCache}\n />\n </CrmCreateSiteDefault>\n )\n}\nDealDetailPage.displayName = 'DealDetailPage'\n\nexport default DealDetailPage\n"],"names":["CRM_COLLECTIONS","dealStageById","pluginDocsHelp","mdiDeleteOutline","mdiPencilOutline","MdiIcon","useConfirmationContext","useSnackbar","useFirestore","Button","Stack","Typography","deleteDoc","doc","useRouter","useCallback","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","CrmCreateSiteDefault","useCrmScope","useDealStageApi","useDeal","useOrgMemberDirectory","usePipeline","crmRoutes","DEAL_STATUS_LABELS","formatMoney","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","RecordActivityCard","CrmSendEmailButton","DealEditDrawer","DealProductsCard","DealPropertiesCard","DealStageCard","RecordFilesCard","RecordTasksCard","DealDetailPage","props","hostId","id","basePath","org","routes","router","firestore","enqueueSnackbar","confirm","scope","data","deal","status","fromCache","orgId","logActivity","pipelineState","pipeline","pipelineById","pipelineId","roster","api","nowMs","Date","now","editing","setEditing","deleting","setDeleting","dealFields","notFound","ready","handleDelete","agreed","title","description","confirmationText","confirmationButtonProps","color","then","catch","deals","$id","type","name","variant","persist","push","section","error","console","allowDuplicate","stage","stageId","undefined","spacing","kind","subtitle","filter","Boolean","join","help","anchor","backHref","backLabel","loading","booking","recordId","actions","size","startIcon","path","onClick","dealId","contactId","contactName","menuItems","key","label","icon","destructive","disabled","disabledReason","chips","value","amountCents","currency","nameOf","ownerUid","taskLink","ref","direction","xs","md","sx","alignItems","flex","minWidth","ownerLabel","customFields","active","unreadable","collection","mediaIds","topic","open","onClose","pipelines","activePipelines","defaultPipeline","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-detail-page.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 { CRM_COLLECTIONS, dealStageById, pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPencilOutline } from '@aglyn/shared-data-mdi'\nimport { MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { Button, Stack, Typography } from '@mui/material'\nimport { deleteDoc, doc } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { CrmCreateSiteDefault } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useDealStageApi } from '../hooks/use-deal-stage-api'\nimport { useDeal } from '../hooks/use-deals'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { usePipeline } from '../hooks/use-pipeline'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport { DEAL_STATUS_LABELS, formatMoney } from '../model/deal-board-model'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { RecordActivityCard } from './record-activity-card'\nimport { CrmShareChipView, RecordSharingCard } from './record-sharing-card'\nimport { crmShareChipFor } from '../model/crm-sharing'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport { DealEditDrawer } from './deal-edit-drawer'\nimport { DealProductsCard } from './deal-products-card'\nimport { DealPropertiesCard } from './deal-properties-card'\nimport { DealStageCard } from './deal-stage-card'\nimport RecordFilesCard from './record-files-card'\nimport { RecordTasksCard } from './record-tasks-card'\n\n/**\n * `/crm/deals/{dealId}` — one deal (AGL-2598).\n *\n * The record behind a card: its stage and the controls that move it, what\n * it is worth and who it is with, the products behind the amount\n * (AGL-2620), and the tasks and activity filed against it. One live\n * document read; the pipeline and the roster are the same bounded reads\n * the board makes. Editing opens the same drawer the board creates with,\n * and deleting is the one destructive act here — confirmed, then a\n * client-direct delete the rules allow the same people who could have\n * created it.\n */\nexport function DealDetailPage(props: CrmDetailPageProps) {\n const { id, basePath, hostId, org } = props\n const routes = crmRoutes(basePath)\n const router = useRouter()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const scope = useCrmScope({ hostId, org })\n const { data: deal, status, fromCache } = useDeal(scope.orgId, id)\n // The feed the act is logged in, decided by the level it was performed\n // at (AGL-2738): this site's under a site, the organization's at the org\n // hub, where a client-direct append to the deal's own site would face a\n // gate the record write never had to pass.\n const logActivity = useCrmActivityLogger(hostId)\n const pipelineState = usePipeline(scope.orgId, {\n hostId,\n org: (org ?? null) as Record<string, unknown> | null,\n })\n const pipeline = pipelineState.pipelineById(deal?.pipelineId)\n const roster = useOrgMemberDirectory(scope.orgId)\n const api = useDealStageApi(hostId)\n const nowMs = useMemo(() => Date.now(), [])\n const [editing, setEditing] = useState(false)\n const [deleting, setDeleting] = useState(false)\n // The org's deal fields (AGL-2661), for the properties card's rows.\n const dealFields = useContactFieldDefinitions(scope.orgId, 'deal')\n\n const notFound = scope.ready && (!scope.orgId || (status !== 'loading' && !deal))\n\n const handleDelete = useCallback(async () => {\n if (!deal || !scope.orgId) return\n const agreed = await confirm({\n title: 'Delete this deal?',\n description:\n `\"${deal.title}\" is removed from the pipeline along with its notes. ` +\n 'The contact and the company it names are not touched.',\n confirmationText: 'Delete deal',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!agreed) return\n setDeleting(true)\n try {\n await deleteDoc(doc(firestore, 'orgs', scope.orgId, CRM_COLLECTIONS.deals, deal.$id))\n // Setup → Activity shows CRM work (AGL-2622): the deal is org data,\n // but the act happened in this site's console and belongs in its feed.\n logActivity('Deleted deal', { type: 'deal', id: deal.$id, name: deal.title })\n enqueueSnackbar('Deal deleted', { variant: 'success', persist: false })\n router.push(routes.section('deals'))\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n setDeleting(false)\n }\n }, [deal, scope.orgId, confirm, firestore, logActivity, enqueueSnackbar, router, routes])\n\n const stage = deal ? dealStageById(pipeline, deal.stageId) : undefined\n\n return (\n // A create opened from this page — a task, an activity — files under\n // the deal's own site unless the reader says otherwise (AGL-2630).\n <CrmCreateSiteDefault hostId={deal?.hostId ?? null}>\n <Stack spacing={2}>\n <CrmRecordHeader\n kind=\"Deal\"\n title={deal?.title}\n subtitle={\n deal\n ? [pipeline?.name, stage?.name ?? deal.stageId].filter(Boolean).join(' · ')\n : undefined\n }\n help={pluginDocsHelp('deals', { anchor: '#a-deals-page' })}\n backHref={routes.section('deals')}\n backLabel=\"Back to deals\"\n loading={!deal && !notFound}\n // The booking door (AGL-2660): the deal's own site's services.\n booking={\n deal\n ? { hostId: hostId ?? deal.hostId ?? null, org, kind: 'deal', recordId: deal.$id }\n : undefined\n }\n actions={\n deal ? (\n <>\n <Button\n size=\"small\"\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPencilOutline.path} size={0.8} />}\n onClick={() => setEditing(true)}\n >\n {'Edit'}\n </Button>\n <CrmSendEmailButton\n hostId={hostId ?? deal.hostId ?? null}\n org={org}\n dealId={deal.$id}\n contactId={deal.contactId}\n name={deal.contactName}\n />\n </>\n ) : null\n }\n menuItems={\n deal\n ? [\n {\n key: 'delete',\n label: 'Delete deal',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: deleting,\n disabledReason: 'The deal is being deleted',\n onClick: () => void handleDelete(),\n },\n ]\n : undefined\n }\n chips={\n deal ? (\n <>\n <CrmRecordChip\n label=\"Status\"\n value={DEAL_STATUS_LABELS[deal.status] ?? deal.status}\n color={deal.status === 'won' ? 'success' : deal.status === 'lost' ? 'default' : 'primary'}\n />\n <CrmRecordChip\n label=\"Amount\"\n value={\n typeof deal.amountCents === 'number'\n ? formatMoney(deal.amountCents, deal.currency)\n : undefined\n }\n />\n <CrmRecordChip label=\"Owner\" value={roster.nameOf(deal.ownerUid) || undefined} />\n {/* Seen here only through a share (AGL-3336). */}\n <CrmShareChipView\n chip={crmShareChipFor(\n deal as unknown as Record<string, unknown>,\n scope.consentGroup?.hostIds ?? [],\n )}\n org={org}\n />\n </>\n ) : null\n }\n >\n {notFound ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This deal does not exist, or is not visible to this site.'}\n </Typography>\n ) : null}\n </CrmRecordHeader>\n {deal ? (\n <>\n {/* What an assistant proposes about the deal, its stage included (AGL-2917). */}\n <CrmRecordInsightsZone\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"deal\"\n recordId={deal.$id}\n name={deal.title}\n taskLink={{ dealId: deal.$id }}\n deal={{ pipeline, ref: deal, stageId: deal.stageId, status: deal.status, api }}\n />\n <DealStageCard deal={deal} pipeline={pipeline} api={api} nowMs={nowMs} />\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2} sx={{ alignItems: 'stretch' }}>\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <DealPropertiesCard\n deal={deal}\n pipeline={pipeline}\n ownerLabel={roster.nameOf(deal.ownerUid)}\n routes={routes}\n customFields={dealFields.active}\n />\n </Stack>\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <RecordTasksCard hostId={hostId} org={org} basePath={basePath} dealId={deal.$id} />\n </Stack>\n </Stack>\n {scope.orgId ? (\n <DealProductsCard\n deal={deal}\n orgId={scope.orgId}\n hostId={hostId ?? deal.hostId ?? null}\n org={org}\n fromCache={fromCache}\n unreadable={status === 'error'}\n />\n ) : null}\n <RecordFilesCard\n scope={scope.scope}\n collection={CRM_COLLECTIONS.deals}\n recordId={deal.$id}\n mediaIds={deal.mediaIds}\n topic=\"deals\"\n />\n {/* Where the deal is visible and why, and a manager's share (AGL-3336). */}\n <RecordSharingCard\n object=\"deals\"\n id={deal.$id}\n record={deal as unknown as Record<string, unknown>}\n hostId={hostId ?? null}\n orgId={scope.orgId}\n org={org}\n viewingHostIds={scope.consentGroup?.hostIds}\n />\n <RecordActivityCard hostId={hostId} org={org} dealId={deal.$id} />\n </>\n ) : null}\n </Stack>\n <DealEditDrawer\n open={editing}\n onClose={() => setEditing(false)}\n hostId={hostId}\n org={org}\n deal={deal ?? null}\n pipelines={pipelineState.activePipelines}\n defaultPipeline={pipeline ?? pipelineState.pipeline}\n unreadable={status === 'error'}\n fromCache={fromCache}\n />\n </CrmCreateSiteDefault>\n )\n}\nDealDetailPage.displayName = 'DealDetailPage'\n\nexport default DealDetailPage\n"],"names":["CRM_COLLECTIONS","dealStageById","pluginDocsHelp","mdiDeleteOutline","mdiPencilOutline","MdiIcon","useConfirmationContext","useSnackbar","useFirestore","Button","Stack","Typography","deleteDoc","doc","useRouter","useCallback","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","CrmCreateSiteDefault","useCrmScope","useDealStageApi","useDeal","useOrgMemberDirectory","usePipeline","crmRoutes","DEAL_STATUS_LABELS","formatMoney","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","RecordActivityCard","CrmShareChipView","RecordSharingCard","crmShareChipFor","CrmSendEmailButton","DealEditDrawer","DealProductsCard","DealPropertiesCard","DealStageCard","RecordFilesCard","RecordTasksCard","DealDetailPage","props","hostId","scope","id","basePath","org","routes","router","firestore","enqueueSnackbar","confirm","data","deal","status","fromCache","orgId","logActivity","pipelineState","pipeline","pipelineById","pipelineId","roster","api","nowMs","Date","now","editing","setEditing","deleting","setDeleting","dealFields","notFound","ready","handleDelete","agreed","title","description","confirmationText","confirmationButtonProps","color","then","catch","deals","$id","type","name","variant","persist","push","section","error","console","allowDuplicate","stage","stageId","undefined","spacing","kind","subtitle","filter","Boolean","join","help","anchor","backHref","backLabel","loading","booking","recordId","actions","size","startIcon","path","onClick","dealId","contactId","contactName","menuItems","key","label","icon","destructive","disabled","disabledReason","chips","value","amountCents","currency","nameOf","ownerUid","chip","consentGroup","hostIds","taskLink","ref","direction","xs","md","sx","alignItems","flex","minWidth","ownerLabel","customFields","active","unreadable","collection","mediaIds","topic","object","record","viewingHostIds","open","onClose","pipelines","activePipelines","defaultPipeline","displayName"],"mappings":"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"}
@@ -38,6 +38,7 @@ import { CrmSitePicker } from "./crm-site-picker.js";
38
38
  import { CRM_CLIENT_SEARCH_FIELDS, crmClientListFields } from "../model/crm-list-query.js";
39
39
  import { DEAL_CURRENCIES, openStages } from "../model/deal-board-model.js";
40
40
  import { contactChoicesFor, DEAL_NOTES_MAX, DEAL_TITLE_MAX, dealDocumentFromForm, dealFormFromDoc, dealFormProblem, dealPatchFromForm, emptyDealForm } from "../model/deal-form-model.js";
41
+ import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
41
42
  /**
42
43
  * How many recent contacts the picker matches against. The window is the
43
44
  * ask — it is read when the drawer opens, not when the section does — and
@@ -84,6 +85,8 @@ import { contactChoicesFor, DEAL_NOTES_MAX, DEAL_TITLE_MAX, dealDocumentFromForm
84
85
  hostId,
85
86
  org
86
87
  });
88
+ // A client-direct write owes the org's sharing rules a re-evaluation (AGL-3336).
89
+ const followUpSharing = useCrmSharingFollowUp(hostId, orgId);
87
90
  // The feed the act is logged in is the level it was PERFORMED at
88
91
  // (AGL-2738): this site's under a site, the organization's at the org
89
92
  // hub. `createHostId` is where the deal is STAMPED, which at the hub is a
@@ -255,6 +258,9 @@ import { contactChoicesFor, DEAL_NOTES_MAX, DEAL_TITLE_MAX, dealDocumentFromForm
255
258
  });
256
259
  return;
257
260
  }
261
+ followUpSharing('deals', [
262
+ deal.$id
263
+ ]);
258
264
  enqueueSnackbar('Deal saved', {
259
265
  variant: 'success',
260
266
  persist: false
@@ -281,6 +287,9 @@ import { contactChoicesFor, DEAL_NOTES_MAX, DEAL_TITLE_MAX, dealDocumentFromForm
281
287
  // Setup → Activity shows CRM work (AGL-2622): the deal is org data,
282
288
  // but the act happened in this site's console and belongs in its
283
289
  // feed. Creation only — an edit is a save the card reports.
290
+ followUpSharing('deals', [
291
+ created.id
292
+ ]);
284
293
  logActivity('Added deal', {
285
294
  type: 'deal',
286
295
  id: created.id,
@@ -306,6 +315,7 @@ import { contactChoicesFor, DEAL_NOTES_MAX, DEAL_TITLE_MAX, dealDocumentFromForm
306
315
  problem,
307
316
  bandFull,
308
317
  orgId,
318
+ followUpSharing,
309
319
  user,
310
320
  deal,
311
321
  values,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-edit-drawer.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 {\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n crmNewRecordListFields,\n dealHasLineItems,\n findOrgMember,\n nameSearchKey,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteField,\n doc,\n endAt,\n getDocs,\n limit,\n orderBy,\n query,\n startAt,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport {\n type CrmOrgDoc,\n crmScopeListable,\n crmVisibleToClause,\n useCrmScope,\n} from '../hooks/use-crm-scope'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { contactPrimaryGroup } from '../model/contact-record'\nimport {\n type CrmCustomDraft,\n crmCustomDraftDocument,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmSitePicker } from './crm-site-picker'\nimport { CRM_CLIENT_SEARCH_FIELDS, crmClientListFields } from '../model/crm-list-query'\nimport {\n DEAL_CURRENCIES,\n type DealDoc,\n openStages,\n type PipelineDoc,\n} from '../model/deal-board-model'\nimport {\n type ContactChoice,\n contactChoicesFor,\n DEAL_NOTES_MAX,\n DEAL_TITLE_MAX,\n dealDocumentFromForm,\n dealFormFromDoc,\n dealFormProblem,\n type DealFormValues,\n dealPatchFromForm,\n emptyDealForm,\n} from '../model/deal-form-model'\n\n/**\n * How many recent contacts the picker matches against. The window is the\n * ask — it is read when the drawer opens, not when the section does — and\n * a person outside it is still linkable by pasting their address, which\n * the match tests as a prefix of the email.\n */\nconst CONTACT_WINDOW = 300\n\n/** Companies offered per keystroke; a longer list is a scroll, not a pick. */\nconst COMPANY_MATCHES = 8\n\n/** A company as the picker offers it. */\ninterface CompanyChoice {\n id: string\n name: string\n}\n\nexport interface DealEditDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the drawer is opened under, or `null` at the organization\n * level (AGL-2630), where a NEW deal asks which site captures it and an\n * edit keeps the deal's own.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The deal being edited; absent for a new one. */\n deal?: DealDoc | null\n /** Links and values a new deal opens with — a contact's page passes itself. */\n defaults?: Partial<DealFormValues>\n pipelines: PipelineDoc[]\n /** Where a new deal lands when nobody picks a pipeline. */\n defaultPipeline: PipelineDoc | null\n /** The listener's verdict on the row being edited, for the stale-seed guard. */\n unreadable?: boolean\n fromCache?: boolean\n /** After a save, with the id of the deal written. */\n onSaved?: (dealId: string, mode: 'create' | 'edit') => void\n}\n\n/**\n * The one form a deal is created and edited through (AGL-2598).\n *\n * ## What it writes, and what it does not\n *\n * Client-direct against the rules, like a contact's notes: the title, the\n * amount and its currency, the expected close, the owner, the linked\n * contact and company, and the notes. The stage and the status are shown on\n * a new deal — it has to start somewhere — and never on an existing one,\n * because a stage change is the moment an automation listens for and only\n * the server route can emit that event. A drawer that wrote `stageId`\n * directly would move a deal without anybody hearing it.\n *\n * Nor the amount of a deal that has line items (AGL-2620): it is their\n * sum, the products card on the deal's page owns it, and the field here\n * is read-only with a caption saying so. The pipeline picker on a new\n * deal lists every ACTIVE pipeline (`pipelines`) and opens on the one the\n * caller was looking at.\n *\n * ## The pickers\n *\n * The owner is picked from the org's roster, the company by a server-side\n * prefix on `nameLower` (indexed with the scope), and the contact from a\n * bounded window of the newest contacts matched in memory — see\n * `contactChoicesFor` for why the contact search cannot be a query. Both\n * pickers copy the chosen NAME onto the deal beside the id, so a board of\n * cards can caption itself without a read per card.\n */\nexport function DealEditDrawer(props: DealEditDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n deal,\n defaults,\n pipelines,\n defaultPipeline,\n unreadable = false,\n fromCache = false,\n onSaved,\n } = props\n const mode: 'create' | 'edit' = deal ? 'edit' : 'create'\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const { orgId, consentGroup, visibleTo, createHostId, createTokens } =\n useCrmScope({ hostId, org })\n // The feed the act is logged in is the level it was PERFORMED at\n // (AGL-2738): this site's under a site, the organization's at the org\n // hub. `createHostId` is where the deal is STAMPED, which at the hub is a\n // site the reader picked and may hold no role on — a different question\n // from where the line belongs, and a gate the record write never faced.\n const logActivity = useCrmActivityLogger(hostId)\n const roster = useOrgMemberDirectory(open ? orgId : null)\n\n const [values, setValues] = useState<DealFormValues>(() =>\n emptyDealForm(defaultPipeline, defaults),\n )\n const owner = findOrgMember(roster.members, values.ownerUid)\n const [busy, setBusy] = useState(false)\n /*\n * THE CUSTOM FIELDS (AGL-2661): the org's deal definitions, and the keys\n * the reader has touched over what the deal stores — saved as the\n * difference (`crm-custom-draft`) in the same write as the fixed fields.\n */\n const fields = useContactFieldDefinitions(open ? orgId : null, 'deal')\n const storedCustom = useMemo(() => deal?.custom ?? {}, [deal?.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n // Re-seeded on every open: an existing deal's stored values, or a blank\n // form aimed at the default pipeline with whatever the opener preselected.\n useEffect(() => {\n if (!open) return\n setValues(deal ? dealFormFromDoc(deal) : emptyDealForm(defaultPipeline, defaults))\n setCustom({})\n }, [open, deal, defaultPipeline, defaults])\n\n const update = useCallback(\n (patch: Partial<DealFormValues>) =>\n setValues((current) => ({ ...current, ...patch })),\n [],\n )\n\n const pipeline = useMemo(\n () =>\n pipelines.find((entry) => entry.$id === values.pipelineId) ??\n defaultPipeline,\n [pipelines, values.pipelineId, defaultPipeline],\n )\n const stages = useMemo(() => openStages(pipeline), [pipeline])\n\n /*\n * THE CONTACT WINDOW: the newest contacts this viewer may see, read only\n * while the drawer is open. Scoped and ordered like the contacts list —\n * the same `(visibleTo, updatedAt DESC)` index — and bounded, because the\n * picker's job is to find one person, not to list the audience.\n */\n const { data: contactRows } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n open && orgId && crmScopeListable(visibleTo)\n ? query(\n collection(firestore, 'orgs', orgId, 'contacts'),\n ...crmVisibleToClause(visibleTo),\n orderBy('updatedAt', 'desc'),\n limit(CONTACT_WINDOW),\n )\n : null,\n [firestore, open, orgId, visibleTo],\n { idField: '$id' },\n )\n const [contactQuery, setContactQuery] = useState('')\n // Named through the viewing group under a site; at the organization level\n // through each person's own primary holder.\n const contactChoices = useMemo(\n () =>\n contactChoicesFor(\n contactQuery,\n contactRows ?? [],\n consentGroup?.groupId ??\n ((row) => contactPrimaryGroup(row, org as Record<string, unknown>).groupId),\n ),\n [contactQuery, contactRows, consentGroup, org],\n )\n const contactValue: ContactChoice | null = values.contactId\n ? {\n id: values.contactId,\n name: values.contactName,\n email:\n contactChoices.find((choice) => choice.id === values.contactId)?.email ?? '',\n }\n : null\n\n /*\n * THE COMPANY SEARCH: a prefix on `nameLower` with the scope, which the\n * companies collection indexes. Debounced so a name typed at speed is one\n * query rather than one per letter.\n */\n const [companyQuery, setCompanyQuery] = useState('')\n const [companyChoices, setCompanyChoices] = useState<CompanyChoice[]>([])\n useEffect(() => {\n if (!open || !orgId || !crmScopeListable(visibleTo)) return\n const key = nameSearchKey(companyQuery)\n let active = true\n const timer = setTimeout(() => {\n void getDocs(\n query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n ...(key ? [startAt(key), endAt(`${key}\\uf8ff`)] : []),\n limit(COMPANY_MATCHES),\n ),\n )\n .then((snapshot) => {\n if (!active) return\n setCompanyChoices(\n snapshot.docs.map((entry) => ({\n id: entry.id,\n name: String(entry.get('name') ?? ''),\n })),\n )\n })\n .catch(() => {\n if (active) setCompanyChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, orgId, visibleTo, companyQuery])\n const companyValue: CompanyChoice | null = values.companyId\n ? { id: values.companyId, name: values.companyName }\n : null\n\n const customMissing = crmCustomDraftMissingRequired(fields.active, storedCustom, custom, mode)\n const problem =\n dealFormProblem(values, mode) ??\n (customMissing.length ? `${customMissing.join(', ')} is required.` : null)\n const amountDerived = Boolean(deal && dealHasLineItems(deal))\n\n /*\n * THE RECORDS BAND (AGL-2611), read only while a CREATE is open: a deal\n * is a record of the band the contacts list is banded by, and on an org\n * at a full hard band this drawer refuses the way `upsertHostContact`\n * refuses a capture — same number, same sentence. Memoized, because the\n * tuple is an effect dependency and a fresh one per render would re-read\n * the three aggregates on every keystroke.\n */\n const recordsScope = useMemo(\n () => (open && mode === 'create' && orgId ? (['orgs', orgId] as const) : null),\n [open, mode, orgId],\n )\n const records = useCrmRecordsQuota(recordsScope, org)\n const bandFull = mode === 'create' && records.ready && !records.quota.allowed\n\n const handleSave = useCallback(async () => {\n if (problem || !orgId || !user?.uid) return\n if (bandFull) {\n enqueueSnackbar(CRM_RECORDS_BAND_FULL_MESSAGE, { variant: 'warning', persist: false })\n return\n }\n setBusy(true)\n try {\n const nowMs = Date.now()\n if (deal) {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n const { set, clear } = dealPatchFromForm(values, nowMs, { amountDerived })\n await updateDoc(\n doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id),\n {\n ...set,\n ...Object.fromEntries(clear.map((key) => [key, deleteField()])),\n // The custom keys that changed, merged into the stored map.\n ...crmCustomDraftWrites(storedCustom, custom),\n // The title is what the Deals table searches (AGL-3321),\n // from the listener's row with the edit over it.\n ...crmClientListFields(\n 'deals',\n deal as unknown as Record<string, unknown>,\n set,\n CRM_CLIENT_SEARCH_FIELDS,\n ),\n },\n )\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return\n }\n enqueueSnackbar('Deal saved', { variant: 'success', persist: false })\n onSaved?.(deal.$id, 'edit')\n } else {\n // Held by the button until the capturing site is known; checked\n // again here because a callback can outlive the render that held it.\n if (!createHostId) return\n const customDocument = crmCustomDraftDocument(custom)\n const record: Record<string, unknown> = {\n ...dealDocumentFromForm(values, {\n visibleTo: [...createTokens],\n hostId: createHostId,\n uid: user.uid,\n nowMs,\n }),\n ...(customDocument ? { custom: customDocument } : {}),\n }\n const created = await addDoc(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n // What the Deals table searches and filters by (AGL-3321), and\n // nothing scheduled against a deal that did not exist.\n { ...record, ...crmNewRecordListFields('deals', record) },\n )\n // Setup → Activity shows CRM work (AGL-2622): the deal is org data,\n // but the act happened in this site's console and belongs in its\n // feed. Creation only — an edit is a save the card reports.\n logActivity('Added deal', { type: 'deal', id: created.id, name: values.title })\n enqueueSnackbar('Deal created', { variant: 'success', persist: false })\n onSaved?.(created.id, 'create')\n }\n onClose()\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [\n problem,\n bandFull,\n orgId,\n user,\n deal,\n values,\n unreadable,\n fromCache,\n firestore,\n createTokens,\n createHostId,\n logActivity,\n enqueueSnackbar,\n onSaved,\n onClose,\n storedCustom,\n custom,\n ])\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={busy ? undefined : onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton color=\"inherit\" edge=\"start\" onClick={onClose} sx={{ mr: 2 }}>\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {deal ? 'Edit deal' : 'New deal'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2} sx={{ width: { xs: '100%', sm: 420 } }}>\n {/*\n First, because the scope of everything below follows from it: at\n the organization level a new deal names the site that captures\n it. Nothing under a site, and nothing on an edit — a deal keeps\n the site it was made on (AGL-2630).\n */}\n {mode === 'create' ? (\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText=\"The site this deal belongs to — it decides which of your sites may see it.\"\n />\n ) : null}\n <TextField\n label=\"Title\"\n value={values.title}\n onChange={(event) => update({ title: event.target.value })}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_TITLE_MAX } }}\n />\n {mode === 'create' ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n select\n label=\"Pipeline\"\n value={pipelines.some((entry) => entry.$id === values.pipelineId) ? values.pipelineId : ''}\n onChange={(event) => {\n const next = pipelines.find(\n (entry) => entry.$id === event.target.value,\n )\n update({\n pipelineId: event.target.value,\n stageId: openStages(next)[0]?.id ?? '',\n })\n }}\n sx={{ flex: 1 }}\n >\n {pipelines.map((entry) => (\n <MenuItem key={entry.$id} value={entry.$id}>\n {entry.name}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n label=\"Stage\"\n value={stages.some((stage) => stage.id === values.stageId) ? values.stageId : ''}\n onChange={(event) => update({ stageId: event.target.value })}\n sx={{ flex: 1 }}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Amount\"\n value={values.amount}\n onChange={(event) => update({ amount: event.target.value })}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n helperText={\n amountDerived\n ? \"The sum of the deal's line items — edit them on the Products card.\"\n : undefined\n }\n slotProps={{\n htmlInput: { inputMode: 'decimal', readOnly: amountDerived },\n }}\n />\n <TextField\n select\n label=\"Currency\"\n value={values.currency}\n onChange={(event) => update({ currency: event.target.value })}\n disabled={amountDerived}\n sx={{ width: 120 }}\n >\n {DEAL_CURRENCIES.map((code) => (\n <MenuItem key={code} value={code}>\n {code.toUpperCase()}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n <TextField\n type=\"date\"\n label=\"Expected close\"\n value={values.expectedClose}\n onChange={(event) => update({ expectedClose: event.target.value })}\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n <TextField\n select\n label=\"Owner\"\n // The stored owner resolved to a member — by uid, or by an\n // address the roster has — so the picker highlights the person\n // and a save writes their uid.\n value={owner ? owner.uid : values.ownerUid ? '__keep' : ''}\n onChange={(event) =>\n update({\n ownerUid: event.target.value === '__keep' ? values.ownerUid : event.target.value,\n })\n }\n fullWidth\n helperText={\n !roster.loading && !roster.members.length\n ? 'The roster could not be read; the owner stays as stored.'\n : undefined\n }\n >\n <MenuItem value=\"\">{'Nobody yet'}</MenuItem>\n {values.ownerUid && !owner ? (\n // The stored owner is not on the roster the route returned —\n // a member who left, or a roster that failed to load. Shown by\n // uid so the field is never blank while the deal has an owner.\n <MenuItem value=\"__keep\">{values.ownerUid}</MenuItem>\n ) : null}\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n <Autocomplete<ContactChoice, false, false, false>\n options={contactChoices}\n value={contactValue}\n inputValue={contactQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setContactQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n contactId: picked?.id ?? '',\n contactName: picked ? picked.name || picked.email : '',\n })\n }\n getOptionLabel={(choice) => choice.name || choice.email}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n // Matching is `contactChoicesFor`'s — the list's grammar over the\n // window — and never MUI's own substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.id}>\n <Stack sx={{ lineHeight: 1.25 }}>\n <Typography variant=\"body2\">{choice.name || choice.email}</Typography>\n {choice.name ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {choice.email}\n </Typography>\n ) : null}\n </Stack>\n </li>\n )}\n noOptionsText=\"No recent contact matches\"\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Contact\"\n placeholder=\"Name or email\"\n helperText=\"Searches your most recent contacts by name or email.\"\n />\n )}\n />\n <Autocomplete<CompanyChoice, false, false, false>\n options={companyChoices}\n value={companyValue}\n inputValue={companyQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setCompanyQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n companyId: picked?.id ?? '',\n companyName: picked?.name ?? '',\n })\n }\n getOptionLabel={(choice) => choice.name}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n filterOptions={(options) => options}\n noOptionsText=\"No company matches\"\n renderInput={(params) => (\n <TextField {...params} label=\"Company\" placeholder=\"Company name\" />\n )}\n />\n <TextField\n label=\"Notes\"\n value={values.notes}\n onChange={(event) => update({ notes: event.target.value })}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_NOTES_MAX } }}\n />\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'Custom fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={busy}\n />\n ))}\n </>\n ) : null}\n {bandFull ? (\n <Alert severity=\"warning\">{CRM_RECORDS_BAND_FULL_MESSAGE}</Alert>\n ) : null}\n {problem && values.title ? <Alert severity=\"warning\">{problem}</Alert> : null}\n <Button\n variant=\"contained\"\n disabled={\n busy ||\n Boolean(problem) ||\n !orgId ||\n bandFull ||\n // A new deal waits for its capturing site.\n (mode === 'create' && !createHostId)\n }\n onClick={() => void handleSave()}\n >\n {deal ? 'Save deal' : 'Create deal'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nDealEditDrawer.displayName = 'DealEditDrawer'\n\nexport default DealEditDrawer\n"],"names":["CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","crmNewRecordListFields","dealHasLineItems","findOrgMember","nameSearchKey","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","useSnackbar","useFirestore","useFirestoreCollection","useUser","writeGuardedBySeed","Alert","Autocomplete","Button","IconButton","MenuItem","Stack","TextField","Typography","addDoc","collection","deleteField","doc","endAt","getDocs","limit","orderBy","query","startAt","updateDoc","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","useCrmRecordsQuota","crmScopeListable","crmVisibleToClause","useCrmScope","useOrgMemberDirectory","contactPrimaryGroup","crmCustomDraftDocument","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","CrmSitePicker","CRM_CLIENT_SEARCH_FIELDS","crmClientListFields","DEAL_CURRENCIES","openStages","contactChoicesFor","DEAL_NOTES_MAX","DEAL_TITLE_MAX","dealDocumentFromForm","dealFormFromDoc","dealFormProblem","dealPatchFromForm","emptyDealForm","CONTACT_WINDOW","COMPANY_MATCHES","DealEditDrawer","props","contactChoices","open","onClose","hostId","org","deal","defaults","pipelines","defaultPipeline","unreadable","fromCache","onSaved","mode","firestore","enqueueSnackbar","data","user","orgId","consentGroup","visibleTo","createHostId","createTokens","logActivity","roster","values","setValues","owner","members","ownerUid","busy","setBusy","fields","storedCustom","custom","setCustom","update","patch","current","pipeline","find","entry","$id","pipelineId","stages","contactRows","idField","contactQuery","setContactQuery","groupId","row","contactValue","contactId","id","name","contactName","email","choice","companyQuery","setCompanyQuery","companyChoices","setCompanyChoices","key","active","timer","setTimeout","companies","then","snapshot","docs","map","String","get","catch","clearTimeout","companyValue","companyId","companyName","customMissing","problem","length","join","amountDerived","Boolean","recordsScope","records","bandFull","ready","quota","allowed","handleSave","uid","variant","persist","nowMs","Date","now","verdict","subject","set","clear","deals","Object","fromEntries","ok","message","customDocument","record","created","type","title","error","console","allowDuplicate","anchor","undefined","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","appBarRight","disabled","gutterY","spacing","width","xs","sm","helperText","label","value","onChange","event","target","autoFocus","fullWidth","slotProps","htmlInput","maxLength","direction","select","some","next","stageId","flex","stage","alignItems","amount","placeholder","inputMode","readOnly","currency","code","toUpperCase","expectedClose","inputLabel","shrink","loading","member","options","inputValue","onInputChange","_event","reason","picked","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","lineHeight","noOptionsText","renderInput","params","notes","multiline","minRows","definition","severity","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-edit-drawer.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 {\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n crmNewRecordListFields,\n dealHasLineItems,\n findOrgMember,\n nameSearchKey,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteField,\n doc,\n endAt,\n getDocs,\n limit,\n orderBy,\n query,\n startAt,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport {\n type CrmOrgDoc,\n crmScopeListable,\n crmVisibleToClause,\n useCrmScope,\n} from '../hooks/use-crm-scope'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { contactPrimaryGroup } from '../model/contact-record'\nimport {\n type CrmCustomDraft,\n crmCustomDraftDocument,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmSitePicker } from './crm-site-picker'\nimport { CRM_CLIENT_SEARCH_FIELDS, crmClientListFields } from '../model/crm-list-query'\nimport {\n DEAL_CURRENCIES,\n type DealDoc,\n openStages,\n type PipelineDoc,\n} from '../model/deal-board-model'\nimport {\n type ContactChoice,\n contactChoicesFor,\n DEAL_NOTES_MAX,\n DEAL_TITLE_MAX,\n dealDocumentFromForm,\n dealFormFromDoc,\n dealFormProblem,\n type DealFormValues,\n dealPatchFromForm,\n emptyDealForm,\n} from '../model/deal-form-model'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\n/**\n * How many recent contacts the picker matches against. The window is the\n * ask — it is read when the drawer opens, not when the section does — and\n * a person outside it is still linkable by pasting their address, which\n * the match tests as a prefix of the email.\n */\nconst CONTACT_WINDOW = 300\n\n/** Companies offered per keystroke; a longer list is a scroll, not a pick. */\nconst COMPANY_MATCHES = 8\n\n/** A company as the picker offers it. */\ninterface CompanyChoice {\n id: string\n name: string\n}\n\nexport interface DealEditDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the drawer is opened under, or `null` at the organization\n * level (AGL-2630), where a NEW deal asks which site captures it and an\n * edit keeps the deal's own.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The deal being edited; absent for a new one. */\n deal?: DealDoc | null\n /** Links and values a new deal opens with — a contact's page passes itself. */\n defaults?: Partial<DealFormValues>\n pipelines: PipelineDoc[]\n /** Where a new deal lands when nobody picks a pipeline. */\n defaultPipeline: PipelineDoc | null\n /** The listener's verdict on the row being edited, for the stale-seed guard. */\n unreadable?: boolean\n fromCache?: boolean\n /** After a save, with the id of the deal written. */\n onSaved?: (dealId: string, mode: 'create' | 'edit') => void\n}\n\n/**\n * The one form a deal is created and edited through (AGL-2598).\n *\n * ## What it writes, and what it does not\n *\n * Client-direct against the rules, like a contact's notes: the title, the\n * amount and its currency, the expected close, the owner, the linked\n * contact and company, and the notes. The stage and the status are shown on\n * a new deal — it has to start somewhere — and never on an existing one,\n * because a stage change is the moment an automation listens for and only\n * the server route can emit that event. A drawer that wrote `stageId`\n * directly would move a deal without anybody hearing it.\n *\n * Nor the amount of a deal that has line items (AGL-2620): it is their\n * sum, the products card on the deal's page owns it, and the field here\n * is read-only with a caption saying so. The pipeline picker on a new\n * deal lists every ACTIVE pipeline (`pipelines`) and opens on the one the\n * caller was looking at.\n *\n * ## The pickers\n *\n * The owner is picked from the org's roster, the company by a server-side\n * prefix on `nameLower` (indexed with the scope), and the contact from a\n * bounded window of the newest contacts matched in memory — see\n * `contactChoicesFor` for why the contact search cannot be a query. Both\n * pickers copy the chosen NAME onto the deal beside the id, so a board of\n * cards can caption itself without a read per card.\n */\nexport function DealEditDrawer(props: DealEditDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n deal,\n defaults,\n pipelines,\n defaultPipeline,\n unreadable = false,\n fromCache = false,\n onSaved,\n } = props\n const mode: 'create' | 'edit' = deal ? 'edit' : 'create'\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const { orgId, consentGroup, visibleTo, createHostId, createTokens } =\n useCrmScope({ hostId, org })\n // A client-direct write owes the org's sharing rules a re-evaluation (AGL-3336).\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n // The feed the act is logged in is the level it was PERFORMED at\n // (AGL-2738): this site's under a site, the organization's at the org\n // hub. `createHostId` is where the deal is STAMPED, which at the hub is a\n // site the reader picked and may hold no role on — a different question\n // from where the line belongs, and a gate the record write never faced.\n const logActivity = useCrmActivityLogger(hostId)\n const roster = useOrgMemberDirectory(open ? orgId : null)\n\n const [values, setValues] = useState<DealFormValues>(() =>\n emptyDealForm(defaultPipeline, defaults),\n )\n const owner = findOrgMember(roster.members, values.ownerUid)\n const [busy, setBusy] = useState(false)\n /*\n * THE CUSTOM FIELDS (AGL-2661): the org's deal definitions, and the keys\n * the reader has touched over what the deal stores — saved as the\n * difference (`crm-custom-draft`) in the same write as the fixed fields.\n */\n const fields = useContactFieldDefinitions(open ? orgId : null, 'deal')\n const storedCustom = useMemo(() => deal?.custom ?? {}, [deal?.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n // Re-seeded on every open: an existing deal's stored values, or a blank\n // form aimed at the default pipeline with whatever the opener preselected.\n useEffect(() => {\n if (!open) return\n setValues(deal ? dealFormFromDoc(deal) : emptyDealForm(defaultPipeline, defaults))\n setCustom({})\n }, [open, deal, defaultPipeline, defaults])\n\n const update = useCallback(\n (patch: Partial<DealFormValues>) =>\n setValues((current) => ({ ...current, ...patch })),\n [],\n )\n\n const pipeline = useMemo(\n () =>\n pipelines.find((entry) => entry.$id === values.pipelineId) ??\n defaultPipeline,\n [pipelines, values.pipelineId, defaultPipeline],\n )\n const stages = useMemo(() => openStages(pipeline), [pipeline])\n\n /*\n * THE CONTACT WINDOW: the newest contacts this viewer may see, read only\n * while the drawer is open. Scoped and ordered like the contacts list —\n * the same `(visibleTo, updatedAt DESC)` index — and bounded, because the\n * picker's job is to find one person, not to list the audience.\n */\n const { data: contactRows } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n open && orgId && crmScopeListable(visibleTo)\n ? query(\n collection(firestore, 'orgs', orgId, 'contacts'),\n ...crmVisibleToClause(visibleTo),\n orderBy('updatedAt', 'desc'),\n limit(CONTACT_WINDOW),\n )\n : null,\n [firestore, open, orgId, visibleTo],\n { idField: '$id' },\n )\n const [contactQuery, setContactQuery] = useState('')\n // Named through the viewing group under a site; at the organization level\n // through each person's own primary holder.\n const contactChoices = useMemo(\n () =>\n contactChoicesFor(\n contactQuery,\n contactRows ?? [],\n consentGroup?.groupId ??\n ((row) => contactPrimaryGroup(row, org as Record<string, unknown>).groupId),\n ),\n [contactQuery, contactRows, consentGroup, org],\n )\n const contactValue: ContactChoice | null = values.contactId\n ? {\n id: values.contactId,\n name: values.contactName,\n email:\n contactChoices.find((choice) => choice.id === values.contactId)?.email ?? '',\n }\n : null\n\n /*\n * THE COMPANY SEARCH: a prefix on `nameLower` with the scope, which the\n * companies collection indexes. Debounced so a name typed at speed is one\n * query rather than one per letter.\n */\n const [companyQuery, setCompanyQuery] = useState('')\n const [companyChoices, setCompanyChoices] = useState<CompanyChoice[]>([])\n useEffect(() => {\n if (!open || !orgId || !crmScopeListable(visibleTo)) return\n const key = nameSearchKey(companyQuery)\n let active = true\n const timer = setTimeout(() => {\n void getDocs(\n query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n ...(key ? [startAt(key), endAt(`${key}\\uf8ff`)] : []),\n limit(COMPANY_MATCHES),\n ),\n )\n .then((snapshot) => {\n if (!active) return\n setCompanyChoices(\n snapshot.docs.map((entry) => ({\n id: entry.id,\n name: String(entry.get('name') ?? ''),\n })),\n )\n })\n .catch(() => {\n if (active) setCompanyChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, orgId, visibleTo, companyQuery])\n const companyValue: CompanyChoice | null = values.companyId\n ? { id: values.companyId, name: values.companyName }\n : null\n\n const customMissing = crmCustomDraftMissingRequired(fields.active, storedCustom, custom, mode)\n const problem =\n dealFormProblem(values, mode) ??\n (customMissing.length ? `${customMissing.join(', ')} is required.` : null)\n const amountDerived = Boolean(deal && dealHasLineItems(deal))\n\n /*\n * THE RECORDS BAND (AGL-2611), read only while a CREATE is open: a deal\n * is a record of the band the contacts list is banded by, and on an org\n * at a full hard band this drawer refuses the way `upsertHostContact`\n * refuses a capture — same number, same sentence. Memoized, because the\n * tuple is an effect dependency and a fresh one per render would re-read\n * the three aggregates on every keystroke.\n */\n const recordsScope = useMemo(\n () => (open && mode === 'create' && orgId ? (['orgs', orgId] as const) : null),\n [open, mode, orgId],\n )\n const records = useCrmRecordsQuota(recordsScope, org)\n const bandFull = mode === 'create' && records.ready && !records.quota.allowed\n\n const handleSave = useCallback(async () => {\n if (problem || !orgId || !user?.uid) return\n if (bandFull) {\n enqueueSnackbar(CRM_RECORDS_BAND_FULL_MESSAGE, { variant: 'warning', persist: false })\n return\n }\n setBusy(true)\n try {\n const nowMs = Date.now()\n if (deal) {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n const { set, clear } = dealPatchFromForm(values, nowMs, { amountDerived })\n await updateDoc(\n doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id),\n {\n ...set,\n ...Object.fromEntries(clear.map((key) => [key, deleteField()])),\n // The custom keys that changed, merged into the stored map.\n ...crmCustomDraftWrites(storedCustom, custom),\n // The title is what the Deals table searches (AGL-3321),\n // from the listener's row with the edit over it.\n ...crmClientListFields(\n 'deals',\n deal as unknown as Record<string, unknown>,\n set,\n CRM_CLIENT_SEARCH_FIELDS,\n ),\n },\n )\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return\n }\n followUpSharing('deals', [deal.$id])\n enqueueSnackbar('Deal saved', { variant: 'success', persist: false })\n onSaved?.(deal.$id, 'edit')\n } else {\n // Held by the button until the capturing site is known; checked\n // again here because a callback can outlive the render that held it.\n if (!createHostId) return\n const customDocument = crmCustomDraftDocument(custom)\n const record: Record<string, unknown> = {\n ...dealDocumentFromForm(values, {\n visibleTo: [...createTokens],\n hostId: createHostId,\n uid: user.uid,\n nowMs,\n }),\n ...(customDocument ? { custom: customDocument } : {}),\n }\n const created = await addDoc(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n // What the Deals table searches and filters by (AGL-3321), and\n // nothing scheduled against a deal that did not exist.\n { ...record, ...crmNewRecordListFields('deals', record) },\n )\n // Setup → Activity shows CRM work (AGL-2622): the deal is org data,\n // but the act happened in this site's console and belongs in its\n // feed. Creation only — an edit is a save the card reports.\n followUpSharing('deals', [created.id])\n logActivity('Added deal', { type: 'deal', id: created.id, name: values.title })\n enqueueSnackbar('Deal created', { variant: 'success', persist: false })\n onSaved?.(created.id, 'create')\n }\n onClose()\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [\n problem,\n bandFull,\n orgId,\n followUpSharing,\n user,\n deal,\n values,\n unreadable,\n fromCache,\n firestore,\n createTokens,\n createHostId,\n logActivity,\n enqueueSnackbar,\n onSaved,\n onClose,\n storedCustom,\n custom,\n ])\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={busy ? undefined : onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton color=\"inherit\" edge=\"start\" onClick={onClose} sx={{ mr: 2 }}>\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {deal ? 'Edit deal' : 'New deal'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2} sx={{ width: { xs: '100%', sm: 420 } }}>\n {/*\n First, because the scope of everything below follows from it: at\n the organization level a new deal names the site that captures\n it. Nothing under a site, and nothing on an edit — a deal keeps\n the site it was made on (AGL-2630).\n */}\n {mode === 'create' ? (\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText=\"The site this deal belongs to — it decides which of your sites may see it.\"\n />\n ) : null}\n <TextField\n label=\"Title\"\n value={values.title}\n onChange={(event) => update({ title: event.target.value })}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_TITLE_MAX } }}\n />\n {mode === 'create' ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n select\n label=\"Pipeline\"\n value={pipelines.some((entry) => entry.$id === values.pipelineId) ? values.pipelineId : ''}\n onChange={(event) => {\n const next = pipelines.find(\n (entry) => entry.$id === event.target.value,\n )\n update({\n pipelineId: event.target.value,\n stageId: openStages(next)[0]?.id ?? '',\n })\n }}\n sx={{ flex: 1 }}\n >\n {pipelines.map((entry) => (\n <MenuItem key={entry.$id} value={entry.$id}>\n {entry.name}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n label=\"Stage\"\n value={stages.some((stage) => stage.id === values.stageId) ? values.stageId : ''}\n onChange={(event) => update({ stageId: event.target.value })}\n sx={{ flex: 1 }}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Amount\"\n value={values.amount}\n onChange={(event) => update({ amount: event.target.value })}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n helperText={\n amountDerived\n ? \"The sum of the deal's line items — edit them on the Products card.\"\n : undefined\n }\n slotProps={{\n htmlInput: { inputMode: 'decimal', readOnly: amountDerived },\n }}\n />\n <TextField\n select\n label=\"Currency\"\n value={values.currency}\n onChange={(event) => update({ currency: event.target.value })}\n disabled={amountDerived}\n sx={{ width: 120 }}\n >\n {DEAL_CURRENCIES.map((code) => (\n <MenuItem key={code} value={code}>\n {code.toUpperCase()}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n <TextField\n type=\"date\"\n label=\"Expected close\"\n value={values.expectedClose}\n onChange={(event) => update({ expectedClose: event.target.value })}\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n <TextField\n select\n label=\"Owner\"\n // The stored owner resolved to a member — by uid, or by an\n // address the roster has — so the picker highlights the person\n // and a save writes their uid.\n value={owner ? owner.uid : values.ownerUid ? '__keep' : ''}\n onChange={(event) =>\n update({\n ownerUid: event.target.value === '__keep' ? values.ownerUid : event.target.value,\n })\n }\n fullWidth\n helperText={\n !roster.loading && !roster.members.length\n ? 'The roster could not be read; the owner stays as stored.'\n : undefined\n }\n >\n <MenuItem value=\"\">{'Nobody yet'}</MenuItem>\n {values.ownerUid && !owner ? (\n // The stored owner is not on the roster the route returned —\n // a member who left, or a roster that failed to load. Shown by\n // uid so the field is never blank while the deal has an owner.\n <MenuItem value=\"__keep\">{values.ownerUid}</MenuItem>\n ) : null}\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n <Autocomplete<ContactChoice, false, false, false>\n options={contactChoices}\n value={contactValue}\n inputValue={contactQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setContactQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n contactId: picked?.id ?? '',\n contactName: picked ? picked.name || picked.email : '',\n })\n }\n getOptionLabel={(choice) => choice.name || choice.email}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n // Matching is `contactChoicesFor`'s — the list's grammar over the\n // window — and never MUI's own substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.id}>\n <Stack sx={{ lineHeight: 1.25 }}>\n <Typography variant=\"body2\">{choice.name || choice.email}</Typography>\n {choice.name ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {choice.email}\n </Typography>\n ) : null}\n </Stack>\n </li>\n )}\n noOptionsText=\"No recent contact matches\"\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Contact\"\n placeholder=\"Name or email\"\n helperText=\"Searches your most recent contacts by name or email.\"\n />\n )}\n />\n <Autocomplete<CompanyChoice, false, false, false>\n options={companyChoices}\n value={companyValue}\n inputValue={companyQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setCompanyQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n companyId: picked?.id ?? '',\n companyName: picked?.name ?? '',\n })\n }\n getOptionLabel={(choice) => choice.name}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n filterOptions={(options) => options}\n noOptionsText=\"No company matches\"\n renderInput={(params) => (\n <TextField {...params} label=\"Company\" placeholder=\"Company name\" />\n )}\n />\n <TextField\n label=\"Notes\"\n value={values.notes}\n onChange={(event) => update({ notes: event.target.value })}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_NOTES_MAX } }}\n />\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'Custom fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={busy}\n />\n ))}\n </>\n ) : null}\n {bandFull ? (\n <Alert severity=\"warning\">{CRM_RECORDS_BAND_FULL_MESSAGE}</Alert>\n ) : null}\n {problem && values.title ? <Alert severity=\"warning\">{problem}</Alert> : null}\n <Button\n variant=\"contained\"\n disabled={\n busy ||\n Boolean(problem) ||\n !orgId ||\n bandFull ||\n // A new deal waits for its capturing site.\n (mode === 'create' && !createHostId)\n }\n onClick={() => void handleSave()}\n >\n {deal ? 'Save deal' : 'Create deal'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nDealEditDrawer.displayName = 'DealEditDrawer'\n\nexport default DealEditDrawer\n"],"names":["CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","crmNewRecordListFields","dealHasLineItems","findOrgMember","nameSearchKey","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","useSnackbar","useFirestore","useFirestoreCollection","useUser","writeGuardedBySeed","Alert","Autocomplete","Button","IconButton","MenuItem","Stack","TextField","Typography","addDoc","collection","deleteField","doc","endAt","getDocs","limit","orderBy","query","startAt","updateDoc","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","useCrmRecordsQuota","crmScopeListable","crmVisibleToClause","useCrmScope","useOrgMemberDirectory","contactPrimaryGroup","crmCustomDraftDocument","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","CrmSitePicker","CRM_CLIENT_SEARCH_FIELDS","crmClientListFields","DEAL_CURRENCIES","openStages","contactChoicesFor","DEAL_NOTES_MAX","DEAL_TITLE_MAX","dealDocumentFromForm","dealFormFromDoc","dealFormProblem","dealPatchFromForm","emptyDealForm","useCrmSharingFollowUp","CONTACT_WINDOW","COMPANY_MATCHES","DealEditDrawer","props","contactChoices","open","onClose","hostId","org","deal","defaults","pipelines","defaultPipeline","unreadable","fromCache","onSaved","mode","firestore","enqueueSnackbar","data","user","orgId","consentGroup","visibleTo","createHostId","createTokens","followUpSharing","logActivity","roster","values","setValues","owner","members","ownerUid","busy","setBusy","fields","storedCustom","custom","setCustom","update","patch","current","pipeline","find","entry","$id","pipelineId","stages","contactRows","idField","contactQuery","setContactQuery","groupId","row","contactValue","contactId","id","name","contactName","email","choice","companyQuery","setCompanyQuery","companyChoices","setCompanyChoices","key","active","timer","setTimeout","companies","then","snapshot","docs","map","String","get","catch","clearTimeout","companyValue","companyId","companyName","customMissing","problem","length","join","amountDerived","Boolean","recordsScope","records","bandFull","ready","quota","allowed","handleSave","uid","variant","persist","nowMs","Date","now","verdict","subject","set","clear","deals","Object","fromEntries","ok","message","customDocument","record","created","type","title","error","console","allowDuplicate","anchor","undefined","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","appBarRight","disabled","gutterY","spacing","width","xs","sm","helperText","label","value","onChange","event","target","autoFocus","fullWidth","slotProps","htmlInput","maxLength","direction","select","some","next","stageId","flex","stage","alignItems","amount","placeholder","inputMode","readOnly","currency","code","toUpperCase","expectedClose","inputLabel","shrink","loading","member","options","inputValue","onInputChange","_event","reason","picked","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","lineHeight","noOptionsText","renderInput","params","notes","multiline","minRows","definition","severity","displayName"],"mappings":"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"}
@@ -48,6 +48,7 @@ import { dealsCsv } from "../model/deals-csv.js";
48
48
  import { CrmBulkBarFrame, CrmBulkValueDialog, countNoun } from "./crm-bulk-bar-frame.js";
49
49
  import CrmExportAllButton from "./crm-export-all-button.js";
50
50
  import { LostReasonDialog } from "./lost-reason-dialog.js";
51
+ import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
51
52
  const NOUN = {
52
53
  singular: 'deal',
53
54
  plural: 'deals'
@@ -63,11 +64,12 @@ export function DealsBulkBar(props) {
63
64
  }
64
65
  DealsBulkBar.displayName = 'DealsBulkBar';
65
66
  function DealsBulkBarBody(props) {
66
- var _roster_error, _ref, _ref1;
67
+ var _ref, _roster_error, _ref1, _ref2;
67
68
  var _selectedRows_;
68
69
  const { hostId, scope, rows, selected, onSelectedChange, pipelineById, roster, api, csv } = props;
69
70
  const firestore = useFirestore();
70
71
  const { confirm } = useConfirmationContext();
72
+ const followUpSharing = useCrmSharingFollowUp(hostId != null ? hostId : null, (_ref = scope == null ? void 0 : scope[1]) != null ? _ref : null);
71
73
  const logActivity = useHostActivityLogger(hostId != null ? hostId : undefined);
72
74
  const { busy, report, apply, dismissReport } = useCrmBulkApply({
73
75
  recordKind: 'deal'
@@ -123,11 +125,17 @@ function DealsBulkBarBody(props) {
123
125
  const runPlan = useCallback((plan, done)=>apply({
124
126
  attempted: plan.writes.length,
125
127
  skipped: plan.skipped,
126
- job: ()=>runCrmBulkWrites(writers, plan.writes, (write)=>write.label),
128
+ job: async ()=>{
129
+ const outcome = await runCrmBulkWrites(writers, plan.writes, (write)=>write.label);
130
+ // A client-direct write owes the sharing rules a re-evaluation (AGL-3336).
131
+ followUpSharing('deals', plan.writes.filter((write)=>write.kind === 'update').map((write)=>write.id));
132
+ return outcome;
133
+ },
127
134
  done
128
135
  }), [
129
136
  apply,
130
- writers
137
+ writers,
138
+ followUpSharing
131
139
  ]);
132
140
  /** One route request per deal, in order, named by title. */ const runCalls = useCallback((call, done)=>apply({
133
141
  attempted: selectedRows.length,
@@ -285,7 +293,7 @@ function DealsBulkBarBody(props) {
285
293
  }),
286
294
  /*#__PURE__*/ _jsx(LostReasonDialog, {
287
295
  open: losing,
288
- dealTitle: (_ref = (_selectedRows_ = selectedRows[0]) == null ? void 0 : _selectedRows_.title) != null ? _ref : '',
296
+ dealTitle: (_ref1 = (_selectedRows_ = selectedRows[0]) == null ? void 0 : _selectedRows_.title) != null ? _ref1 : '',
289
297
  count: selectedRows.length,
290
298
  busy: busy,
291
299
  onClose: ()=>setLosing(false),
@@ -320,7 +328,7 @@ function DealsBulkBarBody(props) {
320
328
  }),
321
329
  /*#__PURE__*/ _jsx(CrmExportAllButton, {
322
330
  resource: "deals",
323
- orgId: (_ref1 = scope == null ? void 0 : scope[1]) != null ? _ref1 : null,
331
+ orgId: (_ref2 = scope == null ? void 0 : scope[1]) != null ? _ref2 : null,
324
332
  hostId: hostId,
325
333
  disabled: busy
326
334
  }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deals-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 deals table, for whatever rows are ticked (AGL-2621).\n *\n * Move them to a stage, mark them lost, hand them to an owner, take them\n * into a spreadsheet, or delete them. The owner and the delete are\n * document writes and go through the shared runner in batches; the stage\n * and the loss go through `crm/deal-stage`, ONE REQUEST PER DEAL, because\n * a stage change is what an automation listens for and only the route\n * emits the event — a batch that wrote `stageId` would move forty deals\n * and tell no workflow. Each refusal carries the route's own sentence,\n * named by the deal.\n *\n * ## Set stage needs one pipeline\n *\n * A stage belongs to a pipeline, and a selection that spans two has no\n * one list of stages to offer. The dialog says so rather than offering the\n * first pipeline's stages to a deal in the second, which the route would\n * refuse one row at a time.\n */\n\nimport { CRM_COLLECTIONS, dealStageById, crmMemberPickerLabel } from '@aglyn/aglyn'\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useFirestore, useHostActivityLogger } from '@aglyn/tenant-feature-instance'\nimport { Button, MenuItem, TextField, Typography } from '@mui/material'\nimport { deleteField, doc } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport type { DealStageApi } from '../hooks/use-deal-stage-api'\nimport type { OrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport {\n type CrmBulkPlan,\n type CrmBulkWrite,\n crmBulkWriters,\n runCrmBulkCalls,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport {\n type DealDoc,\n type PipelineDoc,\n closingStage,\n openStages,\n} from '../model/deal-board-model'\nimport { type DealCsvOptions, dealsCsv } from '../model/deals-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 { LostReasonDialog } from './lost-reason-dialog'\n\nexport interface DealsBulkBarProps {\n /**\n * The site whose activity feed the bar's acts are logged in, or `null`\n * at the organization level (AGL-2630), where the selection spans sites\n * and the org's feed takes one line for the action instead (AGL-2634);\n * a stage move runs as the org there and is logged per deal by the route.\n */\n hostId: string | null\n /** `['orgs', orgId]`, or `null` while the org is unresolved. */\n scope: readonly ['orgs', string] | null\n rows: readonly DealDoc[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /** The pipeline a deal's `pipelineId` names, for the stage list. */\n pipelineById: (id: string | undefined) => PipelineDoc | null\n /** The section's roster — already read for the Owner column. */\n roster: OrgMemberDirectory\n /** The one door a stage change goes through. */\n api: DealStageApi\n /** How the export names the pipeline, the stage and the owner — the table's own. */\n csv?: DealCsvOptions\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'deal', plural: 'deals' }\n\ntype PendingAction = 'stage' | 'owner'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n stage: 'Set the stage',\n owner: 'Set the owner',\n}\n\n/** The label a report lists a deal under. */\nconst labelOf = (deal: DealDoc): string => deal.title || 'Untitled deal'\n\nexport function DealsBulkBar(props: DealsBulkBarProps) {\n if (!props.selected.length) return null\n return <DealsBulkBarBody {...props} />\n}\nDealsBulkBar.displayName = 'DealsBulkBar'\n\nfunction DealsBulkBarBody(props: DealsBulkBarProps) {\n const { hostId, scope, rows, selected, onSelectedChange, pipelineById, roster, api, csv } =\n props\n const firestore = useFirestore()\n const { confirm } = useConfirmationContext()\n const logActivity = useHostActivityLogger(hostId ?? undefined)\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'deal' })\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 /*\n * The one pipeline the selection is in, or `null` when it spans more\n * than one — in which case Set stage has no list to offer.\n */\n const pipeline = useMemo(() => {\n const ids = new Set(selectedRows.map((deal) => String(deal.pipelineId ?? '')))\n return ids.size === 1 ? pipelineById([...ids][0]) : null\n }, [selectedRows, pipelineById])\n const stages = useMemo(() => {\n if (!pipeline) return []\n const won = closingStage(pipeline, 'won')\n return [...openStages(pipeline), ...(won ? [won] : [])]\n }, [pipeline])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n const [losing, setLosing] = useState(false)\n\n const writers = useMemo(\n () =>\n crmBulkWriters(firestore, (id) =>\n doc(firestore, scope?.[0] ?? 'orgs', scope?.[1] ?? '', CRM_COLLECTIONS.deals, id),\n ),\n [firestore, scope],\n )\n\n const openAction = (action: PendingAction) => {\n setValue('')\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 /** One route request per deal, in order, named by title. */\n const runCalls = useCallback(\n (call: (deal: DealDoc) => Promise<unknown>, done: (count: number) => string) =>\n apply({\n attempted: selectedRows.length,\n skipped: [],\n job: () => runCrmBulkCalls(selectedRows, labelOf, call),\n done,\n // The stage route writes the org's line per deal (AGL-2634).\n loggedByRoute: true,\n }),\n [apply, selectedRows],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending || !scope) return\n const action = pending\n setPending(null)\n if (action === 'stage') {\n const stage = dealStageById(pipeline, value)\n if (!stage) return\n await runCalls(\n (deal) => api.moveToStage(deal, stage.id),\n (count) => `Moved ${countNoun(count, NOUN)} to ${stage.name}`,\n )\n return\n }\n const nowMs = Date.now()\n const writes: CrmBulkWrite[] = selectedRows.map((deal) => ({\n id: deal.$id,\n label: labelOf(deal),\n kind: 'update',\n data: {\n ownerUid: value ? value : deleteField(),\n updatedAt: new Date(nowMs),\n },\n }))\n await runPlan({ writes, skipped: [] }, (count) => `Owner set on ${countNoun(count, NOUN)}`)\n }, [pending, scope, value, pipeline, selectedRows, api, runCalls, runPlan])\n\n const handleLost = useCallback(\n async (reason: string) => {\n setLosing(false)\n await runCalls(\n (deal) => api.markLost(deal, reason),\n (count) => `Marked ${countNoun(count, NOUN)} lost`,\n )\n },\n [api, runCalls],\n )\n\n const handleExport = useCallback(() => {\n downloadTextFile('deals-selected.csv', 'text/csv', dealsCsv(selectedRows, csv))\n }, [selectedRows, csv])\n\n const handleDelete = useCallback(async () => {\n if (!scope || !selectedRows.length) return\n const count = selectedRows.length\n const confirmed = await confirm({\n title: count === 1 ? 'Delete this deal?' : `Delete ${count} deals?`,\n description:\n `${count === 1 ? 'It is' : 'They are'} removed from the pipeline along ` +\n 'with their notes. The contacts and the companies they name are not ' +\n 'touched.',\n confirmationText: count === 1 ? 'Delete deal' : 'Delete deals',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel.\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n const writes: CrmBulkWrite[] = selectedRows.map((deal) => ({\n id: deal.$id,\n label: labelOf(deal),\n kind: 'delete',\n }))\n const outcome = await runPlan(\n { writes, skipped: [] },\n (done) => `Deleted ${countNoun(done, NOUN)}`,\n )\n // One line per deal that went, as its own page would write.\n const refused = new Set(outcome.refused.map((row) => row.label))\n for (const deal of selectedRows) {\n if (!refused.has(labelOf(deal))) {\n logActivity('Deleted deal', { type: 'deal', id: deal.$id, name: deal.title })\n }\n }\n onSelectedChange(\n selectedRows.filter((deal) => refused.has(labelOf(deal))).map((deal) => deal.$id),\n )\n }, [scope, selectedRows, confirm, runPlan, logActivity, onSelectedChange])\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 <>\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={pending === 'owner' || Boolean(value)}\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'stage' ? (\n pipeline ? (\n <TextField\n select\n size=\"small\"\n label=\"Stage\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText={`${pipeline.name} — each move fires its event, as a drag does`}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The selected deals are in different pipelines, so there is ' +\n 'no one list of stages to move them to. Select deals from ' +\n 'one pipeline.'}\n </Typography>\n )\n ) : pending === 'owner' ? (\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n disabled={roster.loading}\n error={Boolean(roster.error)}\n helperText={roster.error ?? (roster.loading ? 'Loading the team…' : undefined)}\n >\n <MenuItem value=\"\">{'Nobody — clear the owner'}</MenuItem>\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n </CrmBulkValueDialog>\n <LostReasonDialog\n open={losing}\n dealTitle={selectedRows[0]?.title ?? ''}\n count={selectedRows.length}\n busy={busy}\n onClose={() => setLosing(false)}\n onConfirm={(reason) => void handleLost(reason)}\n />\n </>\n }\n >\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('stage')}>\n {'Set stage'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => setLosing(true)}>\n {'Mark lost'}\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=\"deals\"\n orgId={scope?.[1] ?? null}\n hostId={hostId}\n disabled={busy}\n />\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy || !scope}\n onClick={() => void handleDelete()}\n >\n {'Delete'}\n </Button>\n </CrmBulkBarFrame>\n )\n}\nDealsBulkBarBody.displayName = 'DealsBulkBarBody'\n\nexport default DealsBulkBar\n"],"names":["CRM_COLLECTIONS","dealStageById","crmMemberPickerLabel","useConfirmationContext","useFirestore","useHostActivityLogger","Button","MenuItem","TextField","Typography","deleteField","doc","useCallback","useMemo","useState","useCrmBulkApply","downloadTextFile","crmBulkWriters","runCrmBulkCalls","runCrmBulkWrites","closingStage","openStages","dealsCsv","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","LostReasonDialog","NOUN","singular","plural","ACTION_TITLES","stage","owner","labelOf","deal","title","DealsBulkBar","props","selected","length","DealsBulkBarBody","displayName","roster","selectedRows","hostId","scope","rows","onSelectedChange","pipelineById","api","csv","firestore","confirm","logActivity","undefined","busy","report","apply","dismissReport","recordKind","chosen","Set","filter","row","has","$id","pipeline","ids","map","String","pipelineId","size","stages","won","pending","setPending","value","setValue","losing","setLosing","writers","id","deals","openAction","action","runPlan","plan","done","attempted","writes","skipped","job","write","label","runCalls","call","loggedByRoute","handleApply","moveToStage","count","name","nowMs","Date","now","kind","data","ownerUid","updatedAt","handleLost","reason","markLost","handleExport","handleDelete","confirmed","description","confirmationText","confirmationButtonProps","color","then","catch","outcome","refused","type","noun","onClear","onDismissReport","extras","open","canApply","Boolean","onClose","onApply","select","onChange","event","target","helperText","variant","disabled","loading","error","members","member","uid","dealTitle","onConfirm","onClick","resource","orgId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deals-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 deals table, for whatever rows are ticked (AGL-2621).\n *\n * Move them to a stage, mark them lost, hand them to an owner, take them\n * into a spreadsheet, or delete them. The owner and the delete are\n * document writes and go through the shared runner in batches; the stage\n * and the loss go through `crm/deal-stage`, ONE REQUEST PER DEAL, because\n * a stage change is what an automation listens for and only the route\n * emits the event — a batch that wrote `stageId` would move forty deals\n * and tell no workflow. Each refusal carries the route's own sentence,\n * named by the deal.\n *\n * ## Set stage needs one pipeline\n *\n * A stage belongs to a pipeline, and a selection that spans two has no\n * one list of stages to offer. The dialog says so rather than offering the\n * first pipeline's stages to a deal in the second, which the route would\n * refuse one row at a time.\n */\n\nimport { CRM_COLLECTIONS, dealStageById, crmMemberPickerLabel } from '@aglyn/aglyn'\nimport { useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useFirestore, useHostActivityLogger } from '@aglyn/tenant-feature-instance'\nimport { Button, MenuItem, TextField, Typography } from '@mui/material'\nimport { deleteField, doc } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport type { DealStageApi } from '../hooks/use-deal-stage-api'\nimport type { OrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport {\n type CrmBulkPlan,\n type CrmBulkWrite,\n crmBulkWriters,\n runCrmBulkCalls,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport {\n type DealDoc,\n type PipelineDoc,\n closingStage,\n openStages,\n} from '../model/deal-board-model'\nimport { type DealCsvOptions, dealsCsv } from '../model/deals-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 { LostReasonDialog } from './lost-reason-dialog'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\nexport interface DealsBulkBarProps {\n /**\n * The site whose activity feed the bar's acts are logged in, or `null`\n * at the organization level (AGL-2630), where the selection spans sites\n * and the org's feed takes one line for the action instead (AGL-2634);\n * a stage move runs as the org there and is logged per deal by the route.\n */\n hostId: string | null\n /** `['orgs', orgId]`, or `null` while the org is unresolved. */\n scope: readonly ['orgs', string] | null\n rows: readonly DealDoc[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /** The pipeline a deal's `pipelineId` names, for the stage list. */\n pipelineById: (id: string | undefined) => PipelineDoc | null\n /** The section's roster — already read for the Owner column. */\n roster: OrgMemberDirectory\n /** The one door a stage change goes through. */\n api: DealStageApi\n /** How the export names the pipeline, the stage and the owner — the table's own. */\n csv?: DealCsvOptions\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'deal', plural: 'deals' }\n\ntype PendingAction = 'stage' | 'owner'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n stage: 'Set the stage',\n owner: 'Set the owner',\n}\n\n/** The label a report lists a deal under. */\nconst labelOf = (deal: DealDoc): string => deal.title || 'Untitled deal'\n\nexport function DealsBulkBar(props: DealsBulkBarProps) {\n if (!props.selected.length) return null\n return <DealsBulkBarBody {...props} />\n}\nDealsBulkBar.displayName = 'DealsBulkBar'\n\nfunction DealsBulkBarBody(props: DealsBulkBarProps) {\n const { hostId, scope, rows, selected, onSelectedChange, pipelineById, roster, api, csv } =\n props\n const firestore = useFirestore()\n const { confirm } = useConfirmationContext()\n const followUpSharing = useCrmSharingFollowUp(hostId ?? null, scope?.[1] ?? null)\n const logActivity = useHostActivityLogger(hostId ?? undefined)\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'deal' })\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 /*\n * The one pipeline the selection is in, or `null` when it spans more\n * than one — in which case Set stage has no list to offer.\n */\n const pipeline = useMemo(() => {\n const ids = new Set(selectedRows.map((deal) => String(deal.pipelineId ?? '')))\n return ids.size === 1 ? pipelineById([...ids][0]) : null\n }, [selectedRows, pipelineById])\n const stages = useMemo(() => {\n if (!pipeline) return []\n const won = closingStage(pipeline, 'won')\n return [...openStages(pipeline), ...(won ? [won] : [])]\n }, [pipeline])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n const [losing, setLosing] = useState(false)\n\n const writers = useMemo(\n () =>\n crmBulkWriters(firestore, (id) =>\n doc(firestore, scope?.[0] ?? 'orgs', scope?.[1] ?? '', CRM_COLLECTIONS.deals, id),\n ),\n [firestore, scope],\n )\n\n const openAction = (action: PendingAction) => {\n setValue('')\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 'deals',\n plan.writes.filter((write) => write.kind === 'update').map((write) => write.id),\n )\n return outcome\n },\n done,\n }),\n [apply, writers, followUpSharing],\n )\n\n /** One route request per deal, in order, named by title. */\n const runCalls = useCallback(\n (call: (deal: DealDoc) => Promise<unknown>, done: (count: number) => string) =>\n apply({\n attempted: selectedRows.length,\n skipped: [],\n job: () => runCrmBulkCalls(selectedRows, labelOf, call),\n done,\n // The stage route writes the org's line per deal (AGL-2634).\n loggedByRoute: true,\n }),\n [apply, selectedRows],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending || !scope) return\n const action = pending\n setPending(null)\n if (action === 'stage') {\n const stage = dealStageById(pipeline, value)\n if (!stage) return\n await runCalls(\n (deal) => api.moveToStage(deal, stage.id),\n (count) => `Moved ${countNoun(count, NOUN)} to ${stage.name}`,\n )\n return\n }\n const nowMs = Date.now()\n const writes: CrmBulkWrite[] = selectedRows.map((deal) => ({\n id: deal.$id,\n label: labelOf(deal),\n kind: 'update',\n data: {\n ownerUid: value ? value : deleteField(),\n updatedAt: new Date(nowMs),\n },\n }))\n await runPlan({ writes, skipped: [] }, (count) => `Owner set on ${countNoun(count, NOUN)}`)\n }, [pending, scope, value, pipeline, selectedRows, api, runCalls, runPlan])\n\n const handleLost = useCallback(\n async (reason: string) => {\n setLosing(false)\n await runCalls(\n (deal) => api.markLost(deal, reason),\n (count) => `Marked ${countNoun(count, NOUN)} lost`,\n )\n },\n [api, runCalls],\n )\n\n const handleExport = useCallback(() => {\n downloadTextFile('deals-selected.csv', 'text/csv', dealsCsv(selectedRows, csv))\n }, [selectedRows, csv])\n\n const handleDelete = useCallback(async () => {\n if (!scope || !selectedRows.length) return\n const count = selectedRows.length\n const confirmed = await confirm({\n title: count === 1 ? 'Delete this deal?' : `Delete ${count} deals?`,\n description:\n `${count === 1 ? 'It is' : 'They are'} removed from the pipeline along ` +\n 'with their notes. The contacts and the companies they name are not ' +\n 'touched.',\n confirmationText: count === 1 ? 'Delete deal' : 'Delete deals',\n confirmationButtonProps: { color: 'error' },\n })\n // `confirm` resolves with no value and REJECTS on cancel.\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n const writes: CrmBulkWrite[] = selectedRows.map((deal) => ({\n id: deal.$id,\n label: labelOf(deal),\n kind: 'delete',\n }))\n const outcome = await runPlan(\n { writes, skipped: [] },\n (done) => `Deleted ${countNoun(done, NOUN)}`,\n )\n // One line per deal that went, as its own page would write.\n const refused = new Set(outcome.refused.map((row) => row.label))\n for (const deal of selectedRows) {\n if (!refused.has(labelOf(deal))) {\n logActivity('Deleted deal', { type: 'deal', id: deal.$id, name: deal.title })\n }\n }\n onSelectedChange(\n selectedRows.filter((deal) => refused.has(labelOf(deal))).map((deal) => deal.$id),\n )\n }, [scope, selectedRows, confirm, runPlan, logActivity, onSelectedChange])\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 <>\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={pending === 'owner' || Boolean(value)}\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'stage' ? (\n pipeline ? (\n <TextField\n select\n size=\"small\"\n label=\"Stage\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText={`${pipeline.name} — each move fires its event, as a drag does`}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The selected deals are in different pipelines, so there is ' +\n 'no one list of stages to move them to. Select deals from ' +\n 'one pipeline.'}\n </Typography>\n )\n ) : pending === 'owner' ? (\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n disabled={roster.loading}\n error={Boolean(roster.error)}\n helperText={roster.error ?? (roster.loading ? 'Loading the team…' : undefined)}\n >\n <MenuItem value=\"\">{'Nobody — clear the owner'}</MenuItem>\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n </CrmBulkValueDialog>\n <LostReasonDialog\n open={losing}\n dealTitle={selectedRows[0]?.title ?? ''}\n count={selectedRows.length}\n busy={busy}\n onClose={() => setLosing(false)}\n onConfirm={(reason) => void handleLost(reason)}\n />\n </>\n }\n >\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('stage')}>\n {'Set stage'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy || !scope} onClick={() => setLosing(true)}>\n {'Mark lost'}\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=\"deals\"\n orgId={scope?.[1] ?? null}\n hostId={hostId}\n disabled={busy}\n />\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy || !scope}\n onClick={() => void handleDelete()}\n >\n {'Delete'}\n </Button>\n </CrmBulkBarFrame>\n )\n}\nDealsBulkBarBody.displayName = 'DealsBulkBarBody'\n\nexport default DealsBulkBar\n"],"names":["CRM_COLLECTIONS","dealStageById","crmMemberPickerLabel","useConfirmationContext","useFirestore","useHostActivityLogger","Button","MenuItem","TextField","Typography","deleteField","doc","useCallback","useMemo","useState","useCrmBulkApply","downloadTextFile","crmBulkWriters","runCrmBulkCalls","runCrmBulkWrites","closingStage","openStages","dealsCsv","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","LostReasonDialog","useCrmSharingFollowUp","NOUN","singular","plural","ACTION_TITLES","stage","owner","labelOf","deal","title","DealsBulkBar","props","selected","length","DealsBulkBarBody","displayName","roster","selectedRows","hostId","scope","rows","onSelectedChange","pipelineById","api","csv","firestore","confirm","followUpSharing","logActivity","undefined","busy","report","apply","dismissReport","recordKind","chosen","Set","filter","row","has","$id","pipeline","ids","map","String","pipelineId","size","stages","won","pending","setPending","value","setValue","losing","setLosing","writers","id","deals","openAction","action","runPlan","plan","done","attempted","writes","skipped","job","outcome","write","label","kind","runCalls","call","loggedByRoute","handleApply","moveToStage","count","name","nowMs","Date","now","data","ownerUid","updatedAt","handleLost","reason","markLost","handleExport","handleDelete","confirmed","description","confirmationText","confirmationButtonProps","color","then","catch","refused","type","noun","onClear","onDismissReport","extras","open","canApply","Boolean","onClose","onApply","select","onChange","event","target","helperText","variant","disabled","loading","error","members","member","uid","dealTitle","onConfirm","onClick","resource","orgId"],"mappings":"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"}
@@ -26,6 +26,7 @@ import { doc, serverTimestamp, updateDoc } from "firebase/firestore";
26
26
  import { useEffect, useState } from "react";
27
27
  import { useCrmScope } from "../hooks/use-crm-scope.js";
28
28
  import { crmClientListFields } from "../model/crm-list-query.js";
29
+ import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
29
30
  /** The list field a campaign filing moves (AGL-3321). */ const LEAD_CAMPAIGN_LIST_FIELDS = [
30
31
  'scopedCampaignIds'
31
32
  ];
@@ -70,6 +71,7 @@ import { crmClientListFields } from "../model/crm-list-query.js";
70
71
  const { orgId } = useCrmScope({
71
72
  hostId
72
73
  });
74
+ const followUpSharing = useCrmSharingFollowUp(hostId, orgId);
73
75
  const firestore = useFirestore();
74
76
  const { enqueueSnackbar } = useSnackbar();
75
77
  const stored = readCampaignIds(lead);
@@ -114,6 +116,10 @@ import { crmClientListFields } from "../model/crm-list-query.js";
114
116
  }));
115
117
  });
116
118
  setSaving(false);
119
+ // A rule may share by campaign (AGL-3336).
120
+ if (verdict.ok) followUpSharing('leads', [
121
+ leadId
122
+ ]);
117
123
  if (!verdict.ok) {
118
124
  var _verdict_message;
119
125
  enqueueSnackbar((_verdict_message = verdict.message) != null ? _verdict_message : 'The filing could not be saved.', {