@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.
- package/package.json +13 -13
- package/src/lib/components/contact-associations-card.js +21 -5
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-field-drawer.js +19 -2
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/components/crm-console-page.js +12 -7
- package/src/lib/components/crm-console-page.js.map +1 -1
- package/src/lib/components/crm-glance-card.js +4 -2
- package/src/lib/components/crm-glance-card.js.map +1 -1
- package/src/lib/components/fields-section.d.ts +2 -1
- package/src/lib/components/fields-section.js +6 -3
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.d.ts +59 -0
- package/src/lib/components/lead-campaigns-card.js +166 -0
- package/src/lib/components/lead-campaigns-card.js.map +1 -0
- package/src/lib/components/lead-detail-page.js +54 -4
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-history-card.js +5 -0
- package/src/lib/components/lead-history-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.d.ts +12 -0
- package/src/lib/components/lead-properties-card.js +64 -6
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.js +7 -1
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.d.ts +7 -2
- package/src/lib/components/leads-bulk-bar.js +42 -6
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +43 -17
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-lead-drawer.d.ts +14 -1
- package/src/lib/components/new-lead-drawer.js +41 -3
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/components/reports/lead-counts.d.ts +16 -9
- package/src/lib/components/reports/lead-counts.js +29 -18
- package/src/lib/components/reports/lead-counts.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +44 -40
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/hooks/use-campaign-filing-log.d.ts +29 -0
- package/src/lib/hooks/use-campaign-filing-log.js +91 -0
- package/src/lib/hooks/use-campaign-filing-log.js.map +1 -0
- package/src/lib/hooks/use-org-leads.d.ts +29 -32
- package/src/lib/hooks/use-org-leads.js +58 -68
- package/src/lib/hooks/use-org-leads.js.map +1 -1
- package/src/lib/model/campaign-filing-activity.d.ts +82 -0
- package/src/lib/model/campaign-filing-activity.js +88 -0
- package/src/lib/model/campaign-filing-activity.js.map +1 -0
- package/src/lib/model/crm-routes.d.ts +9 -6
- package/src/lib/model/crm-routes.js +9 -6
- package/src/lib/model/crm-routes.js.map +1 -1
- package/src/lib/server/campaign-filing-activity.d.ts +79 -0
- package/src/lib/server/campaign-filing-activity.js +93 -0
- package/src/lib/server/campaign-filing-activity.js.map +1 -0
- package/src/lib/server/capture-contact.js +11 -5
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/convert-open-lead.js +6 -2
- package/src/lib/server/convert-open-lead.js.map +1 -1
- package/src/lib/server/email-send.js +3 -3
- package/src/lib/server/email-send.js.map +1 -1
- package/src/lib/server/erase-person.js +25 -7
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/lead-campaign-carry.js +28 -2
- package/src/lib/server/lead-campaign-carry.js.map +1 -1
- package/src/lib/server/lead-create.d.ts +15 -4
- package/src/lib/server/lead-create.js +75 -15
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/leads-import.js +2 -2
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server/record-email-state.js +10 -0
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-facts.js +3 -3
- 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 `
|
|
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
|
|
117
|
+
var _ref;
|
|
112
118
|
const row = byId.get(id);
|
|
113
|
-
|
|
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,
|
|
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
|
-
|
|
125
|
-
|
|
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, '
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
|
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
|
|
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:
|
|
857
|
-
leadId: (
|
|
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:
|
|
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: (
|
|
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
|