@aglyn/plugins-crm 1.0.0-beta.164 → 1.0.0-beta.166

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 (71) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/contact-associations-card.js +21 -5
  3. package/src/lib/components/contact-associations-card.js.map +1 -1
  4. package/src/lib/components/contact-field-drawer.js +19 -2
  5. package/src/lib/components/contact-field-drawer.js.map +1 -1
  6. package/src/lib/components/crm-console-page.js +12 -7
  7. package/src/lib/components/crm-console-page.js.map +1 -1
  8. package/src/lib/components/crm-glance-card.js +4 -2
  9. package/src/lib/components/crm-glance-card.js.map +1 -1
  10. package/src/lib/components/fields-section.d.ts +2 -1
  11. package/src/lib/components/fields-section.js +6 -3
  12. package/src/lib/components/fields-section.js.map +1 -1
  13. package/src/lib/components/lead-campaigns-card.d.ts +59 -0
  14. package/src/lib/components/lead-campaigns-card.js +166 -0
  15. package/src/lib/components/lead-campaigns-card.js.map +1 -0
  16. package/src/lib/components/lead-detail-page.js +54 -4
  17. package/src/lib/components/lead-detail-page.js.map +1 -1
  18. package/src/lib/components/lead-history-card.js +5 -0
  19. package/src/lib/components/lead-history-card.js.map +1 -1
  20. package/src/lib/components/lead-properties-card.d.ts +12 -0
  21. package/src/lib/components/lead-properties-card.js +64 -6
  22. package/src/lib/components/lead-properties-card.js.map +1 -1
  23. package/src/lib/components/lead-unqualify-dialog.js +7 -1
  24. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  25. package/src/lib/components/leads-bulk-bar.d.ts +7 -2
  26. package/src/lib/components/leads-bulk-bar.js +42 -6
  27. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  28. package/src/lib/components/leads-section.js +43 -17
  29. package/src/lib/components/leads-section.js.map +1 -1
  30. package/src/lib/components/new-lead-drawer.d.ts +14 -1
  31. package/src/lib/components/new-lead-drawer.js +41 -3
  32. package/src/lib/components/new-lead-drawer.js.map +1 -1
  33. package/src/lib/components/reports/lead-counts.d.ts +16 -9
  34. package/src/lib/components/reports/lead-counts.js +29 -18
  35. package/src/lib/components/reports/lead-counts.js.map +1 -1
  36. package/src/lib/components/reports/lead-funnel-card.js +44 -40
  37. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  38. package/src/lib/hooks/use-campaign-filing-log.d.ts +29 -0
  39. package/src/lib/hooks/use-campaign-filing-log.js +91 -0
  40. package/src/lib/hooks/use-campaign-filing-log.js.map +1 -0
  41. package/src/lib/hooks/use-org-leads.d.ts +29 -32
  42. package/src/lib/hooks/use-org-leads.js +58 -68
  43. package/src/lib/hooks/use-org-leads.js.map +1 -1
  44. package/src/lib/model/campaign-filing-activity.d.ts +82 -0
  45. package/src/lib/model/campaign-filing-activity.js +88 -0
  46. package/src/lib/model/campaign-filing-activity.js.map +1 -0
  47. package/src/lib/model/crm-routes.d.ts +9 -6
  48. package/src/lib/model/crm-routes.js +9 -6
  49. package/src/lib/model/crm-routes.js.map +1 -1
  50. package/src/lib/server/campaign-filing-activity.d.ts +79 -0
  51. package/src/lib/server/campaign-filing-activity.js +93 -0
  52. package/src/lib/server/campaign-filing-activity.js.map +1 -0
  53. package/src/lib/server/capture-contact.js +11 -5
  54. package/src/lib/server/capture-contact.js.map +1 -1
  55. package/src/lib/server/convert-open-lead.js +6 -2
  56. package/src/lib/server/convert-open-lead.js.map +1 -1
  57. package/src/lib/server/email-send.js +3 -3
  58. package/src/lib/server/email-send.js.map +1 -1
  59. package/src/lib/server/erase-person.js +25 -7
  60. package/src/lib/server/erase-person.js.map +1 -1
  61. package/src/lib/server/lead-campaign-carry.js +28 -2
  62. package/src/lib/server/lead-campaign-carry.js.map +1 -1
  63. package/src/lib/server/lead-create.d.ts +15 -4
  64. package/src/lib/server/lead-create.js +75 -15
  65. package/src/lib/server/lead-create.js.map +1 -1
  66. package/src/lib/server/leads-import.js +2 -2
  67. package/src/lib/server/leads-import.js.map +1 -1
  68. package/src/lib/server/record-email-state.js +10 -0
  69. package/src/lib/server/record-email-state.js.map +1 -1
  70. package/src/lib/server/record-facts.js +3 -3
  71. package/src/lib/server/record-facts.js.map +1 -1
@@ -28,7 +28,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
28
28
  *
29
29
  * ## A row names its own site
30
30
  *
31
- * A lead lives at `hosts/{hostId}/leads/{leadId}`, and at the organization
31
+ * A lead lives at `orgs/{orgId}/leads/{leadId}` (AGL-3275), and at the organization
32
32
  * level the selection spans sites, so the reference for a write is built
33
33
  * from the ROW rather than from a scope the bar was handed: the row knows
34
34
  * its site, the bar need not. Under a site every row names the same one.
@@ -40,12 +40,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
40
40
  * menu and the inline select do. A lead already closed is not unqualified
41
41
  * twice. A lead already at the asked status is skipped rather than rewritten
42
42
  * so "Nothing to change" means what it says.
43
- */ import { CRM_LEAD_STATUS_LABELS, crmLeadStatus, isCrmLeadOpen } from "@aglyn/aglyn";
43
+ */ import { CRM_LEAD_STATUS_LABELS, crmLeadStatus, isCrmLeadOpen, readCampaignIds } from "@aglyn/aglyn";
44
44
  import CampaignPicker from "@aglyn/shared-ui-email-campaigns/components/campaign-picker.component";
45
45
  import { useFirestore, useHostCampaigns } from "@aglyn/tenant-feature-instance";
46
46
  import { Button, MenuItem, TextField } from "@mui/material";
47
47
  import { arrayUnion, deleteField, doc, serverTimestamp } from "firebase/firestore";
48
48
  import { useCallback, useMemo, useState } from "react";
49
+ import { useCampaignFilingLog } from "../hooks/use-campaign-filing-log.js";
49
50
  import { useCrmBulkApply } from "../hooks/use-crm-bulk-apply.js";
50
51
  import { downloadTextFile } from "../model/contacts-csv.js";
51
52
  import { crmBulkWriters, runCrmBulkWrites } from "../model/crm-bulk-writes.js";
@@ -101,6 +102,11 @@ function LeadsBulkBarBody(props) {
101
102
  const campaigns = useHostCampaigns(hostId != null ? hostId : undefined, {
102
103
  enabled: pending === 'campaign' && Boolean(hostId)
103
104
  });
105
+ const logFiling = useCampaignFilingLog({
106
+ orgId,
107
+ hostId,
108
+ org: props.org
109
+ });
104
110
  // The reference a write names is the row's own site and document.
105
111
  const writers = useMemo(()=>{
106
112
  const byId = new Map(rows.map((row)=>[
@@ -108,9 +114,14 @@ function LeadsBulkBarBody(props) {
108
114
  row
109
115
  ]));
110
116
  return crmBulkWriters(firestore, (id)=>{
111
- var _ref, _ref1;
117
+ var _ref;
112
118
  const row = byId.get(id);
113
- return doc(firestore, 'hosts', (_ref = row == null ? void 0 : row.hostId) != null ? _ref : '', 'leads', (_ref1 = row == null ? void 0 : row.leadId) != null ? _ref1 : id);
119
+ // One org collection (AGL-3275): the row no longer names a site, and
120
+ // the bulk write addresses the same document at either level.
121
+ // `orgs//leads` is a path Firestore accepts the shape of and nothing
122
+ // owns, so an absent org refuses the write rather than composing one.
123
+ if (!orgId) throw new Error('[crm] a lead write needs an organization');
124
+ return doc(firestore, 'orgs', orgId, 'leads', (_ref = row == null ? void 0 : row.leadId) != null ? _ref : id);
114
125
  });
115
126
  }, [
116
127
  firestore,
@@ -168,10 +179,33 @@ function LeadsBulkBarBody(props) {
168
179
  }
169
180
  });
170
181
  }
171
- await runPlan({
182
+ const outcome = await runPlan({
172
183
  writes,
173
184
  skipped
174
185
  }, (count)=>`Added ${countNoun(count, NOUN)} to ${campaignIds.length === 1 ? 'the campaign' : `${campaignIds.length} campaigns`}`);
186
+ // What landed is written on each lead's Activity (AGL-3274): one
187
+ // "Filed under" per campaign the lead was not already in, and none
188
+ // for a lead the store refused — the row's own document is the
189
+ // membership, and the entry only records the act.
190
+ const refused = new Set(outcome.refused.map((entry)=>entry.label));
191
+ const named = campaignIds.map((campaignId)=>{
192
+ var _ref;
193
+ var _campaigns_options_find;
194
+ return {
195
+ id: campaignId,
196
+ name: (_ref = (_campaigns_options_find = campaigns.options.find((option)=>option.value === campaignId)) == null ? void 0 : _campaigns_options_find.label) != null ? _ref : ''
197
+ };
198
+ });
199
+ for (const lead of selectedRows){
200
+ if (refused.has(labelOf(lead))) continue;
201
+ const already = readCampaignIds(lead);
202
+ const filed = named.filter((campaign)=>!already.includes(campaign.id));
203
+ if (filed.length) await logFiling({
204
+ leadId: lead.leadId
205
+ }, {
206
+ filed
207
+ });
208
+ }
175
209
  return;
176
210
  }
177
211
  if (action === 'status') {
@@ -242,8 +276,10 @@ function LeadsBulkBarBody(props) {
242
276
  pending,
243
277
  value,
244
278
  campaignIds,
279
+ campaigns.options,
245
280
  selectedRows,
246
- runPlan
281
+ runPlan,
282
+ logFiling
247
283
  ]);
248
284
  const handleExport = useCallback(()=>{
249
285
  downloadTextFile('leads-selected.csv', 'text/csv', leadsCsv(selectedRows, csv));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/leads-bulk-bar.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * The bar over the Leads table, for whatever rows are ticked (AGL-2662).\n *\n * Hand them to an owner, set their status, unqualify them with one reason,\n * or take them into a spreadsheet. Every act here is a document write and\n * nothing more — a lead's status and owner are the team's own notes on a\n * capture, with no event and no notification behind them, which is why the\n * row's inline select writes them client-direct — so the whole bar goes\n * through the shared batched runner under the same rules.\n *\n * ## A row names its own site\n *\n * A lead lives at `hosts/{hostId}/leads/{leadId}`, and at the organization\n * level the selection spans sites, so the reference for a write is built\n * from the ROW rather than from a scope the bar was handed: the row knows\n * its site, the bar need not. Under a site every row names the same one.\n *\n * ## What is declined, by name\n *\n * A converted lead's status IS its conversion, stamped by the route, and\n * the bar leaves it alone under Set status and Unqualify the way the row\n * menu and the inline select do. A lead already closed is not unqualified\n * twice. A lead already at the asked status is skipped rather than rewritten\n * so \"Nothing to change\" means what it says.\n */\n\nimport {\n CRM_LEAD_STATUS_LABELS,\n type CrmLeadFields,\n type CrmLeadStatus,\n crmLeadStatus,\n isCrmLeadOpen,\n} from '@aglyn/aglyn'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { useFirestore, useHostCampaigns } from '@aglyn/tenant-feature-instance'\nimport { Button, MenuItem, TextField } from '@mui/material'\nimport { arrayUnion, deleteField, doc, serverTimestamp } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport {\n type CrmBulkPlan,\n type CrmBulkSkip,\n type CrmBulkWrite,\n crmBulkWriters,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport { type LeadCsvOptions, leadsCsv } from '../model/leads-csv'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { UNQUALIFY_REASON_MAX } from './lead-unqualify-dialog'\n\n/**\n * One row of the Leads list as the bar needs it: the document's fields,\n * the grid's key, and which document under which site a write names.\n */\nexport type LeadBulkRow = Record<string, unknown> &\n CrmLeadFields & { $id: string; leadId: string; hostId: string }\n\nexport interface LeadsBulkBarProps {\n rows: readonly LeadBulkRow[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /** The section's roster — already read for the Owner column. */\n roster: OrgMemberOptions\n /** How the export names the owner and, at the org level, the site — the list's own. */\n csv?: LeadCsvOptions\n /** The organization these leads belong to; null while it is unresolved. */\n orgId?: string | null\n /**\n * The site the list is read under, or `null` at the organization level\n * where the complete export spans every site (AGL-2662).\n */\n hostId?: string | null\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'lead', plural: 'leads' }\n\ntype PendingAction = 'owner' | 'status' | 'unqualify' | 'campaign'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n owner: 'Set the owner',\n status: 'Set the status',\n unqualify: 'Unqualify',\n campaign: 'Add to campaign',\n}\n\n/** The statuses the bar can set — the open ones; closing goes through Unqualify. */\nconst SETTABLE_STATUSES: readonly CrmLeadStatus[] = ['new', 'working']\n\n/** The label a report lists a lead under — its name, else its address. */\nconst labelOf = (lead: LeadBulkRow): string =>\n String(lead['name'] || lead['email'] || lead.leadId)\n\nexport function LeadsBulkBar(props: LeadsBulkBarProps) {\n if (!props.selected.length) return null\n return <LeadsBulkBarBody {...props} />\n}\nLeadsBulkBar.displayName = 'LeadsBulkBar'\n\nfunction LeadsBulkBarBody(props: LeadsBulkBarProps) {\n const { rows, selected, onSelectedChange, roster, csv } = props\n const orgId = props.orgId ?? null\n const hostId = props.hostId ?? null\n const firestore = useFirestore()\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'lead' })\n\n const selectedRows = useMemo(() => {\n const chosen = new Set(selected)\n return rows.filter((row) => chosen.has(row.$id))\n }, [rows, selected])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n /*\n * The campaigns to add the selection to (AGL-3254), and the site's\n * containers the picker offers — read only while that dialog is open.\n * Under a site alone: a campaign belongs to one site, and at the\n * organization level a selection spans them.\n */\n const [campaignIds, setCampaignIds] = useState<string[]>([])\n const campaigns = useHostCampaigns(hostId ?? undefined, {\n enabled: pending === 'campaign' && Boolean(hostId),\n })\n\n // The reference a write names is the row's own site and document.\n const writers = useMemo(() => {\n const byId = new Map(rows.map((row) => [row.$id, row]))\n return crmBulkWriters(firestore, (id) => {\n const row = byId.get(id)\n return doc(firestore, 'hosts', row?.hostId ?? '', 'leads', row?.leadId ?? id)\n })\n }, [firestore, rows])\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setCampaignIds([])\n setPending(action)\n }\n\n const runPlan = useCallback(\n (plan: CrmBulkPlan, done: (count: number) => string) =>\n apply({\n attempted: plan.writes.length,\n skipped: plan.skipped,\n job: () => runCrmBulkWrites(writers, plan.writes, (write) => write.label),\n done,\n }),\n [apply, writers],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending) return\n const action = pending\n setPending(null)\n const writes: CrmBulkWrite[] = []\n const skipped: CrmBulkSkip[] = []\n if (action === 'owner') {\n for (const lead of selectedRows) {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: { ownerUid: value ? value : deleteField(), updatedAt: serverTimestamp() },\n })\n }\n await runPlan(\n { writes, skipped },\n (count) => (value ? `Owner set on ${countNoun(count, NOUN)}` : `Owner cleared on ${countNoun(count, NOUN)}`),\n )\n return\n }\n if (action === 'campaign') {\n // Added to what each lead carries — `arrayUnion`, the way a sequence's\n // enroll and the import add one — never in place of it.\n for (const lead of selectedRows) {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: { campaignIds: arrayUnion(...campaignIds), updatedAt: serverTimestamp() },\n })\n }\n await runPlan(\n { writes, skipped },\n (count) =>\n `Added ${countNoun(count, NOUN)} to ${campaignIds.length === 1 ? 'the campaign' : `${campaignIds.length} campaigns`}`,\n )\n return\n }\n if (action === 'status') {\n const status = value as CrmLeadStatus\n for (const lead of selectedRows) {\n const current = crmLeadStatus(lead)\n if (lead.convertedContactId) {\n skipped.push({ label: labelOf(lead), reason: 'was converted' })\n } else if (current === status) {\n skipped.push({\n label: labelOf(lead),\n reason: `already ${CRM_LEAD_STATUS_LABELS[status]}`,\n })\n } else {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n status,\n // Reopening an unqualified lead clears its reason, as the row does.\n ...(current === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n updatedAt: serverTimestamp(),\n },\n })\n }\n }\n await runPlan(\n { writes, skipped },\n (count) => `Status set on ${countNoun(count, NOUN)}`,\n )\n return\n }\n const reason = value.trim().slice(0, UNQUALIFY_REASON_MAX)\n for (const lead of selectedRows) {\n if (lead.convertedContactId) {\n skipped.push({ label: labelOf(lead), reason: 'was converted' })\n } else if (!isCrmLeadOpen(lead)) {\n skipped.push({ label: labelOf(lead), reason: 'is already closed' })\n } else {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n status: 'unqualified' satisfies CrmLeadStatus,\n unqualifiedReason: reason,\n updatedAt: serverTimestamp(),\n },\n })\n }\n }\n await runPlan(\n { writes, skipped },\n (count) => `Marked ${countNoun(count, NOUN)} unqualified`,\n )\n }, [pending, value, campaignIds, selectedRows, runPlan])\n\n const handleExport = useCallback(() => {\n downloadTextFile('leads-selected.csv', 'text/csv', leadsCsv(selectedRows, csv))\n }, [selectedRows, csv])\n\n const canApply =\n pending === 'owner' ||\n (pending === 'campaign'\n ? campaignIds.length > 0\n : pending === 'status'\n ? Boolean(value)\n : Boolean(value.trim()))\n\n return (\n <CrmBulkBarFrame\n count={selected.length}\n noun={NOUN}\n busy={busy}\n onClear={() => onSelectedChange([])}\n report={report}\n onDismissReport={dismissReport}\n extras={\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={canApply}\n applyLabel={pending === 'unqualify' ? 'Unqualify' : pending === 'campaign' ? 'Add' : undefined}\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'owner' ? (\n <LeadOwnerSelect value={value} onChange={setValue} roster={roster} size=\"medium\" />\n ) : pending === 'status' ? (\n <TextField\n select\n size=\"small\"\n label=\"Status\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText=\"Closing a lead goes through Unqualify, which asks why.\"\n >\n {SETTABLE_STATUSES.map((status) => (\n <MenuItem key={status} value={status}>\n {CRM_LEAD_STATUS_LABELS[status]}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'unqualify' ? (\n <TextField\n label=\"Reason\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n multiline\n minRows={2}\n autoFocus\n helperText=\"One reason for every selected lead, kept on each so it can be counted later.\"\n slotProps={{ htmlInput: { maxLength: UNQUALIFY_REASON_MAX } }}\n />\n ) : pending === 'campaign' ? (\n <CampaignPicker\n options={campaigns.options}\n value={campaignIds}\n onChange={setCampaignIds}\n helperText=\"Added to the campaigns each selected lead is already in. It does not decide who a campaign mails.\"\n empty={campaigns.ready && !campaigns.options.length}\n emptyText=\"This site has no campaigns yet. Create one from Marketing to file leads under it.\"\n />\n ) : null}\n </CrmBulkValueDialog>\n }\n >\n <Button size=\"small\" disabled={busy} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('status')}>\n {'Set status'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('unqualify')}>\n {'Unqualify'}\n </Button>\n {hostId ? (\n <Button size=\"small\" disabled={busy} onClick={() => openAction('campaign')}>\n {'Add to campaign'}\n </Button>\n ) : null}\n <Button size=\"small\" disabled={busy} onClick={handleExport}>\n {'Export CSV'}\n </Button>\n {/*\n The selection's file is the rows on screen; this one is the whole\n collection, streamed by the server (AGL-2662).\n */}\n <CrmExportAllButton\n resource=\"leads\"\n orgId={orgId}\n hostId={hostId}\n disabled={busy}\n />\n </CrmBulkBarFrame>\n )\n}\nLeadsBulkBarBody.displayName = 'LeadsBulkBarBody'\n\nexport default LeadsBulkBar\n"],"names":["CRM_LEAD_STATUS_LABELS","crmLeadStatus","isCrmLeadOpen","CampaignPicker","useFirestore","useHostCampaigns","Button","MenuItem","TextField","arrayUnion","deleteField","doc","serverTimestamp","useCallback","useMemo","useState","useCrmBulkApply","downloadTextFile","crmBulkWriters","runCrmBulkWrites","leadsCsv","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","LeadOwnerSelect","UNQUALIFY_REASON_MAX","NOUN","singular","plural","ACTION_TITLES","owner","status","unqualify","campaign","SETTABLE_STATUSES","labelOf","lead","String","leadId","LeadsBulkBar","props","selected","length","LeadsBulkBarBody","displayName","rows","onSelectedChange","roster","csv","orgId","hostId","firestore","busy","report","apply","dismissReport","recordKind","selectedRows","chosen","Set","filter","row","has","$id","pending","setPending","value","setValue","campaignIds","setCampaignIds","campaigns","undefined","enabled","Boolean","writers","byId","Map","map","id","get","openAction","action","runPlan","plan","done","attempted","writes","skipped","job","write","label","handleApply","push","kind","data","ownerUid","updatedAt","count","current","convertedContactId","reason","unqualifiedReason","trim","slice","handleExport","canApply","noun","onClear","onDismissReport","extras","open","title","applyLabel","onClose","onApply","onChange","size","select","event","target","helperText","multiline","minRows","autoFocus","slotProps","htmlInput","maxLength","options","empty","ready","emptyText","disabled","onClick","resource"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/leads-bulk-bar.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * The bar over the Leads table, for whatever rows are ticked (AGL-2662).\n *\n * Hand them to an owner, set their status, unqualify them with one reason,\n * or take them into a spreadsheet. Every act here is a document write and\n * nothing more — a lead's status and owner are the team's own notes on a\n * capture, with no event and no notification behind them, which is why the\n * row's inline select writes them client-direct — so the whole bar goes\n * through the shared batched runner under the same rules.\n *\n * ## A row names its own site\n *\n * A lead lives at `orgs/{orgId}/leads/{leadId}` (AGL-3275), and at the organization\n * level the selection spans sites, so the reference for a write is built\n * from the ROW rather than from a scope the bar was handed: the row knows\n * its site, the bar need not. Under a site every row names the same one.\n *\n * ## What is declined, by name\n *\n * A converted lead's status IS its conversion, stamped by the route, and\n * the bar leaves it alone under Set status and Unqualify the way the row\n * menu and the inline select do. A lead already closed is not unqualified\n * twice. A lead already at the asked status is skipped rather than rewritten\n * so \"Nothing to change\" means what it says.\n */\n\nimport {\n CRM_LEAD_STATUS_LABELS,\n type CrmLeadFields,\n type CrmLeadStatus,\n crmLeadStatus,\n isCrmLeadOpen,\n readCampaignIds,\n} from '@aglyn/aglyn'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { useFirestore, useHostCampaigns } from '@aglyn/tenant-feature-instance'\nimport { Button, MenuItem, TextField } from '@mui/material'\nimport { arrayUnion, deleteField, doc, serverTimestamp } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useCampaignFilingLog } from '../hooks/use-campaign-filing-log'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport {\n type CrmBulkPlan,\n type CrmBulkSkip,\n type CrmBulkWrite,\n crmBulkWriters,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport { type LeadCsvOptions, leadsCsv } from '../model/leads-csv'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { UNQUALIFY_REASON_MAX } from './lead-unqualify-dialog'\n\n/**\n * One row of the Leads list as the bar needs it: the document's fields,\n * the grid's key, and which document under which site a write names.\n */\nexport type LeadBulkRow = Record<string, unknown> &\n CrmLeadFields & { $id: string; leadId: string }\n\nexport interface LeadsBulkBarProps {\n rows: readonly LeadBulkRow[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /** The section's roster — already read for the Owner column. */\n roster: OrgMemberOptions\n /** How the export names the owner and, at the org level, the site — the list's own. */\n csv?: LeadCsvOptions\n /** The organization these leads belong to; null while it is unresolved. */\n orgId?: string | null\n /**\n * The site the list is read under, or `null` at the organization level\n * where the complete export spans every site (AGL-2662).\n */\n hostId?: string | null\n /**\n * The org the shell passed, for the scope a filing entry carries\n * (AGL-3274): Add to campaign writes one on each lead's Activity, and\n * the entry is visible to whoever sees a record made on this site.\n */\n org?: Record<string, unknown> | null\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'lead', plural: 'leads' }\n\ntype PendingAction = 'owner' | 'status' | 'unqualify' | 'campaign'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n owner: 'Set the owner',\n status: 'Set the status',\n unqualify: 'Unqualify',\n campaign: 'Add to campaign',\n}\n\n/** The statuses the bar can set — the open ones; closing goes through Unqualify. */\nconst SETTABLE_STATUSES: readonly CrmLeadStatus[] = ['new', 'working']\n\n/** The label a report lists a lead under — its name, else its address. */\nconst labelOf = (lead: LeadBulkRow): string =>\n String(lead['name'] || lead['email'] || lead.leadId)\n\nexport function LeadsBulkBar(props: LeadsBulkBarProps) {\n if (!props.selected.length) return null\n return <LeadsBulkBarBody {...props} />\n}\nLeadsBulkBar.displayName = 'LeadsBulkBar'\n\nfunction LeadsBulkBarBody(props: LeadsBulkBarProps) {\n const { rows, selected, onSelectedChange, roster, csv } = props\n const orgId = props.orgId ?? null\n const hostId = props.hostId ?? null\n const firestore = useFirestore()\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'lead' })\n\n const selectedRows = useMemo(() => {\n const chosen = new Set(selected)\n return rows.filter((row) => chosen.has(row.$id))\n }, [rows, selected])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n /*\n * The campaigns to add the selection to (AGL-3254), and the site's\n * containers the picker offers — read only while that dialog is open.\n * Under a site alone: a campaign belongs to one site, and at the\n * organization level a selection spans them.\n */\n const [campaignIds, setCampaignIds] = useState<string[]>([])\n const campaigns = useHostCampaigns(hostId ?? undefined, {\n enabled: pending === 'campaign' && Boolean(hostId),\n })\n const logFiling = useCampaignFilingLog({ orgId, hostId, org: props.org })\n\n // The reference a write names is the row's own site and document.\n const writers = useMemo(() => {\n const byId = new Map(rows.map((row) => [row.$id, row]))\n return crmBulkWriters(firestore, (id) => {\n const row = byId.get(id)\n // One org collection (AGL-3275): the row no longer names a site, and\n // the bulk write addresses the same document at either level.\n // `orgs//leads` is a path Firestore accepts the shape of and nothing\n // owns, so an absent org refuses the write rather than composing one.\n if (!orgId) throw new Error('[crm] a lead write needs an organization')\n return doc(firestore, 'orgs', orgId, 'leads', row?.leadId ?? id)\n })\n }, [firestore, rows])\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setCampaignIds([])\n setPending(action)\n }\n\n const runPlan = useCallback(\n (plan: CrmBulkPlan, done: (count: number) => string) =>\n apply({\n attempted: plan.writes.length,\n skipped: plan.skipped,\n job: () => runCrmBulkWrites(writers, plan.writes, (write) => write.label),\n done,\n }),\n [apply, writers],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending) return\n const action = pending\n setPending(null)\n const writes: CrmBulkWrite[] = []\n const skipped: CrmBulkSkip[] = []\n if (action === 'owner') {\n for (const lead of selectedRows) {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: { ownerUid: value ? value : deleteField(), updatedAt: serverTimestamp() },\n })\n }\n await runPlan(\n { writes, skipped },\n (count) => (value ? `Owner set on ${countNoun(count, NOUN)}` : `Owner cleared on ${countNoun(count, NOUN)}`),\n )\n return\n }\n if (action === 'campaign') {\n // Added to what each lead carries — `arrayUnion`, the way a sequence's\n // enroll and the import add one — never in place of it.\n for (const lead of selectedRows) {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: { campaignIds: arrayUnion(...campaignIds), updatedAt: serverTimestamp() },\n })\n }\n const outcome = await runPlan(\n { writes, skipped },\n (count) =>\n `Added ${countNoun(count, NOUN)} to ${campaignIds.length === 1 ? 'the campaign' : `${campaignIds.length} campaigns`}`,\n )\n // What landed is written on each lead's Activity (AGL-3274): one\n // \"Filed under\" per campaign the lead was not already in, and none\n // for a lead the store refused — the row's own document is the\n // membership, and the entry only records the act.\n const refused = new Set(outcome.refused.map((entry) => entry.label))\n const named = campaignIds.map((campaignId) => ({\n id: campaignId,\n name: campaigns.options.find((option) => option.value === campaignId)?.label ?? '',\n }))\n for (const lead of selectedRows) {\n if (refused.has(labelOf(lead))) continue\n const already = readCampaignIds(lead)\n const filed = named.filter((campaign) => !already.includes(campaign.id))\n if (filed.length) await logFiling({ leadId: lead.leadId }, { filed })\n }\n return\n }\n if (action === 'status') {\n const status = value as CrmLeadStatus\n for (const lead of selectedRows) {\n const current = crmLeadStatus(lead)\n if (lead.convertedContactId) {\n skipped.push({ label: labelOf(lead), reason: 'was converted' })\n } else if (current === status) {\n skipped.push({\n label: labelOf(lead),\n reason: `already ${CRM_LEAD_STATUS_LABELS[status]}`,\n })\n } else {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n status,\n // Reopening an unqualified lead clears its reason, as the row does.\n ...(current === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n updatedAt: serverTimestamp(),\n },\n })\n }\n }\n await runPlan(\n { writes, skipped },\n (count) => `Status set on ${countNoun(count, NOUN)}`,\n )\n return\n }\n const reason = value.trim().slice(0, UNQUALIFY_REASON_MAX)\n for (const lead of selectedRows) {\n if (lead.convertedContactId) {\n skipped.push({ label: labelOf(lead), reason: 'was converted' })\n } else if (!isCrmLeadOpen(lead)) {\n skipped.push({ label: labelOf(lead), reason: 'is already closed' })\n } else {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n status: 'unqualified' satisfies CrmLeadStatus,\n unqualifiedReason: reason,\n updatedAt: serverTimestamp(),\n },\n })\n }\n }\n await runPlan(\n { writes, skipped },\n (count) => `Marked ${countNoun(count, NOUN)} unqualified`,\n )\n }, [pending, value, campaignIds, campaigns.options, selectedRows, runPlan, logFiling])\n\n const handleExport = useCallback(() => {\n downloadTextFile('leads-selected.csv', 'text/csv', leadsCsv(selectedRows, csv))\n }, [selectedRows, csv])\n\n const canApply =\n pending === 'owner' ||\n (pending === 'campaign'\n ? campaignIds.length > 0\n : pending === 'status'\n ? Boolean(value)\n : Boolean(value.trim()))\n\n return (\n <CrmBulkBarFrame\n count={selected.length}\n noun={NOUN}\n busy={busy}\n onClear={() => onSelectedChange([])}\n report={report}\n onDismissReport={dismissReport}\n extras={\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={canApply}\n applyLabel={pending === 'unqualify' ? 'Unqualify' : pending === 'campaign' ? 'Add' : undefined}\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'owner' ? (\n <LeadOwnerSelect value={value} onChange={setValue} roster={roster} size=\"medium\" />\n ) : pending === 'status' ? (\n <TextField\n select\n size=\"small\"\n label=\"Status\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText=\"Closing a lead goes through Unqualify, which asks why.\"\n >\n {SETTABLE_STATUSES.map((status) => (\n <MenuItem key={status} value={status}>\n {CRM_LEAD_STATUS_LABELS[status]}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'unqualify' ? (\n <TextField\n label=\"Reason\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n multiline\n minRows={2}\n autoFocus\n helperText=\"One reason for every selected lead, kept on each so it can be counted later.\"\n slotProps={{ htmlInput: { maxLength: UNQUALIFY_REASON_MAX } }}\n />\n ) : pending === 'campaign' ? (\n <CampaignPicker\n options={campaigns.options}\n value={campaignIds}\n onChange={setCampaignIds}\n helperText=\"Added to the campaigns each selected lead is already in. It does not decide who a campaign mails.\"\n empty={campaigns.ready && !campaigns.options.length}\n emptyText=\"This site has no campaigns yet. Create one from Marketing to file leads under it.\"\n />\n ) : null}\n </CrmBulkValueDialog>\n }\n >\n <Button size=\"small\" disabled={busy} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('status')}>\n {'Set status'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('unqualify')}>\n {'Unqualify'}\n </Button>\n {hostId ? (\n <Button size=\"small\" disabled={busy} onClick={() => openAction('campaign')}>\n {'Add to campaign'}\n </Button>\n ) : null}\n <Button size=\"small\" disabled={busy} onClick={handleExport}>\n {'Export CSV'}\n </Button>\n {/*\n The selection's file is the rows on screen; this one is the whole\n collection, streamed by the server (AGL-2662).\n */}\n <CrmExportAllButton\n resource=\"leads\"\n orgId={orgId}\n hostId={hostId}\n disabled={busy}\n />\n </CrmBulkBarFrame>\n )\n}\nLeadsBulkBarBody.displayName = 'LeadsBulkBarBody'\n\nexport default LeadsBulkBar\n"],"names":["CRM_LEAD_STATUS_LABELS","crmLeadStatus","isCrmLeadOpen","readCampaignIds","CampaignPicker","useFirestore","useHostCampaigns","Button","MenuItem","TextField","arrayUnion","deleteField","doc","serverTimestamp","useCallback","useMemo","useState","useCampaignFilingLog","useCrmBulkApply","downloadTextFile","crmBulkWriters","runCrmBulkWrites","leadsCsv","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","LeadOwnerSelect","UNQUALIFY_REASON_MAX","NOUN","singular","plural","ACTION_TITLES","owner","status","unqualify","campaign","SETTABLE_STATUSES","labelOf","lead","String","leadId","LeadsBulkBar","props","selected","length","LeadsBulkBarBody","displayName","rows","onSelectedChange","roster","csv","orgId","hostId","firestore","busy","report","apply","dismissReport","recordKind","selectedRows","chosen","Set","filter","row","has","$id","pending","setPending","value","setValue","campaignIds","setCampaignIds","campaigns","undefined","enabled","Boolean","logFiling","org","writers","byId","Map","map","id","get","Error","openAction","action","runPlan","plan","done","attempted","writes","skipped","job","write","label","handleApply","push","kind","data","ownerUid","updatedAt","count","outcome","refused","entry","named","campaignId","name","options","find","option","already","filed","includes","current","convertedContactId","reason","unqualifiedReason","trim","slice","handleExport","canApply","noun","onClear","onDismissReport","extras","open","title","applyLabel","onClose","onApply","onChange","size","select","event","target","helperText","multiline","minRows","autoFocus","slotProps","htmlInput","maxLength","empty","ready","emptyText","disabled","onClick","resource"],"mappings":"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"}
@@ -25,6 +25,8 @@ import { useOrgMemberOptions } from "../hooks/use-org-member-options.js";
25
25
  import { useCrmOrgMount } from "../hooks/use-crm-org-mount.js";
26
26
  import { useCrmSavedView } from "../hooks/use-crm-saved-view.js";
27
27
  import { useCrmScope } from "../hooks/use-crm-scope.js";
28
+ import { useContactFieldDefinitions } from "../hooks/use-contact-field-definitions.js";
29
+ import { customFieldColumns } from "./contact-custom-columns.js";
28
30
  import { useCrmViewGrid } from "../hooks/use-crm-view-grid.js";
29
31
  import { CRM_LIST_SLOTS, CrmColumnOrderProvider } from "./crm-column-menu.js";
30
32
  import { useOrgLeads } from "../hooks/use-org-leads.js";
@@ -94,7 +96,7 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
94
96
  * its writes and its link go to. The per-site notes — which of a site's
95
97
  * forms file a lead — belong to a site's own hub and are not drawn here.
96
98
  */ export function CrmLeadsSection(props) {
97
- var _ref, _ref1, _ref2, _ref3, _ref4, _ref5;
99
+ var _ref, _converting_capturedByHostIds_, _ref1;
98
100
  const { hostId, org, basePath } = props;
99
101
  const firestore = useFirestore();
100
102
  const router = useRouter();
@@ -105,6 +107,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
105
107
  });
106
108
  const mount = useCrmOrgMount();
107
109
  const roster = useOrgMemberOptions(orgId);
110
+ // The org's lead fields, for the optional columns below (AGL-3272).
111
+ const leadFields = useContactFieldDefinitions(orgId, 'lead');
108
112
  const routes = crmRoutes(basePath != null ? basePath : '');
109
113
  // Under a site: the site's own window, rows keyed by document id.
110
114
  const site = useFirestoreCollection(()=>hostId ? query(collection(firestore, 'hosts', hostId, 'leads'), orderBy('lastSeenAtMs', 'desc'), limit(LEADS_WINDOW + 1)) : null, [
@@ -121,13 +125,17 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
121
125
  hostId,
122
126
  mount == null ? void 0 : mount.hosts
123
127
  ]);
124
- const orgLeads = useOrgLeads({
125
- hostIds: orgHostIds,
128
+ /*
129
+ * ONE LISTENER (AGL-3275), where this used to fan out across the org's
130
+ * sites and merge. At the organization level an org-wide member reads with
131
+ * no scope clause, which is what `visibleTo: null` asks for.
132
+ */ const orgLeads = useOrgLeads({
133
+ orgId,
134
+ visibleTo: null,
126
135
  windowSize: LEADS_WINDOW
127
136
  });
128
137
  const leadDocs = useMemo(()=>hostId ? site.data.map((row)=>_extends({}, row, {
129
- leadId: row.$id,
130
- hostId
138
+ leadId: row.$id
131
139
  })) : orgLeads.data, [
132
140
  hostId,
133
141
  site.data,
@@ -341,6 +349,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
341
349
  ownerUid: values.ownerUid
342
350
  } : {}, values.notes ? {
343
351
  notes: values.notes
352
+ } : {}, values.custom ? {
353
+ custom: values.custom
344
354
  } : {}, {
345
355
  status: values.status
346
356
  }));
@@ -369,7 +379,7 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
369
379
  ]);
370
380
  const writeLead = useCallback(async (lead, fields, done)=>{
371
381
  try {
372
- await updateDoc(doc(firestore, 'hosts', lead.hostId, 'leads', lead.leadId), _extends({}, fields, {
382
+ await updateDoc(doc(firestore, 'orgs', orgId, 'leads', lead.leadId), _extends({}, fields, {
373
383
  updatedAt: serverTimestamp()
374
384
  }));
375
385
  enqueueSnackbar(done, {
@@ -498,9 +508,17 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
498
508
  headerName: 'Site',
499
509
  flex: 0.9,
500
510
  minWidth: 140,
501
- valueGetter: (_value, row)=>{
502
- var _ref;
503
- return (_ref = mount == null ? void 0 : mount.siteName(row.hostId)) != null ? _ref : row.hostId;
511
+ /*
512
+ * KNOWN BY, not "the site" (AGL-3275). A lead is one org row
513
+ * and several sites in a consent group can hold the same
514
+ * person, so this names every site that captured them — the
515
+ * answer the Contacts list has always given in its own column.
516
+ */ valueGetter: (_value, row)=>{
517
+ const held = Array.isArray(row['capturedByHostIds']) ? row['capturedByHostIds'] : [];
518
+ return held.map((id)=>{
519
+ var _ref;
520
+ return (_ref = mount == null ? void 0 : mount.siteName(id)) != null ? _ref : id;
521
+ }).join(', ');
504
522
  }
505
523
  }
506
524
  ],
@@ -542,6 +560,11 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
542
560
  return leadTimeLabel((_row_lastSeenAtMs = row['lastSeenAtMs']) != null ? _row_lastSeenAtMs : row['createdAt']);
543
561
  }
544
562
  },
563
+ // The org's lead fields as optional columns (AGL-3272), read off the
564
+ // row's own `custom` map the way the contacts list reads its own.
565
+ // Ahead of the row menu, so the overflow stays at the right edge
566
+ // however many fields the org has defined.
567
+ ...customFieldColumns(leadFields.active),
545
568
  {
546
569
  field: 'actions',
547
570
  headerName: '',
@@ -576,7 +599,7 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
576
599
  path: mdiAccountArrowRight.path,
577
600
  size: 0.8
578
601
  }),
579
- href: routes.lead(row.leadId, hostId ? null : row.hostId)
602
+ href: routes.lead(row.leadId)
580
603
  },
581
604
  {
582
605
  key: 'convert',
@@ -620,7 +643,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
620
643
  writeLead,
621
644
  hostId,
622
645
  mount,
623
- campaignName
646
+ campaignName,
647
+ leadFields.active
624
648
  ]);
625
649
  /* The column and sort models are the view's (AGL-2617). */ const grid = useCrmViewGrid(views, columns);
626
650
  return /*#__PURE__*/ _jsxs(_Fragment, {
@@ -785,7 +809,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
785
809
  roster: roster,
786
810
  csv: csvOptions,
787
811
  orgId: orgId,
788
- hostId: hostId
812
+ hostId: hostId,
813
+ org: org
789
814
  }),
790
815
  /*#__PURE__*/ _jsx(CrmColumnOrderProvider, {
791
816
  value: grid.columnOrder,
@@ -798,7 +823,7 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
798
823
  onChange: setSelectedIds
799
824
  },
800
825
  loading: status === 'loading',
801
- onOpen: (_id, row)=>router.push(routes.lead(row.leadId, hostId ? null : row.hostId)),
826
+ onOpen: (_id, row)=>router.push(routes.lead(row.leadId)),
802
827
  // Columns and sort are the view's, controlled (AGL-2617).
803
828
  columnVisibilityModel: grid.columnVisibilityModel,
804
829
  onColumnVisibilityModelChange: grid.onColumnVisibilityModelChange,
@@ -836,6 +861,7 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
836
861
  busy: createBusy,
837
862
  error: createError,
838
863
  roster: roster,
864
+ orgId: orgId,
839
865
  onSubmit: (values)=>void handleCreate(values)
840
866
  }),
841
867
  /*#__PURE__*/ _jsx(AssignOwnerDialog, {
@@ -853,17 +879,17 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
853
879
  /*#__PURE__*/ _jsx(LeadUnqualifyDialog, {
854
880
  open: Boolean(unqualifying),
855
881
  onClose: ()=>setUnqualifying(null),
856
- hostId: (_ref = (_ref1 = unqualifying == null ? void 0 : unqualifying.hostId) != null ? _ref1 : hostId) != null ? _ref : '',
857
- leadId: (_ref2 = unqualifying == null ? void 0 : unqualifying.leadId) != null ? _ref2 : '',
882
+ hostId: hostId != null ? hostId : '',
883
+ leadId: (_ref = unqualifying == null ? void 0 : unqualifying.leadId) != null ? _ref : '',
858
884
  leadLabel: String((unqualifying == null ? void 0 : unqualifying['name']) || (unqualifying == null ? void 0 : unqualifying['email']) || '')
859
885
  }),
860
886
  /*#__PURE__*/ _jsx(LeadConvertDialog, {
861
887
  open: Boolean(converting),
862
888
  onClose: ()=>setConverting(null),
863
- hostId: (_ref3 = (_ref4 = converting == null ? void 0 : converting.hostId) != null ? _ref4 : hostId) != null ? _ref3 : '',
889
+ hostId: hostId != null ? hostId : Array.isArray(converting == null ? void 0 : converting['capturedByHostIds']) ? String((_converting_capturedByHostIds_ = converting['capturedByHostIds'][0]) != null ? _converting_capturedByHostIds_ : '') : '',
864
890
  orgId: orgId,
865
891
  org: org,
866
- leadId: (_ref5 = converting == null ? void 0 : converting.leadId) != null ? _ref5 : '',
892
+ leadId: (_ref1 = converting == null ? void 0 : converting.leadId) != null ? _ref1 : '',
867
893
  lead: converting != null ? converting : {},
868
894
  basePath: basePath != null ? basePath : '',
869
895
  roster: roster