@aglyn/plugins-crm 1.0.0-beta.162 → 1.0.0-beta.163
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/leads-bulk-bar.js +51 -5
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +93 -4
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-lead-drawer.d.ts +2 -0
- package/src/lib/components/new-lead-drawer.js +27 -1
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/declarations.server.js +15 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/crm-lead-import.d.ts +11 -2
- package/src/lib/model/crm-lead-import.js +22 -2
- package/src/lib/model/crm-lead-import.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +8 -0
- package/src/lib/model/lead-filters.js +11 -1
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/server/lead-campaign-carry.d.ts +50 -0
- package/src/lib/server/lead-campaign-carry.js +75 -0
- package/src/lib/server/lead-campaign-carry.js.map +1 -0
- package/src/lib/server/lead-create.d.ts +13 -0
- package/src/lib/server/lead-create.js +30 -0
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/leads-import.js +50 -5
- package/src/lib/server/leads-import.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-crm",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.163",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
37
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.163",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.163",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.163",
|
|
31
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.163",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.163",
|
|
33
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.163",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.163",
|
|
35
|
+
"@aglyn/shared-util-email": "1.0.0-beta.163",
|
|
36
|
+
"@aglyn/shared-util-http": "1.0.0-beta.163",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.163",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.163",
|
|
39
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.163",
|
|
40
40
|
"@dnd-kit/core": "^6.3.1",
|
|
41
41
|
"@dnd-kit/utilities": "^3.2.2",
|
|
42
42
|
"@swc/helpers": "0.5.23",
|
|
@@ -41,9 +41,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
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
43
|
*/ import { CRM_LEAD_STATUS_LABELS, crmLeadStatus, isCrmLeadOpen } from "@aglyn/aglyn";
|
|
44
|
-
import
|
|
44
|
+
import CampaignPicker from "@aglyn/shared-ui-email-campaigns/components/campaign-picker.component";
|
|
45
|
+
import { useFirestore, useHostCampaigns } from "@aglyn/tenant-feature-instance";
|
|
45
46
|
import { Button, MenuItem, TextField } from "@mui/material";
|
|
46
|
-
import { deleteField, doc, serverTimestamp } from "firebase/firestore";
|
|
47
|
+
import { arrayUnion, deleteField, doc, serverTimestamp } from "firebase/firestore";
|
|
47
48
|
import { useCallback, useMemo, useState } from "react";
|
|
48
49
|
import { useCrmBulkApply } from "../hooks/use-crm-bulk-apply.js";
|
|
49
50
|
import { downloadTextFile } from "../model/contacts-csv.js";
|
|
@@ -60,7 +61,8 @@ const NOUN = {
|
|
|
60
61
|
const ACTION_TITLES = {
|
|
61
62
|
owner: 'Set the owner',
|
|
62
63
|
status: 'Set the status',
|
|
63
|
-
unqualify: 'Unqualify'
|
|
64
|
+
unqualify: 'Unqualify',
|
|
65
|
+
campaign: 'Add to campaign'
|
|
64
66
|
};
|
|
65
67
|
/** The statuses the bar can set — the open ones; closing goes through Unqualify. */ const SETTABLE_STATUSES = [
|
|
66
68
|
'new',
|
|
@@ -90,6 +92,15 @@ function LeadsBulkBarBody(props) {
|
|
|
90
92
|
]);
|
|
91
93
|
const [pending, setPending] = useState(null);
|
|
92
94
|
const [value, setValue] = useState('');
|
|
95
|
+
/*
|
|
96
|
+
* The campaigns to add the selection to (AGL-3254), and the site's
|
|
97
|
+
* containers the picker offers — read only while that dialog is open.
|
|
98
|
+
* Under a site alone: a campaign belongs to one site, and at the
|
|
99
|
+
* organization level a selection spans them.
|
|
100
|
+
*/ const [campaignIds, setCampaignIds] = useState([]);
|
|
101
|
+
const campaigns = useHostCampaigns(hostId != null ? hostId : undefined, {
|
|
102
|
+
enabled: pending === 'campaign' && Boolean(hostId)
|
|
103
|
+
});
|
|
93
104
|
// The reference a write names is the row's own site and document.
|
|
94
105
|
const writers = useMemo(()=>{
|
|
95
106
|
const byId = new Map(rows.map((row)=>[
|
|
@@ -107,6 +118,7 @@ function LeadsBulkBarBody(props) {
|
|
|
107
118
|
]);
|
|
108
119
|
const openAction = (action)=>{
|
|
109
120
|
setValue('');
|
|
121
|
+
setCampaignIds([]);
|
|
110
122
|
setPending(action);
|
|
111
123
|
};
|
|
112
124
|
const runPlan = useCallback((plan, done)=>apply({
|
|
@@ -142,6 +154,26 @@ function LeadsBulkBarBody(props) {
|
|
|
142
154
|
}, (count)=>value ? `Owner set on ${countNoun(count, NOUN)}` : `Owner cleared on ${countNoun(count, NOUN)}`);
|
|
143
155
|
return;
|
|
144
156
|
}
|
|
157
|
+
if (action === 'campaign') {
|
|
158
|
+
// Added to what each lead carries — `arrayUnion`, the way a sequence's
|
|
159
|
+
// enroll and the import add one — never in place of it.
|
|
160
|
+
for (const lead of selectedRows){
|
|
161
|
+
writes.push({
|
|
162
|
+
id: lead.$id,
|
|
163
|
+
label: labelOf(lead),
|
|
164
|
+
kind: 'update',
|
|
165
|
+
data: {
|
|
166
|
+
campaignIds: arrayUnion(...campaignIds),
|
|
167
|
+
updatedAt: serverTimestamp()
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
await runPlan({
|
|
172
|
+
writes,
|
|
173
|
+
skipped
|
|
174
|
+
}, (count)=>`Added ${countNoun(count, NOUN)} to ${campaignIds.length === 1 ? 'the campaign' : `${campaignIds.length} campaigns`}`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
145
177
|
if (action === 'status') {
|
|
146
178
|
const status = value;
|
|
147
179
|
for (const lead of selectedRows){
|
|
@@ -209,6 +241,7 @@ function LeadsBulkBarBody(props) {
|
|
|
209
241
|
}, [
|
|
210
242
|
pending,
|
|
211
243
|
value,
|
|
244
|
+
campaignIds,
|
|
212
245
|
selectedRows,
|
|
213
246
|
runPlan
|
|
214
247
|
]);
|
|
@@ -218,7 +251,7 @@ function LeadsBulkBarBody(props) {
|
|
|
218
251
|
selectedRows,
|
|
219
252
|
csv
|
|
220
253
|
]);
|
|
221
|
-
const canApply = pending === 'owner' || (pending === 'status' ? Boolean(value) : Boolean(value.trim()));
|
|
254
|
+
const canApply = pending === 'owner' || (pending === 'campaign' ? campaignIds.length > 0 : pending === 'status' ? Boolean(value) : Boolean(value.trim()));
|
|
222
255
|
return /*#__PURE__*/ _jsxs(CrmBulkBarFrame, {
|
|
223
256
|
count: selected.length,
|
|
224
257
|
noun: NOUN,
|
|
@@ -233,7 +266,7 @@ function LeadsBulkBarBody(props) {
|
|
|
233
266
|
noun: NOUN,
|
|
234
267
|
busy: busy,
|
|
235
268
|
canApply: canApply,
|
|
236
|
-
applyLabel: pending === 'unqualify' ? 'Unqualify' : undefined,
|
|
269
|
+
applyLabel: pending === 'unqualify' ? 'Unqualify' : pending === 'campaign' ? 'Add' : undefined,
|
|
237
270
|
onClose: ()=>setPending(null),
|
|
238
271
|
onApply: ()=>void handleApply(),
|
|
239
272
|
children: pending === 'owner' ? /*#__PURE__*/ _jsx(LeadOwnerSelect, {
|
|
@@ -265,6 +298,13 @@ function LeadsBulkBarBody(props) {
|
|
|
265
298
|
maxLength: UNQUALIFY_REASON_MAX
|
|
266
299
|
}
|
|
267
300
|
}
|
|
301
|
+
}) : pending === 'campaign' ? /*#__PURE__*/ _jsx(CampaignPicker, {
|
|
302
|
+
options: campaigns.options,
|
|
303
|
+
value: campaignIds,
|
|
304
|
+
onChange: setCampaignIds,
|
|
305
|
+
helperText: "Added to the campaigns each selected lead is already in. It does not decide who a campaign mails.",
|
|
306
|
+
empty: campaigns.ready && !campaigns.options.length,
|
|
307
|
+
emptyText: "This site has no campaigns yet. Create one from Marketing to file leads under it."
|
|
268
308
|
}) : null
|
|
269
309
|
}),
|
|
270
310
|
children: [
|
|
@@ -286,6 +326,12 @@ function LeadsBulkBarBody(props) {
|
|
|
286
326
|
onClick: ()=>openAction('unqualify'),
|
|
287
327
|
children: 'Unqualify'
|
|
288
328
|
}),
|
|
329
|
+
hostId ? /*#__PURE__*/ _jsx(Button, {
|
|
330
|
+
size: "small",
|
|
331
|
+
disabled: busy,
|
|
332
|
+
onClick: ()=>openAction('campaign'),
|
|
333
|
+
children: 'Add to campaign'
|
|
334
|
+
}) : null,
|
|
289
335
|
/*#__PURE__*/ _jsx(Button, {
|
|
290
336
|
size: "small",
|
|
291
337
|
disabled: busy,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/leads-bulk-bar.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * The bar over the Leads table, for whatever rows are ticked (AGL-2662).\n *\n * Hand them to an owner, set their status, unqualify them with one reason,\n * or take them into a spreadsheet. Every act here is a document write and\n * nothing more — a lead's status and owner are the team's own notes on a\n * capture, with no event and no notification behind them, which is why the\n * row's inline select writes them client-direct — so the whole bar goes\n * through the shared batched runner under the same rules.\n *\n * ## A row names its own site\n *\n * A lead lives at `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 { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { Button, MenuItem, TextField } from '@mui/material'\nimport { 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'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n owner: 'Set the owner',\n status: 'Set the status',\n unqualify: 'Unqualify',\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 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 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 === '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, 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' || (pending === 'status' ? Boolean(value) : 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' : 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 ) : null}\n </CrmBulkValueDialog>\n }\n >\n <Button size=\"small\" disabled={busy} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('status')}>\n {'Set status'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('unqualify')}>\n {'Unqualify'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={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","useFirestore","Button","MenuItem","TextField","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","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","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","Boolean","noun","onClear","onDismissReport","extras","open","title","applyLabel","undefined","onClose","onApply","onChange","size","select","event","target","helperText","multiline","minRows","autoFocus","slotProps","htmlInput","maxLength","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 `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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"}
|
|
@@ -33,14 +33,14 @@ import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.com
|
|
|
33
33
|
import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
34
34
|
import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.component";
|
|
35
35
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
36
|
-
import { useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
|
|
36
|
+
import { useFirestore, useFirestoreCollection, useHostCampaigns } from "@aglyn/tenant-feature-instance";
|
|
37
37
|
import { Alert, Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, InputAdornment, InputLabel, MenuItem, Select, Stack, TextField, Typography } from "@mui/material";
|
|
38
38
|
import { collection, deleteField, doc, limit, orderBy, query, serverTimestamp, updateDoc } from "firebase/firestore";
|
|
39
39
|
import { useRouter } from "next/navigation";
|
|
40
40
|
import { useCallback, useEffect, useId, useMemo, useState } from "react";
|
|
41
41
|
import { downloadTextFile } from "../model/contacts-csv.js";
|
|
42
42
|
import { crmRoutes } from "../model/crm-routes.js";
|
|
43
|
-
import { LEAD_FILTER_LABELS, LEAD_EMAIL_FILTER_LABELS, LEAD_EMAIL_FILTERS, LEAD_FILTERS, leadMatchesEmailFilter, leadMatchesFilter, leadMatchesSearch } from "../model/lead-filters.js";
|
|
43
|
+
import { LEAD_FILTER_LABELS, LEAD_EMAIL_FILTER_LABELS, LEAD_EMAIL_FILTERS, LEAD_FILTERS, leadMatchesCampaignFilter, leadMatchesEmailFilter, leadMatchesFilter, leadMatchesSearch } from "../model/lead-filters.js";
|
|
44
44
|
import { leadsCsv } from "../model/leads-csv.js";
|
|
45
45
|
import { LeadConvertDialog } from "./lead-convert-dialog.js";
|
|
46
46
|
import { leadSourceLabel, leadSources, leadTimeLabel } from "./lead-history-card.js";
|
|
@@ -193,10 +193,47 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
193
193
|
views.setFilters,
|
|
194
194
|
views.state.filters
|
|
195
195
|
]);
|
|
196
|
+
/*
|
|
197
|
+
* The `Campaign` filter (AGL-3254) is the view's too, as a `campaignIds`
|
|
198
|
+
* clause: the id of one of the site's campaign containers, resolved to
|
|
199
|
+
* its name from the containers themselves — ids only in storage, so a
|
|
200
|
+
* renamed campaign keeps its leads. Under a site alone: a campaign
|
|
201
|
+
* belongs to one site, and the organization-level list spans them all.
|
|
202
|
+
*/ const campaignFilter = useMemo(()=>{
|
|
203
|
+
var _ref;
|
|
204
|
+
var _views_state_filters_find;
|
|
205
|
+
return (_ref = (_views_state_filters_find = views.state.filters.find((clause)=>clause.field === 'campaignIds')) == null ? void 0 : _views_state_filters_find.value) != null ? _ref : '';
|
|
206
|
+
}, [
|
|
207
|
+
views.state.filters
|
|
208
|
+
]);
|
|
209
|
+
const setCampaignFilter = useCallback((next)=>views.setFilters([
|
|
210
|
+
...views.state.filters.filter((clause)=>clause.field !== 'campaignIds'),
|
|
211
|
+
...next ? [
|
|
212
|
+
{
|
|
213
|
+
field: 'campaignIds',
|
|
214
|
+
op: 'contains',
|
|
215
|
+
value: next
|
|
216
|
+
}
|
|
217
|
+
] : []
|
|
218
|
+
]), [
|
|
219
|
+
views.setFilters,
|
|
220
|
+
views.state.filters
|
|
221
|
+
]);
|
|
222
|
+
const campaigns = useHostCampaigns(hostId, {
|
|
223
|
+
enabled: Boolean(hostId)
|
|
224
|
+
});
|
|
225
|
+
const campaignName = useCallback((id)=>{
|
|
226
|
+
var _ref;
|
|
227
|
+
var _campaigns_options_find;
|
|
228
|
+
return (_ref = (_campaigns_options_find = campaigns.options.find((option)=>option.value === id)) == null ? void 0 : _campaigns_options_find.label) != null ? _ref : id;
|
|
229
|
+
}, [
|
|
230
|
+
campaigns.options
|
|
231
|
+
]);
|
|
196
232
|
// The label's id, so the filter's combobox is named "Show" rather than
|
|
197
233
|
// after the option it shows — see `LeadOwnerSelect`.
|
|
198
234
|
const filterLabelId = useId();
|
|
199
235
|
const emailFilterLabelId = useId();
|
|
236
|
+
const campaignFilterLabelId = useId();
|
|
200
237
|
/*
|
|
201
238
|
* The search box is the SECTION'S, not the grid's (AGL-3246). The grid's
|
|
202
239
|
* quick filter runs over the rows the grid holds, and the grid holds one
|
|
@@ -206,10 +243,11 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
206
243
|
* before the footer's count and the page slice, over the fields a person
|
|
207
244
|
* types to find a lead: name, email, company, title and tags.
|
|
208
245
|
*/ const [search, setSearch] = useState('');
|
|
209
|
-
const rows = useMemo(()=>window.filter((lead)=>leadMatchesFilter(lead, filter) && leadMatchesEmailFilter(lead, emailFilter) && leadMatchesSearch(lead, search)), [
|
|
246
|
+
const rows = useMemo(()=>window.filter((lead)=>leadMatchesFilter(lead, filter) && leadMatchesEmailFilter(lead, emailFilter) && leadMatchesCampaignFilter(lead, campaignFilter) && leadMatchesSearch(lead, search)), [
|
|
210
247
|
window,
|
|
211
248
|
filter,
|
|
212
249
|
emailFilter,
|
|
250
|
+
campaignFilter,
|
|
213
251
|
search
|
|
214
252
|
]);
|
|
215
253
|
const [page, setPage] = useState(0);
|
|
@@ -222,6 +260,7 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
222
260
|
}, [
|
|
223
261
|
filter,
|
|
224
262
|
emailFilter,
|
|
263
|
+
campaignFilter,
|
|
225
264
|
search
|
|
226
265
|
]);
|
|
227
266
|
const pageRows = useMemo(()=>rows.slice(page * pageSize, (page + 1) * pageSize), [
|
|
@@ -238,6 +277,7 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
238
277
|
useEffect(()=>setSelectedIds([]), [
|
|
239
278
|
filter,
|
|
240
279
|
emailFilter,
|
|
280
|
+
campaignFilter,
|
|
241
281
|
search
|
|
242
282
|
]);
|
|
243
283
|
// How the file names the owner and, at the org level, the site.
|
|
@@ -295,6 +335,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
295
335
|
address: values.address
|
|
296
336
|
} : {}, values.tags.length ? {
|
|
297
337
|
tags: values.tags
|
|
338
|
+
} : {}, values.campaignIds.length ? {
|
|
339
|
+
campaignIds: values.campaignIds
|
|
298
340
|
} : {}, values.ownerUid ? {
|
|
299
341
|
ownerUid: values.ownerUid
|
|
300
342
|
} : {}, values.notes ? {
|
|
@@ -479,6 +521,17 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
479
521
|
return ((_row_tags = row.tags) != null ? _row_tags : []).join(', ');
|
|
480
522
|
}
|
|
481
523
|
},
|
|
524
|
+
// The campaigns the lead is filed under (AGL-3254), by name — the
|
|
525
|
+
// ids are the storage. Under a site only, like the filter.
|
|
526
|
+
...hostId ? [
|
|
527
|
+
{
|
|
528
|
+
field: 'campaignIds',
|
|
529
|
+
headerName: 'Campaign',
|
|
530
|
+
flex: 1,
|
|
531
|
+
minWidth: 150,
|
|
532
|
+
valueGetter: (_value, row)=>Aglyn.readCampaignIds(row).map(campaignName).join(', ')
|
|
533
|
+
}
|
|
534
|
+
] : [],
|
|
482
535
|
{
|
|
483
536
|
field: 'lastSeenAtMs',
|
|
484
537
|
headerName: 'Last seen',
|
|
@@ -566,7 +619,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
566
619
|
routes,
|
|
567
620
|
writeLead,
|
|
568
621
|
hostId,
|
|
569
|
-
mount
|
|
622
|
+
mount,
|
|
623
|
+
campaignName
|
|
570
624
|
]);
|
|
571
625
|
/* The column and sort models are the view's (AGL-2617). */ const grid = useCrmViewGrid(views, columns);
|
|
572
626
|
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
@@ -631,6 +685,41 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
631
685
|
})
|
|
632
686
|
]
|
|
633
687
|
}),
|
|
688
|
+
hostId ? /*#__PURE__*/ _jsxs(FormControl, {
|
|
689
|
+
size: "small",
|
|
690
|
+
sx: {
|
|
691
|
+
minWidth: 180
|
|
692
|
+
},
|
|
693
|
+
children: [
|
|
694
|
+
/*#__PURE__*/ _jsx(InputLabel, {
|
|
695
|
+
id: campaignFilterLabelId,
|
|
696
|
+
shrink: true,
|
|
697
|
+
children: 'Campaign'
|
|
698
|
+
}),
|
|
699
|
+
/*#__PURE__*/ _jsxs(Select, {
|
|
700
|
+
labelId: campaignFilterLabelId,
|
|
701
|
+
label: "Campaign",
|
|
702
|
+
notched: true,
|
|
703
|
+
value: campaignFilter,
|
|
704
|
+
onChange: (event)=>setCampaignFilter(String(event.target.value)),
|
|
705
|
+
displayEmpty: true,
|
|
706
|
+
children: [
|
|
707
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
708
|
+
value: "",
|
|
709
|
+
children: 'Any campaign'
|
|
710
|
+
}),
|
|
711
|
+
campaigns.options.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
712
|
+
value: option.value,
|
|
713
|
+
children: option.label
|
|
714
|
+
}, option.value)),
|
|
715
|
+
campaignFilter && !campaigns.options.some((option)=>option.value === campaignFilter) ? /*#__PURE__*/ _jsx(MenuItem, {
|
|
716
|
+
value: campaignFilter,
|
|
717
|
+
children: campaignFilter
|
|
718
|
+
}) : null
|
|
719
|
+
]
|
|
720
|
+
})
|
|
721
|
+
]
|
|
722
|
+
}) : null,
|
|
634
723
|
/*#__PURE__*/ _jsx(TextField, {
|
|
635
724
|
size: "small",
|
|
636
725
|
value: search,
|