@aglyn/plugins-crm 1.0.0-beta.169 → 1.0.0-beta.171
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/lead-campaigns-card.d.ts +7 -1
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-convert-dialog.d.ts +8 -1
- package/src/lib/components/lead-convert-dialog.js +4 -2
- package/src/lib/components/lead-convert-dialog.js.map +1 -1
- package/src/lib/components/lead-detail-page.d.ts +18 -4
- package/src/lib/components/lead-detail-page.js +69 -29
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-history-card.d.ts +8 -1
- package/src/lib/components/lead-history-card.js +10 -2
- package/src/lib/components/lead-history-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.d.ts +16 -2
- package/src/lib/components/lead-properties-card.js +2 -2
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.d.ts +6 -1
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-section.js +3 -3
- package/src/lib/components/leads-section.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.171",
|
|
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.171",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.171",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.171",
|
|
31
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.171",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.171",
|
|
33
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.171",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.171",
|
|
35
|
+
"@aglyn/shared-util-email": "1.0.0-beta.171",
|
|
36
|
+
"@aglyn/shared-util-http": "1.0.0-beta.171",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.171",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.171",
|
|
39
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.171",
|
|
40
40
|
"@dnd-kit/core": "^6.3.1",
|
|
41
41
|
"@dnd-kit/utilities": "^3.2.2",
|
|
42
42
|
"@swc/helpers": "0.5.23",
|
|
@@ -13,7 +13,13 @@ export declare const LEAD_CAMPAIGNS_HELPER_TEXT = "Your own filing. It never add
|
|
|
13
13
|
*/
|
|
14
14
|
export declare function leadCampaignNames(lead: Record<string, unknown> | null | undefined, options: readonly CampaignPickerOption[]): string[];
|
|
15
15
|
export interface LeadCampaignsCardProps {
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The lead's own site, or `null` at the organization level for a lead no
|
|
18
|
+
* site captured (AGL-3278). Read for the org root alone — the containers
|
|
19
|
+
* themselves are handed down as `options` — so the card serves both
|
|
20
|
+
* levels; the org-level mount answers the same org without a site.
|
|
21
|
+
*/
|
|
22
|
+
hostId: string | null;
|
|
17
23
|
leadId: string;
|
|
18
24
|
lead: Record<string, unknown>;
|
|
19
25
|
leadStatus: FirestoreDocStatus;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-campaigns-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n campaignMembershipUnchanged,\n campaignMembershipValue,\n pluginDocsHelp,\n readCampaignIds,\n} from '@aglyn/aglyn'\nimport CampaignPicker, {\n type CampaignPickerOption,\n} from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack } from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\n\n/** The helper line under the picker — the contact card's, word for word. */\nexport const LEAD_CAMPAIGNS_HELPER_TEXT =\n 'Your own filing. It never adds anyone to a send — a campaign mails its lists.'\n\n/**\n * The campaigns a lead is filed under, as NAMES for the record header.\n *\n * Resolved from the site's containers; an id with no live container — a\n * campaign the console deleted, or one still loading — is left out rather\n * than shown, because a document id on a chip tells a reader nothing and\n * looks like a fault. The picker below keeps such an id visible as itself,\n * so the filing is never silently lost; the header only ever names.\n */\nexport function leadCampaignNames(\n lead: Record<string, unknown> | null | undefined,\n options: readonly CampaignPickerOption[],\n): string[] {\n const labels = new Map(options.map((option) => [option.value, option.label]))\n return readCampaignIds(lead)\n .map((id) => labels.get(id))\n .filter((name): name is string => Boolean(name))\n}\n\nexport interface LeadCampaignsCardProps {\n hostId: string\n leadId: string\n lead: Record<string, unknown>\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n /** The site's campaigns, read once by the page for this card and the header. */\n options: readonly CampaignPickerOption[]\n /** The campaign read has answered — false while it settles. */\n optionsReady: boolean\n /**\n * What a saved filing changed, once it has landed: the ids added to and\n * removed from the lead's membership. The page files the activity\n * entries (AGL-3274); the card only knows the document.\n */\n onFiled?: (change: { added: string[]; removed: string[] }) => void\n}\n\n/**\n * WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).\n *\n * The contact's Relationship card has carried this picker since AGL-2596;\n * the lead's page had none, so a lead filed under a campaign by the New\n * lead drawer, the bulk bar or a sequence's enroll showed nothing of it —\n * the one field on the record the reader had no way to see or change.\n *\n * The same control, the same value helper, the same helper line. The save\n * is the lead's own client door — a one-field `update` on\n * `hosts/{hostId}/leads`, as the properties card writes status and notes —\n * guarded by the seed like every draft edited over a cached read, and\n * `campaignMembershipUnchanged` keeps Save quiet while the picker only\n * reorders what is stored.\n *\n * It is FILING, not audience: the helper says so, in the sentence the\n * contact card uses, because a reader who finds a campaign picker on a\n * person's page assumes the opposite.\n */\nexport function LeadCampaignsCard(props: LeadCampaignsCardProps) {\n const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const stored = readCampaignIds(lead)\n const [selected, setSelected] = useState<string[]>(stored)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n // A newer membership from the server replaces an UNEDITED selection — a\n // sequence's enroll or a bulk add lands while the page is open — and an\n // edited one is the reader's, decided by the guard on save.\n const storedKey = stored.join(',')\n useEffect(() => {\n if (!dirty) setSelected(stored)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storedKey, dirty])\n\n const unchanged = campaignMembershipUnchanged(stored, selected)\n const empty = optionsReady && !options.length\n\n const save = async () => {\n if (unchanged) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setSaving(true)\n const next = campaignMembershipValue(selected)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n campaignIds: next,\n updatedAt: serverTimestamp(),\n })\n },\n )\n setSaving(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The filing could not be saved.', { variant: 'warning' })\n return\n }\n setDirty(false)\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n onFiled?.({\n added: next.filter((id) => !stored.includes(id)),\n removed: stored.filter((id) => !next.includes(id)),\n })\n }\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <CampaignPicker\n options={options}\n value={selected}\n onChange={(next) => {\n setSelected(next)\n setDirty(true)\n }}\n label=\"Filed under campaigns\"\n helperText={LEAD_CAMPAIGNS_HELPER_TEXT}\n disabled={saving}\n empty={empty}\n emptyText=\"This site has no campaigns yet. Create one from Marketing to file leads under it.\"\n />\n {empty ? null : (\n <Stack direction=\"row\">\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || unchanged}\n onClick={() => void save().catch((error: unknown) => {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n setSaving(false)\n })}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n </Stack>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nLeadCampaignsCard.displayName = 'LeadCampaignsCard'\n\nexport default LeadCampaignsCard\n"],"names":["campaignMembershipUnchanged","campaignMembershipValue","pluginDocsHelp","readCampaignIds","CampaignPicker","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Stack","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","LEAD_CAMPAIGNS_HELPER_TEXT","leadCampaignNames","lead","options","labels","Map","map","option","value","label","id","get","filter","name","Boolean","LeadCampaignsCard","props","hostId","leadId","leadStatus","fromCache","optionsReady","onFiled","orgId","firestore","enqueueSnackbar","stored","selected","setSelected","dirty","setDirty","saving","setSaving","storedKey","join","unchanged","empty","length","save","variant","persist","next","verdict","subject","unreadable","campaignIds","updatedAt","ok","message","added","includes","removed","header","help","anchor","contentGutterX","contentGutterY","spacing","onChange","helperText","disabled","emptyText","direction","size","onClick","catch","error","console","Error","allowDuplicate","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-campaigns-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n campaignMembershipUnchanged,\n campaignMembershipValue,\n pluginDocsHelp,\n readCampaignIds,\n} from '@aglyn/aglyn'\nimport CampaignPicker, {\n type CampaignPickerOption,\n} from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack } from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\n\n/** The helper line under the picker — the contact card's, word for word. */\nexport const LEAD_CAMPAIGNS_HELPER_TEXT =\n 'Your own filing. It never adds anyone to a send — a campaign mails its lists.'\n\n/**\n * The campaigns a lead is filed under, as NAMES for the record header.\n *\n * Resolved from the site's containers; an id with no live container — a\n * campaign the console deleted, or one still loading — is left out rather\n * than shown, because a document id on a chip tells a reader nothing and\n * looks like a fault. The picker below keeps such an id visible as itself,\n * so the filing is never silently lost; the header only ever names.\n */\nexport function leadCampaignNames(\n lead: Record<string, unknown> | null | undefined,\n options: readonly CampaignPickerOption[],\n): string[] {\n const labels = new Map(options.map((option) => [option.value, option.label]))\n return readCampaignIds(lead)\n .map((id) => labels.get(id))\n .filter((name): name is string => Boolean(name))\n}\n\nexport interface LeadCampaignsCardProps {\n /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Read for the org root alone — the containers\n * themselves are handed down as `options` — so the card serves both\n * levels; the org-level mount answers the same org without a site.\n */\n hostId: string | null\n leadId: string\n lead: Record<string, unknown>\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n /** The site's campaigns, read once by the page for this card and the header. */\n options: readonly CampaignPickerOption[]\n /** The campaign read has answered — false while it settles. */\n optionsReady: boolean\n /**\n * What a saved filing changed, once it has landed: the ids added to and\n * removed from the lead's membership. The page files the activity\n * entries (AGL-3274); the card only knows the document.\n */\n onFiled?: (change: { added: string[]; removed: string[] }) => void\n}\n\n/**\n * WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).\n *\n * The contact's Relationship card has carried this picker since AGL-2596;\n * the lead's page had none, so a lead filed under a campaign by the New\n * lead drawer, the bulk bar or a sequence's enroll showed nothing of it —\n * the one field on the record the reader had no way to see or change.\n *\n * The same control, the same value helper, the same helper line. The save\n * is the lead's own client door — a one-field `update` on\n * `hosts/{hostId}/leads`, as the properties card writes status and notes —\n * guarded by the seed like every draft edited over a cached read, and\n * `campaignMembershipUnchanged` keeps Save quiet while the picker only\n * reorders what is stored.\n *\n * It is FILING, not audience: the helper says so, in the sentence the\n * contact card uses, because a reader who finds a campaign picker on a\n * person's page assumes the opposite.\n */\nexport function LeadCampaignsCard(props: LeadCampaignsCardProps) {\n const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const stored = readCampaignIds(lead)\n const [selected, setSelected] = useState<string[]>(stored)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n // A newer membership from the server replaces an UNEDITED selection — a\n // sequence's enroll or a bulk add lands while the page is open — and an\n // edited one is the reader's, decided by the guard on save.\n const storedKey = stored.join(',')\n useEffect(() => {\n if (!dirty) setSelected(stored)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storedKey, dirty])\n\n const unchanged = campaignMembershipUnchanged(stored, selected)\n const empty = optionsReady && !options.length\n\n const save = async () => {\n if (unchanged) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setSaving(true)\n const next = campaignMembershipValue(selected)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n campaignIds: next,\n updatedAt: serverTimestamp(),\n })\n },\n )\n setSaving(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The filing could not be saved.', { variant: 'warning' })\n return\n }\n setDirty(false)\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n onFiled?.({\n added: next.filter((id) => !stored.includes(id)),\n removed: stored.filter((id) => !next.includes(id)),\n })\n }\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <CampaignPicker\n options={options}\n value={selected}\n onChange={(next) => {\n setSelected(next)\n setDirty(true)\n }}\n label=\"Filed under campaigns\"\n helperText={LEAD_CAMPAIGNS_HELPER_TEXT}\n disabled={saving}\n empty={empty}\n emptyText=\"This site has no campaigns yet. Create one from Marketing to file leads under it.\"\n />\n {empty ? null : (\n <Stack direction=\"row\">\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || unchanged}\n onClick={() => void save().catch((error: unknown) => {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n setSaving(false)\n })}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n </Stack>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nLeadCampaignsCard.displayName = 'LeadCampaignsCard'\n\nexport default LeadCampaignsCard\n"],"names":["campaignMembershipUnchanged","campaignMembershipValue","pluginDocsHelp","readCampaignIds","CampaignPicker","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Stack","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","LEAD_CAMPAIGNS_HELPER_TEXT","leadCampaignNames","lead","options","labels","Map","map","option","value","label","id","get","filter","name","Boolean","LeadCampaignsCard","props","hostId","leadId","leadStatus","fromCache","optionsReady","onFiled","orgId","firestore","enqueueSnackbar","stored","selected","setSelected","dirty","setDirty","saving","setSaving","storedKey","join","unchanged","empty","length","save","variant","persist","next","verdict","subject","unreadable","campaignIds","updatedAt","ok","message","added","includes","removed","header","help","anchor","contentGutterX","contentGutterY","spacing","onChange","helperText","disabled","emptyText","direction","size","onClick","catch","error","console","Error","allowDuplicate","displayName"],"mappings":"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"}
|
|
@@ -3,7 +3,14 @@ import type { OrgMemberOptions } from '../hooks/use-org-member-options';
|
|
|
3
3
|
export interface LeadConvertDialogProps {
|
|
4
4
|
open: boolean;
|
|
5
5
|
onClose: () => void;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The site the conversion is FILED AS (AGL-3278): the mounted one, or
|
|
8
|
+
* the lead's own first capturing site at the organization level. A
|
|
9
|
+
* conversion has to name one — the contact it makes is captured by a
|
|
10
|
+
* site, and its consent is that site's group's — so with none the
|
|
11
|
+
* dialog refuses rather than posting an empty host.
|
|
12
|
+
*/
|
|
13
|
+
hostId: string | null;
|
|
7
14
|
orgId: string | null;
|
|
8
15
|
org: Record<string, unknown> | null | undefined;
|
|
9
16
|
leadId: string;
|
|
@@ -169,13 +169,15 @@ import { LeadOwnerSelect } from "./lead-owner-select.js";
|
|
|
169
169
|
]);
|
|
170
170
|
const amountCents = dollarsToCents(dealAmount);
|
|
171
171
|
const amountInvalid = amountCents === undefined;
|
|
172
|
-
const submittable = !busy &&
|
|
172
|
+
const submittable = !busy && // A conversion names the site it is filed as; there is none for a lead
|
|
173
|
+
// no site captured (AGL-3278).
|
|
174
|
+
Boolean(hostId) && (companyMode !== 'new' || companyName.trim().length > 0) && (companyMode !== 'existing' || Boolean(companyId)) && (!dealOn || dealTitle.trim().length > 0 && !amountInvalid);
|
|
173
175
|
const submit = async ()=>{
|
|
174
176
|
if (!submittable) return;
|
|
175
177
|
setBusy(true);
|
|
176
178
|
setError(null);
|
|
177
179
|
const body = _extends({
|
|
178
|
-
hostId,
|
|
180
|
+
hostId: hostId != null ? hostId : '',
|
|
179
181
|
leadId
|
|
180
182
|
}, ownerUid ? {
|
|
181
183
|
ownerUid
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-convert-dialog.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type { CrmDealStage, CrmLeadFields } from '@aglyn/aglyn'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Checkbox,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Divider,\n FormControl,\n FormControlLabel,\n InputLabel,\n MenuItem,\n Radio,\n RadioGroup,\n Select,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n documentId,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useId, useMemo, useRef, useState } from 'react'\nimport {\n dollarsToCents,\n suggestCompanyForLead,\n} from '../model/lead-company-suggestion'\nimport { crmRoutes } from '../model/crm-routes'\nimport type { LeadConvertRequest, LeadConvertResponse } from '../server/lead-convert'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { LeadOwnerSelect } from './lead-owner-select'\n\n/**\n * How many companies the picker loads. Ordered by name, so a bigger org sees\n * its first two hundred alphabetically and the domain match still finds the\n * right one when it is among them; past that the converter creates and the\n * route dedupes by domain server-side, which is the backstop this ceiling\n * relies on.\n */\nconst COMPANY_OPTIONS_CEILING = 200\n\ntype CompanyMode = 'none' | 'existing' | 'new'\n\nexport interface LeadConvertDialogProps {\n open: boolean\n onClose: () => void\n hostId: string\n orgId: string | null\n org: Record<string, unknown> | null | undefined\n leadId: string\n lead: Record<string, unknown> & CrmLeadFields\n basePath: string\n roster: OrgMemberOptions\n}\n\n/**\n * Convert a lead into a contact, and optionally a company and a deal\n * (AGL-2608).\n *\n * The dialog collects the decisions and posts ONE request to\n * `crm/lead-convert`; it writes nothing itself, because the contact can only\n * come from the server's dedupe door and the rest of the conversion has to\n * follow it in order. What it does own is the suggestion: the company the\n * lead's email domain implies, matched against the companies this reader can\n * see, so the common case is a glance and a click.\n *\n * Both reads open only while the dialog is — a converter who never opens it\n * pays for neither the company list nor the pipeline.\n */\nexport function LeadConvertDialog(props: LeadConvertDialogProps) {\n const { open, onClose, hostId, orgId, org, leadId, lead, basePath, roster } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n\n /*\n * The scope this reader may see — the same tokens every CRM listener\n * filters on, and the rules refuse a list without them. The Leads list\n * mounts the dialog closed at the organization level with no site at all\n * (the row it converts brings one), and a consent group must name a site,\n * so until then the scope is the organization's token alone — nothing reads\n * it while the dialog is closed.\n */\n const visibleToTokens = useMemo(() => {\n const group = hostId ? Aglyn.consentGroupForHost(org ?? {}, hostId) : null\n return [\n Aglyn.ORG_SCOPE_TOKEN,\n ...(group?.hostIds ?? []).map((id) => Aglyn.hostScopeToken(id)),\n ].slice(0, Aglyn.MAX_SCOPE_HOSTS)\n }, [org, hostId])\n const tokensKey = visibleToTokens.join(',')\n\n const { data: companies, status: companiesStatus } = useFirestoreCollection<\n Record<string, unknown> & { $id: string; name?: string; domain?: string }\n >(\n () =>\n open && orgId\n ? query(\n collection(firestore, 'orgs', orgId, Aglyn.CRM_COLLECTIONS.companies),\n where('visibleTo', 'array-contains-any', visibleToTokens),\n orderBy('nameLower'),\n limit(COMPANY_OPTIONS_CEILING),\n )\n : null,\n [firestore, orgId, open, tokensKey],\n { idField: '$id' },\n )\n const { data: pipelines } = useFirestoreCollection<\n Record<string, unknown> & { $id: string; isDefault?: boolean; stages?: CrmDealStage[] }\n >(\n () =>\n open && orgId\n ? query(\n collection(firestore, 'orgs', orgId, Aglyn.CRM_COLLECTIONS.pipelines),\n where('visibleTo', 'array-contains-any', visibleToTokens),\n orderBy(documentId()),\n limit(20),\n )\n : null,\n [firestore, orgId, open, tokensKey],\n { idField: '$id' },\n )\n /*\n * The pipeline the deal will open in — the route's own choice, mirrored so\n * the stage picker offers the stages the deal will really have: the flagged\n * default, else the first by id, else the stages the route will seed.\n */\n const pipeline = pipelines.find((entry) => entry.isDefault) ?? pipelines[0]\n const stages = useMemo(\n () =>\n [...(pipeline?.stages ?? Aglyn.DEFAULT_DEAL_STAGES)]\n .filter((stage) => stage.kind === 'open')\n .sort((a, b) => a.order - b.order),\n [pipeline],\n )\n\n const email = String(lead['email'] ?? '')\n const leadLabel = String(lead['name'] || email || leadId)\n\n const [ownerUid, setOwnerUid] = useState('')\n const [companyMode, setCompanyMode] = useState<CompanyMode>('none')\n const [companyId, setCompanyId] = useState('')\n const [companyName, setCompanyName] = useState('')\n const [companyDomain, setCompanyDomain] = useState('')\n const [dealOn, setDealOn] = useState(false)\n const [dealTitle, setDealTitle] = useState('')\n const [dealAmount, setDealAmount] = useState('')\n const [dealCurrency, setDealCurrency] = useState('usd')\n const [dealStageId, setDealStageId] = useState('')\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n // The labels' ids, so the two comboboxes are named \"Company\" and \"Stage\"\n // rather than after whatever they show — see `LeadOwnerSelect`.\n const companyLabelId = useId()\n const stageLabelId = useId()\n\n /*\n * Reset on every open, and seed the company step ONCE per open when the\n * company list has answered — not on every snapshot, or a converter who\n * changed their mind would have the suggestion put back under them.\n */\n const seededFor = useRef<string | null>(null)\n useEffect(() => {\n if (!open) {\n seededFor.current = null\n return\n }\n setOwnerUid(String(lead.ownerUid ?? user?.uid ?? ''))\n setDealOn(false)\n setDealTitle(leadLabel)\n setDealAmount('')\n setDealCurrency('usd')\n setDealStageId('')\n setError(null)\n setCompanyMode('none')\n setCompanyId('')\n setCompanyName('')\n setCompanyDomain('')\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, leadId])\n useEffect(() => {\n if (!open || companiesStatus !== 'success') return\n if (seededFor.current === leadId) return\n seededFor.current = leadId\n // The lead's own company text first (AGL-3233), then the domain.\n const suggestion = suggestCompanyForLead(email, companies, lead.company)\n setCompanyMode(suggestion.mode)\n if (suggestion.mode === 'existing') setCompanyId(suggestion.companyId)\n if (suggestion.mode === 'new') {\n setCompanyName(suggestion.name)\n setCompanyDomain(suggestion.domain)\n }\n }, [open, companiesStatus, companies, email, leadId, lead.company])\n\n const amountCents = dollarsToCents(dealAmount)\n const amountInvalid = amountCents === undefined\n const submittable =\n !busy &&\n (companyMode !== 'new' || companyName.trim().length > 0) &&\n (companyMode !== 'existing' || Boolean(companyId)) &&\n (!dealOn || (dealTitle.trim().length > 0 && !amountInvalid))\n\n const submit = async () => {\n if (!submittable) return\n setBusy(true)\n setError(null)\n const body: LeadConvertRequest = {\n hostId,\n leadId,\n ...(ownerUid ? { ownerUid } : {}),\n ...(companyMode === 'existing' ? { companyId } : {}),\n ...(companyMode === 'new'\n ? {\n createCompany: {\n name: companyName.trim(),\n ...(companyDomain.trim() ? { domain: companyDomain.trim() } : {}),\n },\n }\n : {}),\n ...(dealOn\n ? {\n deal: {\n title: dealTitle.trim(),\n ...(typeof amountCents === 'number' ? { amountCents } : {}),\n currency: dealCurrency.trim().toLowerCase() || 'usd',\n ...(dealStageId ? { stageId: dealStageId } : {}),\n },\n }\n : {}),\n }\n try {\n const response = await authorizedFetch(user, '/api/crm/lead-convert', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n const answer = (await response.json().catch(() => ({}))) as\n | LeadConvertResponse\n | { error?: string }\n if (!response.ok || !('ok' in answer)) {\n setError(\n String((answer as { error?: string }).error ?? 'The lead could not be converted.'),\n )\n return\n }\n enqueueSnackbar(\n answer.alreadyConverted\n ? 'This lead was already converted — opening the contact'\n : 'Lead converted',\n { variant: 'success', persist: false },\n )\n onClose()\n router.push(crmRoutes(basePath).contact(answer.contactId))\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : 'The lead could not be converted.')\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} maxWidth=\"sm\" fullWidth>\n <DialogTitle>{`Convert ${leadLabel}`}</DialogTitle>\n <DialogContent>\n <Stack spacing={3} sx={{ pt: 1 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Contact'}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${email} becomes a contact at the Sales qualified stage — or ` +\n 'joins the one this address already has.'}\n </Typography>\n <LeadOwnerSelect value={ownerUid} onChange={setOwnerUid} roster={roster} />\n </Stack>\n <Divider />\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Company'}</Typography>\n <RadioGroup\n value={companyMode}\n onChange={(event) => setCompanyMode(event.target.value as CompanyMode)}\n >\n <FormControlLabel value=\"none\" control={<Radio size=\"small\" />} label=\"No company\" />\n <FormControlLabel\n value=\"existing\"\n control={<Radio size=\"small\" />}\n label=\"Link an existing company\"\n disabled={companies.length === 0}\n />\n <FormControlLabel\n value=\"new\"\n control={<Radio size=\"small\" />}\n label=\"Create a company\"\n />\n </RadioGroup>\n {companyMode === 'existing' ? (\n <FormControl size=\"small\" fullWidth>\n <InputLabel id={companyLabelId}>{'Company'}</InputLabel>\n <Select\n labelId={companyLabelId}\n label=\"Company\"\n value={companyId}\n onChange={(event) => setCompanyId(String(event.target.value))}\n >\n {companies.map((company) => (\n <MenuItem key={company.$id} value={company.$id}>\n {String(company.name ?? company.$id)}\n {company.domain ? (\n <Typography\n component=\"span\"\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ ml: 1 }}\n >\n {String(company.domain)}\n </Typography>\n ) : null}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n ) : null}\n {companyMode === 'new' ? (\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n <TextField\n size=\"small\"\n label=\"Company name\"\n value={companyName}\n onChange={(event) => setCompanyName(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 120 } }}\n />\n <TextField\n size=\"small\"\n label=\"Domain\"\n value={companyDomain}\n onChange={(event) => setCompanyDomain(event.target.value)}\n helperText=\"Used to match future contacts to this company\"\n fullWidth\n />\n </Stack>\n ) : null}\n </Stack>\n <Divider />\n <Stack spacing={1}>\n <FormControlLabel\n control={\n <Checkbox\n checked={dealOn}\n onChange={(event) => setDealOn(event.target.checked)}\n />\n }\n label={<Typography variant=\"subtitle2\">{'Open a deal'}</Typography>}\n />\n {dealOn ? (\n <Stack spacing={1}>\n <TextField\n size=\"small\"\n label=\"Deal title\"\n value={dealTitle}\n onChange={(event) => setDealTitle(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n <TextField\n size=\"small\"\n label=\"Amount\"\n value={dealAmount}\n onChange={(event) => setDealAmount(event.target.value)}\n error={amountInvalid}\n helperText={amountInvalid ? 'Enter an amount like 1200 or 1,200.50' : ' '}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Currency\"\n value={dealCurrency}\n onChange={(event) => setDealCurrency(event.target.value)}\n helperText=\" \"\n sx={{ width: { sm: 140 } }}\n slotProps={{ htmlInput: { maxLength: 3 } }}\n />\n </Stack>\n <FormControl size=\"small\" fullWidth>\n {/* `shrink`, because `displayEmpty` below draws the \"First\n stage\" placeholder where an empty value would leave the\n field blank — and a label that has not shrunk sits on\n top of it. */}\n <InputLabel id={stageLabelId} shrink>\n {'Stage'}\n </InputLabel>\n <Select\n labelId={stageLabelId}\n label=\"Stage\"\n notched\n value={dealStageId}\n onChange={(event) => setDealStageId(String(event.target.value))}\n displayEmpty\n >\n <MenuItem value=\"\">\n <em>{stages[0] ? `First stage (${stages[0].name})` : 'First stage'}</em>\n </MenuItem>\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n {!pipeline ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'This workspace has no pipeline yet — a Sales pipeline ' +\n 'with the default stages is created with the deal.'}\n </Typography>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" onClick={() => void submit()} disabled={!submittable}>\n {busy ? 'Converting…' : 'Convert'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nLeadConvertDialog.displayName = 'LeadConvertDialog'\n\nexport default LeadConvertDialog\n"],"names":["Aglyn","authorizedFetch","useSnackbar","useFirestore","useFirestoreCollection","useUser","Alert","Button","Checkbox","Dialog","DialogActions","DialogContent","DialogTitle","Divider","FormControl","FormControlLabel","InputLabel","MenuItem","Radio","RadioGroup","Select","Stack","TextField","Typography","collection","documentId","limit","orderBy","query","where","useRouter","useEffect","useId","useMemo","useRef","useState","dollarsToCents","suggestCompanyForLead","crmRoutes","LeadOwnerSelect","COMPANY_OPTIONS_CEILING","LeadConvertDialog","props","pipelines","lead","open","onClose","hostId","orgId","org","leadId","basePath","roster","firestore","router","data","user","enqueueSnackbar","visibleToTokens","group","consentGroupForHost","ORG_SCOPE_TOKEN","hostIds","map","id","hostScopeToken","slice","MAX_SCOPE_HOSTS","tokensKey","join","companies","status","companiesStatus","CRM_COLLECTIONS","idField","pipeline","find","entry","isDefault","stages","DEFAULT_DEAL_STAGES","filter","stage","kind","sort","a","b","order","email","String","leadLabel","ownerUid","setOwnerUid","companyMode","setCompanyMode","companyId","setCompanyId","companyName","setCompanyName","companyDomain","setCompanyDomain","dealOn","setDealOn","dealTitle","setDealTitle","dealAmount","setDealAmount","dealCurrency","setDealCurrency","dealStageId","setDealStageId","busy","setBusy","error","setError","companyLabelId","stageLabelId","seededFor","current","uid","suggestion","company","mode","name","domain","amountCents","amountInvalid","undefined","submittable","trim","length","Boolean","submit","body","createCompany","deal","title","currency","toLowerCase","stageId","response","method","headers","JSON","stringify","answer","json","catch","ok","alreadyConverted","variant","persist","push","contact","contactId","cause","Error","message","maxWidth","fullWidth","spacing","sx","pt","color","value","onChange","event","target","control","size","label","disabled","labelId","$id","component","ml","direction","xs","sm","slotProps","htmlInput","maxLength","helperText","checked","width","shrink","notched","displayEmpty","em","severity","onClick","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-convert-dialog.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type { CrmDealStage, CrmLeadFields } from '@aglyn/aglyn'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Checkbox,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Divider,\n FormControl,\n FormControlLabel,\n InputLabel,\n MenuItem,\n Radio,\n RadioGroup,\n Select,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n documentId,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useId, useMemo, useRef, useState } from 'react'\nimport {\n dollarsToCents,\n suggestCompanyForLead,\n} from '../model/lead-company-suggestion'\nimport { crmRoutes } from '../model/crm-routes'\nimport type { LeadConvertRequest, LeadConvertResponse } from '../server/lead-convert'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { LeadOwnerSelect } from './lead-owner-select'\n\n/**\n * How many companies the picker loads. Ordered by name, so a bigger org sees\n * its first two hundred alphabetically and the domain match still finds the\n * right one when it is among them; past that the converter creates and the\n * route dedupes by domain server-side, which is the backstop this ceiling\n * relies on.\n */\nconst COMPANY_OPTIONS_CEILING = 200\n\ntype CompanyMode = 'none' | 'existing' | 'new'\n\nexport interface LeadConvertDialogProps {\n open: boolean\n onClose: () => void\n /**\n * The site the conversion is FILED AS (AGL-3278): the mounted one, or\n * the lead's own first capturing site at the organization level. A\n * conversion has to name one — the contact it makes is captured by a\n * site, and its consent is that site's group's — so with none the\n * dialog refuses rather than posting an empty host.\n */\n hostId: string | null\n orgId: string | null\n org: Record<string, unknown> | null | undefined\n leadId: string\n lead: Record<string, unknown> & CrmLeadFields\n basePath: string\n roster: OrgMemberOptions\n}\n\n/**\n * Convert a lead into a contact, and optionally a company and a deal\n * (AGL-2608).\n *\n * The dialog collects the decisions and posts ONE request to\n * `crm/lead-convert`; it writes nothing itself, because the contact can only\n * come from the server's dedupe door and the rest of the conversion has to\n * follow it in order. What it does own is the suggestion: the company the\n * lead's email domain implies, matched against the companies this reader can\n * see, so the common case is a glance and a click.\n *\n * Both reads open only while the dialog is — a converter who never opens it\n * pays for neither the company list nor the pipeline.\n */\nexport function LeadConvertDialog(props: LeadConvertDialogProps) {\n const { open, onClose, hostId, orgId, org, leadId, lead, basePath, roster } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n\n /*\n * The scope this reader may see — the same tokens every CRM listener\n * filters on, and the rules refuse a list without them. The Leads list\n * mounts the dialog closed at the organization level with no site at all\n * (the row it converts brings one), and a consent group must name a site,\n * so until then the scope is the organization's token alone — nothing reads\n * it while the dialog is closed.\n */\n const visibleToTokens = useMemo(() => {\n const group = hostId ? Aglyn.consentGroupForHost(org ?? {}, hostId) : null\n return [\n Aglyn.ORG_SCOPE_TOKEN,\n ...(group?.hostIds ?? []).map((id) => Aglyn.hostScopeToken(id)),\n ].slice(0, Aglyn.MAX_SCOPE_HOSTS)\n }, [org, hostId])\n const tokensKey = visibleToTokens.join(',')\n\n const { data: companies, status: companiesStatus } = useFirestoreCollection<\n Record<string, unknown> & { $id: string; name?: string; domain?: string }\n >(\n () =>\n open && orgId\n ? query(\n collection(firestore, 'orgs', orgId, Aglyn.CRM_COLLECTIONS.companies),\n where('visibleTo', 'array-contains-any', visibleToTokens),\n orderBy('nameLower'),\n limit(COMPANY_OPTIONS_CEILING),\n )\n : null,\n [firestore, orgId, open, tokensKey],\n { idField: '$id' },\n )\n const { data: pipelines } = useFirestoreCollection<\n Record<string, unknown> & { $id: string; isDefault?: boolean; stages?: CrmDealStage[] }\n >(\n () =>\n open && orgId\n ? query(\n collection(firestore, 'orgs', orgId, Aglyn.CRM_COLLECTIONS.pipelines),\n where('visibleTo', 'array-contains-any', visibleToTokens),\n orderBy(documentId()),\n limit(20),\n )\n : null,\n [firestore, orgId, open, tokensKey],\n { idField: '$id' },\n )\n /*\n * The pipeline the deal will open in — the route's own choice, mirrored so\n * the stage picker offers the stages the deal will really have: the flagged\n * default, else the first by id, else the stages the route will seed.\n */\n const pipeline = pipelines.find((entry) => entry.isDefault) ?? pipelines[0]\n const stages = useMemo(\n () =>\n [...(pipeline?.stages ?? Aglyn.DEFAULT_DEAL_STAGES)]\n .filter((stage) => stage.kind === 'open')\n .sort((a, b) => a.order - b.order),\n [pipeline],\n )\n\n const email = String(lead['email'] ?? '')\n const leadLabel = String(lead['name'] || email || leadId)\n\n const [ownerUid, setOwnerUid] = useState('')\n const [companyMode, setCompanyMode] = useState<CompanyMode>('none')\n const [companyId, setCompanyId] = useState('')\n const [companyName, setCompanyName] = useState('')\n const [companyDomain, setCompanyDomain] = useState('')\n const [dealOn, setDealOn] = useState(false)\n const [dealTitle, setDealTitle] = useState('')\n const [dealAmount, setDealAmount] = useState('')\n const [dealCurrency, setDealCurrency] = useState('usd')\n const [dealStageId, setDealStageId] = useState('')\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n // The labels' ids, so the two comboboxes are named \"Company\" and \"Stage\"\n // rather than after whatever they show — see `LeadOwnerSelect`.\n const companyLabelId = useId()\n const stageLabelId = useId()\n\n /*\n * Reset on every open, and seed the company step ONCE per open when the\n * company list has answered — not on every snapshot, or a converter who\n * changed their mind would have the suggestion put back under them.\n */\n const seededFor = useRef<string | null>(null)\n useEffect(() => {\n if (!open) {\n seededFor.current = null\n return\n }\n setOwnerUid(String(lead.ownerUid ?? user?.uid ?? ''))\n setDealOn(false)\n setDealTitle(leadLabel)\n setDealAmount('')\n setDealCurrency('usd')\n setDealStageId('')\n setError(null)\n setCompanyMode('none')\n setCompanyId('')\n setCompanyName('')\n setCompanyDomain('')\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, leadId])\n useEffect(() => {\n if (!open || companiesStatus !== 'success') return\n if (seededFor.current === leadId) return\n seededFor.current = leadId\n // The lead's own company text first (AGL-3233), then the domain.\n const suggestion = suggestCompanyForLead(email, companies, lead.company)\n setCompanyMode(suggestion.mode)\n if (suggestion.mode === 'existing') setCompanyId(suggestion.companyId)\n if (suggestion.mode === 'new') {\n setCompanyName(suggestion.name)\n setCompanyDomain(suggestion.domain)\n }\n }, [open, companiesStatus, companies, email, leadId, lead.company])\n\n const amountCents = dollarsToCents(dealAmount)\n const amountInvalid = amountCents === undefined\n const submittable =\n !busy &&\n // A conversion names the site it is filed as; there is none for a lead\n // no site captured (AGL-3278).\n Boolean(hostId) &&\n (companyMode !== 'new' || companyName.trim().length > 0) &&\n (companyMode !== 'existing' || Boolean(companyId)) &&\n (!dealOn || (dealTitle.trim().length > 0 && !amountInvalid))\n\n const submit = async () => {\n if (!submittable) return\n setBusy(true)\n setError(null)\n const body: LeadConvertRequest = {\n hostId: hostId ?? '',\n leadId,\n ...(ownerUid ? { ownerUid } : {}),\n ...(companyMode === 'existing' ? { companyId } : {}),\n ...(companyMode === 'new'\n ? {\n createCompany: {\n name: companyName.trim(),\n ...(companyDomain.trim() ? { domain: companyDomain.trim() } : {}),\n },\n }\n : {}),\n ...(dealOn\n ? {\n deal: {\n title: dealTitle.trim(),\n ...(typeof amountCents === 'number' ? { amountCents } : {}),\n currency: dealCurrency.trim().toLowerCase() || 'usd',\n ...(dealStageId ? { stageId: dealStageId } : {}),\n },\n }\n : {}),\n }\n try {\n const response = await authorizedFetch(user, '/api/crm/lead-convert', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n const answer = (await response.json().catch(() => ({}))) as\n | LeadConvertResponse\n | { error?: string }\n if (!response.ok || !('ok' in answer)) {\n setError(\n String((answer as { error?: string }).error ?? 'The lead could not be converted.'),\n )\n return\n }\n enqueueSnackbar(\n answer.alreadyConverted\n ? 'This lead was already converted — opening the contact'\n : 'Lead converted',\n { variant: 'success', persist: false },\n )\n onClose()\n router.push(crmRoutes(basePath).contact(answer.contactId))\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : 'The lead could not be converted.')\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} maxWidth=\"sm\" fullWidth>\n <DialogTitle>{`Convert ${leadLabel}`}</DialogTitle>\n <DialogContent>\n <Stack spacing={3} sx={{ pt: 1 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Contact'}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${email} becomes a contact at the Sales qualified stage — or ` +\n 'joins the one this address already has.'}\n </Typography>\n <LeadOwnerSelect value={ownerUid} onChange={setOwnerUid} roster={roster} />\n </Stack>\n <Divider />\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Company'}</Typography>\n <RadioGroup\n value={companyMode}\n onChange={(event) => setCompanyMode(event.target.value as CompanyMode)}\n >\n <FormControlLabel value=\"none\" control={<Radio size=\"small\" />} label=\"No company\" />\n <FormControlLabel\n value=\"existing\"\n control={<Radio size=\"small\" />}\n label=\"Link an existing company\"\n disabled={companies.length === 0}\n />\n <FormControlLabel\n value=\"new\"\n control={<Radio size=\"small\" />}\n label=\"Create a company\"\n />\n </RadioGroup>\n {companyMode === 'existing' ? (\n <FormControl size=\"small\" fullWidth>\n <InputLabel id={companyLabelId}>{'Company'}</InputLabel>\n <Select\n labelId={companyLabelId}\n label=\"Company\"\n value={companyId}\n onChange={(event) => setCompanyId(String(event.target.value))}\n >\n {companies.map((company) => (\n <MenuItem key={company.$id} value={company.$id}>\n {String(company.name ?? company.$id)}\n {company.domain ? (\n <Typography\n component=\"span\"\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ ml: 1 }}\n >\n {String(company.domain)}\n </Typography>\n ) : null}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n ) : null}\n {companyMode === 'new' ? (\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n <TextField\n size=\"small\"\n label=\"Company name\"\n value={companyName}\n onChange={(event) => setCompanyName(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 120 } }}\n />\n <TextField\n size=\"small\"\n label=\"Domain\"\n value={companyDomain}\n onChange={(event) => setCompanyDomain(event.target.value)}\n helperText=\"Used to match future contacts to this company\"\n fullWidth\n />\n </Stack>\n ) : null}\n </Stack>\n <Divider />\n <Stack spacing={1}>\n <FormControlLabel\n control={\n <Checkbox\n checked={dealOn}\n onChange={(event) => setDealOn(event.target.checked)}\n />\n }\n label={<Typography variant=\"subtitle2\">{'Open a deal'}</Typography>}\n />\n {dealOn ? (\n <Stack spacing={1}>\n <TextField\n size=\"small\"\n label=\"Deal title\"\n value={dealTitle}\n onChange={(event) => setDealTitle(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n <TextField\n size=\"small\"\n label=\"Amount\"\n value={dealAmount}\n onChange={(event) => setDealAmount(event.target.value)}\n error={amountInvalid}\n helperText={amountInvalid ? 'Enter an amount like 1200 or 1,200.50' : ' '}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Currency\"\n value={dealCurrency}\n onChange={(event) => setDealCurrency(event.target.value)}\n helperText=\" \"\n sx={{ width: { sm: 140 } }}\n slotProps={{ htmlInput: { maxLength: 3 } }}\n />\n </Stack>\n <FormControl size=\"small\" fullWidth>\n {/* `shrink`, because `displayEmpty` below draws the \"First\n stage\" placeholder where an empty value would leave the\n field blank — and a label that has not shrunk sits on\n top of it. */}\n <InputLabel id={stageLabelId} shrink>\n {'Stage'}\n </InputLabel>\n <Select\n labelId={stageLabelId}\n label=\"Stage\"\n notched\n value={dealStageId}\n onChange={(event) => setDealStageId(String(event.target.value))}\n displayEmpty\n >\n <MenuItem value=\"\">\n <em>{stages[0] ? `First stage (${stages[0].name})` : 'First stage'}</em>\n </MenuItem>\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n {!pipeline ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'This workspace has no pipeline yet — a Sales pipeline ' +\n 'with the default stages is created with the deal.'}\n </Typography>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" onClick={() => void submit()} disabled={!submittable}>\n {busy ? 'Converting…' : 'Convert'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nLeadConvertDialog.displayName = 'LeadConvertDialog'\n\nexport default LeadConvertDialog\n"],"names":["Aglyn","authorizedFetch","useSnackbar","useFirestore","useFirestoreCollection","useUser","Alert","Button","Checkbox","Dialog","DialogActions","DialogContent","DialogTitle","Divider","FormControl","FormControlLabel","InputLabel","MenuItem","Radio","RadioGroup","Select","Stack","TextField","Typography","collection","documentId","limit","orderBy","query","where","useRouter","useEffect","useId","useMemo","useRef","useState","dollarsToCents","suggestCompanyForLead","crmRoutes","LeadOwnerSelect","COMPANY_OPTIONS_CEILING","LeadConvertDialog","props","pipelines","lead","open","onClose","hostId","orgId","org","leadId","basePath","roster","firestore","router","data","user","enqueueSnackbar","visibleToTokens","group","consentGroupForHost","ORG_SCOPE_TOKEN","hostIds","map","id","hostScopeToken","slice","MAX_SCOPE_HOSTS","tokensKey","join","companies","status","companiesStatus","CRM_COLLECTIONS","idField","pipeline","find","entry","isDefault","stages","DEFAULT_DEAL_STAGES","filter","stage","kind","sort","a","b","order","email","String","leadLabel","ownerUid","setOwnerUid","companyMode","setCompanyMode","companyId","setCompanyId","companyName","setCompanyName","companyDomain","setCompanyDomain","dealOn","setDealOn","dealTitle","setDealTitle","dealAmount","setDealAmount","dealCurrency","setDealCurrency","dealStageId","setDealStageId","busy","setBusy","error","setError","companyLabelId","stageLabelId","seededFor","current","uid","suggestion","company","mode","name","domain","amountCents","amountInvalid","undefined","submittable","Boolean","trim","length","submit","body","createCompany","deal","title","currency","toLowerCase","stageId","response","method","headers","JSON","stringify","answer","json","catch","ok","alreadyConverted","variant","persist","push","contact","contactId","cause","Error","message","maxWidth","fullWidth","spacing","sx","pt","color","value","onChange","event","target","control","size","label","disabled","labelId","$id","component","ml","direction","xs","sm","slotProps","htmlInput","maxLength","helperText","checked","width","shrink","notched","displayEmpty","em","severity","onClick","displayName"],"mappings":"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"}
|
|
@@ -4,10 +4,24 @@ import { type CrmDetailPageProps } from '../model/crm-routes';
|
|
|
4
4
|
*
|
|
5
5
|
* The record behind a row of the Leads section: what the person did on the
|
|
6
6
|
* site, who is working them, and the conversion that turns them into a
|
|
7
|
-
* contact, a company and a deal. One document listen
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* contact, a company and a deal. One document listen and one roster request
|
|
8
|
+
* for the owner picker; the convert dialog's reads open only when it does.
|
|
9
|
+
*
|
|
10
|
+
* ## The two levels, and the site a lead is acted on as (AGL-3278)
|
|
11
|
+
*
|
|
12
|
+
* The org root comes from `useCrmScope`, as it does on every other CRM
|
|
13
|
+
* record page: under a site it is looked up from that site, and at the
|
|
14
|
+
* organization level it is the mount's, known at once. It used to come
|
|
15
|
+
* from `useOrgDataScope({ hostId })` — correct only under a site, because
|
|
16
|
+
* an absent host settles at `orgId: null`, and a null org builds no
|
|
17
|
+
* reference, opens no listener and leaves the page loading forever.
|
|
18
|
+
*
|
|
19
|
+
* Several surfaces still need ONE site: the campaigns to offer, the feed
|
|
20
|
+
* an activity is filed in, the booking door, the consent basis, the
|
|
21
|
+
* conversion. Under a site that is the mounted one. At the organization
|
|
22
|
+
* level it is the lead's own first capturing site — `leadPrimaryGroup` —
|
|
23
|
+
* the same answer `contactPrimaryGroup` gives the contact page, and the
|
|
24
|
+
* same one the Leads list already converts a row as.
|
|
11
25
|
*/
|
|
12
26
|
export declare function LeadDetailPage(props: CrmDetailPageProps): import("react").JSX.Element;
|
|
13
27
|
export declare namespace LeadDetailPage {
|
|
@@ -14,13 +14,15 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
18
|
-
import { pluginDocsHelp, normalizeContactEmail, readErasureRequestedAtMs } from "@aglyn/aglyn";
|
|
19
|
-
import { useFirestore, useFirestoreDoc, useHostCampaigns
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { pluginDocsHelp, leadPrimaryGroup, normalizeContactEmail, readErasureRequestedAtMs } from "@aglyn/aglyn";
|
|
19
|
+
import { useFirestore, useFirestoreDoc, useHostCampaigns } from "@aglyn/tenant-feature-instance";
|
|
20
20
|
import { Stack, Typography } from "@mui/material";
|
|
21
21
|
import { doc } from "firebase/firestore";
|
|
22
|
-
import { useState } from "react";
|
|
22
|
+
import { useMemo, useState } from "react";
|
|
23
23
|
import { useCampaignFilingLog } from "../hooks/use-campaign-filing-log.js";
|
|
24
|
+
import { CrmCreateSiteDefault } from "../hooks/use-crm-org-mount.js";
|
|
25
|
+
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
24
26
|
import { useOrgMemberOptions } from "../hooks/use-org-member-options.js";
|
|
25
27
|
import { crmRoutes } from "../model/crm-routes.js";
|
|
26
28
|
import { CrmRecordChip, CrmRecordHeader } from "./crm-record-header.js";
|
|
@@ -37,42 +39,70 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
37
39
|
*
|
|
38
40
|
* The record behind a row of the Leads section: what the person did on the
|
|
39
41
|
* site, who is working them, and the conversion that turns them into a
|
|
40
|
-
* contact, a company and a deal. One document listen
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* contact, a company and a deal. One document listen and one roster request
|
|
43
|
+
* for the owner picker; the convert dialog's reads open only when it does.
|
|
44
|
+
*
|
|
45
|
+
* ## The two levels, and the site a lead is acted on as (AGL-3278)
|
|
46
|
+
*
|
|
47
|
+
* The org root comes from `useCrmScope`, as it does on every other CRM
|
|
48
|
+
* record page: under a site it is looked up from that site, and at the
|
|
49
|
+
* organization level it is the mount's, known at once. It used to come
|
|
50
|
+
* from `useOrgDataScope({ hostId })` — correct only under a site, because
|
|
51
|
+
* an absent host settles at `orgId: null`, and a null org builds no
|
|
52
|
+
* reference, opens no listener and leaves the page loading forever.
|
|
53
|
+
*
|
|
54
|
+
* Several surfaces still need ONE site: the campaigns to offer, the feed
|
|
55
|
+
* an activity is filed in, the booking door, the consent basis, the
|
|
56
|
+
* conversion. Under a site that is the mounted one. At the organization
|
|
57
|
+
* level it is the lead's own first capturing site — `leadPrimaryGroup` —
|
|
58
|
+
* the same answer `contactPrimaryGroup` gives the contact page, and the
|
|
59
|
+
* same one the Leads list already converts a row as.
|
|
44
60
|
*/ export function LeadDetailPage(props) {
|
|
45
61
|
const { id, hostId, org, basePath } = props;
|
|
46
62
|
const firestore = useFirestore();
|
|
47
63
|
const routes = crmRoutes(basePath);
|
|
48
64
|
/*
|
|
49
65
|
* Resolved BEFORE the read that needs it (AGL-3275). A lead is an org row
|
|
50
|
-
* now, so the org has to be known to address one — and `
|
|
66
|
+
* now, so the org has to be known to address one — and `scope` is null
|
|
51
67
|
* while the host-index lookup settles. Handing that null to `doc()` is a
|
|
52
68
|
* thrown `TypeError`, which the page renders as a 500 rather than as a
|
|
53
69
|
* record still loading.
|
|
54
|
-
*/ const { orgId } =
|
|
55
|
-
hostId
|
|
70
|
+
*/ const { scope, orgId, ready, consentGroup: viewingGroup } = useCrmScope({
|
|
71
|
+
hostId,
|
|
72
|
+
org
|
|
56
73
|
});
|
|
57
|
-
const { data: lead, status, fromCache } = useFirestoreDoc(()=>
|
|
74
|
+
const { data: lead, status, fromCache } = useFirestoreDoc(()=>scope ? doc(firestore, scope[0], scope[1], 'leads', id) : null, [
|
|
58
75
|
firestore,
|
|
59
|
-
|
|
76
|
+
scope,
|
|
60
77
|
id
|
|
61
78
|
]);
|
|
79
|
+
/*
|
|
80
|
+
* The group this lead is READ through: the mounted site's under a site,
|
|
81
|
+
* and the lead's own first capturing site at the organization level
|
|
82
|
+
* (AGL-3278). The consent basis is read against it — a refusal recorded
|
|
83
|
+
* for any sibling in a declared group stands against every one of them,
|
|
84
|
+
* so a group of one would report a person as opted in who had
|
|
85
|
+
* unsubscribed from the brand beside this one.
|
|
86
|
+
*/ const leadGroup = useMemo(()=>viewingGroup != null ? viewingGroup : leadPrimaryGroup(lead, org), [
|
|
87
|
+
viewingGroup,
|
|
88
|
+
lead,
|
|
89
|
+
org
|
|
90
|
+
]);
|
|
91
|
+
/** The one site this page acts as, or null for a lead no site captured. */ const siteHostId = hostId != null ? hostId : leadGroup.hostId || null;
|
|
62
92
|
const roster = useOrgMemberOptions(orgId);
|
|
63
93
|
/*
|
|
64
94
|
* The site's campaigns, read once for the page (AGL-3274): the header
|
|
65
95
|
* names the ones the lead is filed under and the Campaigns card offers
|
|
66
|
-
* them. One listener, not one per surface —
|
|
67
|
-
*
|
|
68
|
-
*/ const campaigns = useHostCampaigns(
|
|
96
|
+
* them. One listener, not one per surface — the containers are the
|
|
97
|
+
* capturing site's.
|
|
98
|
+
*/ const campaigns = useHostCampaigns(siteHostId != null ? siteHostId : undefined, {
|
|
69
99
|
enabled: true
|
|
70
100
|
});
|
|
71
101
|
// A saved filing is written on the lead's Activity too (AGL-3274), one
|
|
72
102
|
// entry per campaign added or removed, by the member who saved it.
|
|
73
103
|
const logFiling = useCampaignFilingLog({
|
|
74
104
|
orgId,
|
|
75
|
-
hostId,
|
|
105
|
+
hostId: siteHostId,
|
|
76
106
|
org: org
|
|
77
107
|
});
|
|
78
108
|
const [converting, setConverting] = useState(false);
|
|
@@ -81,7 +111,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
81
111
|
// contact: the same request, filed by the lead's address.
|
|
82
112
|
const leadEmail = lead ? normalizeContactEmail(lead['email']) : null;
|
|
83
113
|
const erase = useErasePersonAction({
|
|
84
|
-
hostId,
|
|
114
|
+
hostId: siteHostId,
|
|
85
115
|
orgId,
|
|
86
116
|
subject: lead && leadEmail ? {
|
|
87
117
|
kind: 'lead',
|
|
@@ -91,7 +121,13 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
91
121
|
requestedAtMs: readErasureRequestedAtMs(lead)
|
|
92
122
|
});
|
|
93
123
|
const label = lead ? String(lead['name'] || lead['email'] || id) : undefined;
|
|
94
|
-
|
|
124
|
+
/*
|
|
125
|
+
* A SETTLED lookup with no org is an answer, not a wait (AGL-3278). The
|
|
126
|
+
* spinner below is for a read in flight; standing on it once `ready` has
|
|
127
|
+
* said there is no org to read from is the endless "Loading…" this page
|
|
128
|
+
* shipped with.
|
|
129
|
+
*/ const noOrg = ready && !scope;
|
|
130
|
+
if (noOrg || status === 'error' || status === 'success' && !lead) {
|
|
95
131
|
return /*#__PURE__*/ _jsx(CrmRecordHeader, {
|
|
96
132
|
kind: "Lead",
|
|
97
133
|
title: undefined,
|
|
@@ -103,7 +139,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
103
139
|
children: /*#__PURE__*/ _jsx(Typography, {
|
|
104
140
|
variant: "body2",
|
|
105
141
|
color: "text.secondary",
|
|
106
|
-
children: status === 'error' ? 'This lead could not be read.' : 'This lead no longer exists — it may have been removed from the Inbox.'
|
|
142
|
+
children: noOrg || status === 'error' ? 'This lead could not be read.' : 'This lead no longer exists — it may have been removed from the Inbox.'
|
|
107
143
|
})
|
|
108
144
|
});
|
|
109
145
|
}
|
|
@@ -119,13 +155,17 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
119
155
|
loading: true
|
|
120
156
|
});
|
|
121
157
|
}
|
|
122
|
-
return
|
|
158
|
+
return(/* A task or an activity created from this page defaults to the lead's
|
|
159
|
+
own site, not to the site the reader last picked in a list's drawer
|
|
160
|
+
(AGL-2630) — as on every other record page. */ /*#__PURE__*/ _jsxs(CrmCreateSiteDefault, {
|
|
161
|
+
hostId: siteHostId,
|
|
123
162
|
children: [
|
|
124
163
|
/*#__PURE__*/ _jsxs(Stack, {
|
|
125
164
|
spacing: 3,
|
|
126
165
|
children: [
|
|
127
166
|
/*#__PURE__*/ _jsx(LeadPropertiesCard, {
|
|
128
|
-
hostId:
|
|
167
|
+
hostId: siteHostId,
|
|
168
|
+
consentGroup: leadGroup,
|
|
129
169
|
orgId: orgId,
|
|
130
170
|
leadId: id,
|
|
131
171
|
lead: lead,
|
|
@@ -148,14 +188,14 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
148
188
|
}, name))
|
|
149
189
|
}),
|
|
150
190
|
/*#__PURE__*/ _jsx(CrmRecordInsightsZone, {
|
|
151
|
-
hostId:
|
|
191
|
+
hostId: siteHostId,
|
|
152
192
|
org: org,
|
|
153
193
|
kind: "lead",
|
|
154
194
|
recordId: id,
|
|
155
195
|
name: label != null ? label : ''
|
|
156
196
|
}),
|
|
157
197
|
/*#__PURE__*/ _jsx(LeadCampaignsCard, {
|
|
158
|
-
hostId:
|
|
198
|
+
hostId: siteHostId,
|
|
159
199
|
leadId: id,
|
|
160
200
|
lead: lead,
|
|
161
201
|
leadStatus: status,
|
|
@@ -180,12 +220,12 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
180
220
|
}
|
|
181
221
|
}),
|
|
182
222
|
/*#__PURE__*/ _jsx(LeadHistoryCard, {
|
|
183
|
-
hostId:
|
|
223
|
+
hostId: siteHostId,
|
|
184
224
|
leadId: id,
|
|
185
225
|
lead: lead
|
|
186
226
|
}),
|
|
187
227
|
/*#__PURE__*/ _jsx(RecordActivityCard, {
|
|
188
|
-
hostId:
|
|
228
|
+
hostId: siteHostId,
|
|
189
229
|
org: org,
|
|
190
230
|
leadId: id
|
|
191
231
|
})
|
|
@@ -194,7 +234,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
194
234
|
/*#__PURE__*/ _jsx(LeadConvertDialog, {
|
|
195
235
|
open: converting,
|
|
196
236
|
onClose: ()=>setConverting(false),
|
|
197
|
-
hostId:
|
|
237
|
+
hostId: siteHostId,
|
|
198
238
|
orgId: orgId,
|
|
199
239
|
org: org,
|
|
200
240
|
leadId: id,
|
|
@@ -205,13 +245,13 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
205
245
|
/*#__PURE__*/ _jsx(LeadUnqualifyDialog, {
|
|
206
246
|
open: unqualifying,
|
|
207
247
|
onClose: ()=>setUnqualifying(false),
|
|
208
|
-
hostId:
|
|
248
|
+
hostId: siteHostId,
|
|
209
249
|
leadId: id,
|
|
210
250
|
leadLabel: label != null ? label : id
|
|
211
251
|
}),
|
|
212
252
|
erase.dialog
|
|
213
253
|
]
|
|
214
|
-
});
|
|
254
|
+
}));
|
|
215
255
|
}
|
|
216
256
|
LeadDetailPage.displayName = 'LeadDetailPage';
|
|
217
257
|
export default LeadDetailPage;
|