@aglyn/plugins-crm 1.0.0-beta.168 → 1.0.0-beta.170
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/crm-console-page.js +5 -4
- package/src/lib/components/crm-console-page.js.map +1 -1
- 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/src/lib/server/erase-person.js +3 -11
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/record-email-state.js +4 -14
- package/src/lib/server/record-email-state.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.170",
|
|
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.170",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.170",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.170",
|
|
31
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.170",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.170",
|
|
33
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.170",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.170",
|
|
35
|
+
"@aglyn/shared-util-email": "1.0.0-beta.170",
|
|
36
|
+
"@aglyn/shared-util-http": "1.0.0-beta.170",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.170",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.170",
|
|
39
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.170",
|
|
40
40
|
"@dnd-kit/core": "^6.3.1",
|
|
41
41
|
"@dnd-kit/utilities": "^3.2.2",
|
|
42
42
|
"@swc/helpers": "0.5.23",
|
|
@@ -68,10 +68,11 @@ import TasksSection from "./tasks-section.js";
|
|
|
68
68
|
* them, so the id alone could not say which record was meant. A lead is
|
|
69
69
|
* one org row now, and addresses like every other CRM record.
|
|
70
70
|
*
|
|
71
|
-
* The two-segment form is
|
|
72
|
-
*
|
|
73
|
-
* and the id
|
|
74
|
-
*
|
|
71
|
+
* The two-segment form is STILL ACCEPTED, and stays accepted past
|
|
72
|
+
* AGL-3277: links were shared, bookmarked and mailed while the old
|
|
73
|
+
* shape was live, and the id they carry is the same person key. What
|
|
74
|
+
* AGL-3277 removed is the storage behind the old shape, not the
|
|
75
|
+
* courtesy of honoring a link somebody already has.
|
|
75
76
|
*/ return record ? !props.hostId && detail.length >= 2 ? /*#__PURE__*/ _jsx(LeadDetailPage, _extends({}, recordProps, {
|
|
76
77
|
id: detail[1]
|
|
77
78
|
})) : /*#__PURE__*/ _jsx(LeadDetailPage, _extends({}, recordProps, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-console-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport type { ReactNode } from 'react'\nimport { CrmOrgMountProvider } from '../hooks/use-crm-org-mount'\nimport CompaniesSection from './companies-section'\nimport CompanyDetailPage from './company-detail-page'\nimport ContactDetailPage from './contact-detail-page'\nimport type { CrmConsoleSectionId } from './crm-console-sections'\nimport DealDetailPage from './deal-detail-page'\nimport DealsSection from './deals-section'\nimport FieldsSection from './fields-section'\nimport LeadDetailPage from './lead-detail-page'\nimport LeadsSection from './leads-section'\nimport ContactsSection from './contacts-section'\nimport ReportsSection from './reports-section'\nimport CrmSettingsSection from './settings-section'\nimport TasksSection from './tasks-section'\n\n/**\n * The body of one Contacts section, built only when that section is the one\n * being read (AGL-2595).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT every section on every render, and the people list alone\n * opens four Firestore listens and an aggregate read on mount. Only the\n * returned branch is ever built.\n *\n * Three sections own a deeper route — a record beneath the list — and read\n * it off `detail`, the segments after the section id. A list section with\n * nothing after it is the list; one segment is the record; the shell has\n * already 404'd anything it could not resolve to this surface.\n */\nfunction sectionBody(\n section: CrmConsoleSectionId,\n props: ConsolePluginPageProps,\n /** The section's OWN segments — `segments[1]` onward. */\n detail: readonly string[],\n basePath: string,\n): ReactNode {\n const record = detail[0]\n const recordProps = {\n hostId: props.hostId,\n org: props.org,\n permissions: props.permissions,\n releaseFlag: props.releaseFlag,\n hostRole: props.hostRole,\n basePath,\n }\n switch (section) {\n case 'contacts':\n return record ? (\n <ContactDetailPage {...recordProps} id={record} />\n ) : (\n <ContactsSection {...props} />\n )\n case 'leads':\n /*\n * ONE SEGMENT, at both levels (AGL-3275).\n *\n * The organization level used to name the site before the id —\n * `leads/{hostId}/{leadId}` (AGL-2630) — because a lead lived under its\n * site and a person key was the same id under each site that had met\n * them, so the id alone could not say which record was meant. A lead is\n * one org row now, and addresses like every other CRM record.\n *\n * The two-segment form is
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-console-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport type { ReactNode } from 'react'\nimport { CrmOrgMountProvider } from '../hooks/use-crm-org-mount'\nimport CompaniesSection from './companies-section'\nimport CompanyDetailPage from './company-detail-page'\nimport ContactDetailPage from './contact-detail-page'\nimport type { CrmConsoleSectionId } from './crm-console-sections'\nimport DealDetailPage from './deal-detail-page'\nimport DealsSection from './deals-section'\nimport FieldsSection from './fields-section'\nimport LeadDetailPage from './lead-detail-page'\nimport LeadsSection from './leads-section'\nimport ContactsSection from './contacts-section'\nimport ReportsSection from './reports-section'\nimport CrmSettingsSection from './settings-section'\nimport TasksSection from './tasks-section'\n\n/**\n * The body of one Contacts section, built only when that section is the one\n * being read (AGL-2595).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT every section on every render, and the people list alone\n * opens four Firestore listens and an aggregate read on mount. Only the\n * returned branch is ever built.\n *\n * Three sections own a deeper route — a record beneath the list — and read\n * it off `detail`, the segments after the section id. A list section with\n * nothing after it is the list; one segment is the record; the shell has\n * already 404'd anything it could not resolve to this surface.\n */\nfunction sectionBody(\n section: CrmConsoleSectionId,\n props: ConsolePluginPageProps,\n /** The section's OWN segments — `segments[1]` onward. */\n detail: readonly string[],\n basePath: string,\n): ReactNode {\n const record = detail[0]\n const recordProps = {\n hostId: props.hostId,\n org: props.org,\n permissions: props.permissions,\n releaseFlag: props.releaseFlag,\n hostRole: props.hostRole,\n basePath,\n }\n switch (section) {\n case 'contacts':\n return record ? (\n <ContactDetailPage {...recordProps} id={record} />\n ) : (\n <ContactsSection {...props} />\n )\n case 'leads':\n /*\n * ONE SEGMENT, at both levels (AGL-3275).\n *\n * The organization level used to name the site before the id —\n * `leads/{hostId}/{leadId}` (AGL-2630) — because a lead lived under its\n * site and a person key was the same id under each site that had met\n * them, so the id alone could not say which record was meant. A lead is\n * one org row now, and addresses like every other CRM record.\n *\n * The two-segment form is STILL ACCEPTED, and stays accepted past\n * AGL-3277: links were shared, bookmarked and mailed while the old\n * shape was live, and the id they carry is the same person key. What\n * AGL-3277 removed is the storage behind the old shape, not the\n * courtesy of honoring a link somebody already has.\n */\n return record ? (\n !props.hostId && detail.length >= 2 ? (\n <LeadDetailPage {...recordProps} id={detail[1]} />\n ) : (\n <LeadDetailPage {...recordProps} id={record} />\n )\n ) : (\n <LeadsSection {...props} />\n )\n case 'companies':\n return record ? (\n <CompanyDetailPage {...recordProps} id={record} />\n ) : (\n <CompaniesSection\n hostId={props.hostId}\n org={props.org}\n basePath={basePath}\n />\n )\n case 'deals':\n return record ? (\n <DealDetailPage {...recordProps} id={record} />\n ) : (\n <DealsSection {...props} />\n )\n case 'tasks':\n return <TasksSection {...props} />\n case 'reports':\n // The shell's props whole, like the contacts list: the reports resolve\n // the org scope from `hostId`, the consent group from `org`, and their\n // drill-down links from `basePath` (AGL-2604).\n return <ReportsSection {...props} />\n case 'fields':\n return <FieldsSection hostId={props.hostId} org={props.org} />\n case 'settings':\n // The org-wide switches (AGL-2613): what the CRM does on its own for\n // every site in the workspace, written to the org document.\n return <CrmSettingsSection hostId={props.hostId} org={props.org} />\n default:\n return null\n }\n}\n\n/**\n * Contacts (AGL-109 → AGL-395 → AGL-2595): the CRM hub — people, companies,\n * deals, tasks, reports and fields — owned by the contacts plugin and\n * rendered by the shell's generic plugin route. The shell applies the\n * `release_crm` gate (via the nav tab) and passes the resolved `org`\n * doc, which the people section reads for the `contactsPerHost` quota.\n *\n * Sections are ROUTES, following the hubs that migrated before it (AGL-2501):\n * `/contacts/people` is the v1 list, and every other section is a URL of its\n * own, so the page builds one section's body and the others do not exist to\n * subscribe. The v1 body moved to `contacts-section.tsx` untouched; what this\n * file adds is the rail and the switch.\n */\nexport function CrmConsolePage(props: ConsolePluginPageProps) {\n const { section, sections, basePath, segments, orgMount } = props\n\n /*\n * Nothing until the URL names a section. The shell redirects a bare\n * `/contacts` to the landing section and holds a spinner while it does, so\n * this state is transient — and rendering the people list here instead\n * would pay for its listens on a URL that is already being replaced.\n */\n if (!section || !sections?.length || !basePath) return null\n\n const hub = (\n <HubSections sections={sections}>\n {sectionBody(\n section as CrmConsoleSectionId,\n props,\n // `segments[0]` IS the section — the shell resolved it into `section`\n // already — so what a section owns is everything after it.\n (segments ?? []).slice(1),\n basePath,\n )}\n </HubSections>\n )\n /*\n * THE ORGANIZATION-LEVEL MOUNT (AGL-2630). The shell hands an `orgMount`\n * only from `/[orgSlug]/crm`, where there is no site: the hub publishes it\n * so every section, record page and drawer beneath resolves its scope from\n * the org rather than from the `null` host it was handed. Under a site\n * there is no provider, and `useCrmOrgMount` answering `null` is how a\n * surface knows it is on one.\n */\n return orgMount ? (\n <CrmOrgMountProvider mount={orgMount}>{hub}</CrmOrgMountProvider>\n ) : (\n hub\n )\n}\nCrmConsolePage.displayName = 'CrmConsolePage'\n\nexport default CrmConsolePage\n"],"names":["HubSections","CrmOrgMountProvider","CompaniesSection","CompanyDetailPage","ContactDetailPage","DealDetailPage","DealsSection","FieldsSection","LeadDetailPage","LeadsSection","ContactsSection","ReportsSection","CrmSettingsSection","TasksSection","sectionBody","section","props","detail","basePath","record","recordProps","hostId","org","permissions","releaseFlag","hostRole","id","length","CrmConsolePage","sections","segments","orgMount","hub","slice","mount","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAGA,SAASA,WAAW,QAAQ,wBAAuB;AAEnD,SAASC,mBAAmB,QAAQ,gCAA4B;AAChE,OAAOC,sBAAsB,yBAAqB;AAClD,OAAOC,uBAAuB,2BAAuB;AACrD,OAAOC,uBAAuB,2BAAuB;AAErD,OAAOC,oBAAoB,wBAAoB;AAC/C,OAAOC,kBAAkB,qBAAiB;AAC1C,OAAOC,mBAAmB,sBAAkB;AAC5C,OAAOC,oBAAoB,wBAAoB;AAC/C,OAAOC,kBAAkB,qBAAiB;AAC1C,OAAOC,qBAAqB,wBAAoB;AAChD,OAAOC,oBAAoB,uBAAmB;AAC9C,OAAOC,wBAAwB,wBAAoB;AACnD,OAAOC,kBAAkB,qBAAiB;AAE1C;;;;;;;;;;;;;CAaC,GACD,SAASC,YACPC,OAA4B,EAC5BC,KAA6B,EAC7B,uDAAuD,GACvDC,MAAyB,EACzBC,QAAgB;IAEhB,MAAMC,SAASF,MAAM,CAAC,EAAE;IACxB,MAAMG,cAAc;QAClBC,QAAQL,MAAMK,MAAM;QACpBC,KAAKN,MAAMM,GAAG;QACdC,aAAaP,MAAMO,WAAW;QAC9BC,aAAaR,MAAMQ,WAAW;QAC9BC,UAAUT,MAAMS,QAAQ;QACxBP;IACF;IACA,OAAQH;QACN,KAAK;YACH,OAAOI,uBACL,KAACf,gCAAsBgB;gBAAaM,IAAIP;gCAExC,KAACT,8BAAoBM;QAEzB,KAAK;YACH;;;;;;;;;;;;;;OAcC,GACD,OAAOG,SACL,CAACH,MAAMK,MAAM,IAAIJ,OAAOU,MAAM,IAAI,kBAChC,KAACnB,6BAAmBY;gBAAaM,IAAIT,MAAM,CAAC,EAAE;gCAE9C,KAACT,6BAAmBY;gBAAaM,IAAIP;gCAGvC,KAACV,2BAAiBO;QAEtB,KAAK;YACH,OAAOG,uBACL,KAAChB,gCAAsBiB;gBAAaM,IAAIP;gCAExC,KAACjB;gBACCmB,QAAQL,MAAMK,MAAM;gBACpBC,KAAKN,MAAMM,GAAG;gBACdJ,UAAUA;;QAGhB,KAAK;YACH,OAAOC,uBACL,KAACd,6BAAmBe;gBAAaM,IAAIP;gCAErC,KAACb,2BAAiBU;QAEtB,KAAK;YACH,qBAAO,KAACH,2BAAiBG;QAC3B,KAAK;YACH,uEAAuE;YACvE,uEAAuE;YACvE,+CAA+C;YAC/C,qBAAO,KAACL,6BAAmBK;QAC7B,KAAK;YACH,qBAAO,KAACT;gBAAcc,QAAQL,MAAMK,MAAM;gBAAEC,KAAKN,MAAMM,GAAG;;QAC5D,KAAK;YACH,qEAAqE;YACrE,4DAA4D;YAC5D,qBAAO,KAACV;gBAAmBS,QAAQL,MAAMK,MAAM;gBAAEC,KAAKN,MAAMM,GAAG;;QACjE;YACE,OAAO;IACX;AACF;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASM,eAAeZ,KAA6B;IAC1D,MAAM,EAAED,OAAO,EAAEc,QAAQ,EAAEX,QAAQ,EAAEY,QAAQ,EAAEC,QAAQ,EAAE,GAAGf;IAE5D;;;;;GAKC,GACD,IAAI,CAACD,WAAW,EAACc,4BAAAA,SAAUF,MAAM,KAAI,CAACT,UAAU,OAAO;IAEvD,MAAMc,oBACJ,KAAChC;QAAY6B,UAAUA;kBACpBf,YACCC,SACAC,OAGA,CAFA,sEAAsE;QACtE,2DAA2D;QAC1Dc,mBAAAA,WAAY,EAAE,EAAEG,KAAK,CAAC,IACvBf;;IAIN;;;;;;;GAOC,GACD,OAAOa,yBACL,KAAC9B;QAAoBiC,OAAOH;kBAAWC;SAEvCA;AAEJ;AACAJ,eAAeO,WAAW,GAAG;AAE7B,eAAeP,eAAc"}
|
|
@@ -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":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,2BAA2B,EAC3BC,uBAAuB,EACvBC,cAAc,EACdC,eAAe,QACV,eAAc;AACrB,OAAOC,oBAEA,wEAAuE;AAC9E,SAASC,WAAW,QAAQ,uBAAsB;AAClD,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAEEC,YAAY,EACZC,kBAAkB,QACb,iCAAgC;AACvC,SAASC,MAAM,EAAEC,KAAK,QAAQ,gBAAe;AAC7C,SAASC,GAAG,EAAEC,eAAe,EAAEC,SAAS,QAAQ,qBAAoB;AACpE,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,WAAW,QAAQ,4BAAwB;AAEpD,0EAA0E,GAC1E,OAAO,MAAMC,6BACX,gFAA+E;AAEjF;;;;;;;;CAQC,GACD,OAAO,SAASC,kBACdC,IAAgD,EAChDC,OAAwC;IAExC,MAAMC,SAAS,IAAIC,IAAIF,QAAQG,GAAG,CAAC,CAACC,SAAW;YAACA,OAAOC,KAAK;YAAED,OAAOE,KAAK;SAAC;IAC3E,OAAOvB,gBAAgBgB,MACpBI,GAAG,CAAC,CAACI,KAAON,OAAOO,GAAG,CAACD,KACvBE,MAAM,CAAC,CAACC,OAAyBC,QAAQD;AAC9C;AA2BA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASE,kBAAkBC,KAA6B;IAC7D,MAAM,EAAEC,MAAM,EAAEC,MAAM,EAAEhB,IAAI,EAAEiB,UAAU,EAAEC,SAAS,EAAEjB,OAAO,EAAEkB,YAAY,EAAEC,OAAO,EAAE,GAAGN;IACxF,0EAA0E;IAC1E,4DAA4D;IAC5D,MAAM,EAAEO,KAAK,EAAE,GAAGxB,YAAY;QAAEkB;IAAO;IACvC,MAAMO,YAAYlC;IAClB,MAAM,EAAEmC,eAAe,EAAE,GAAGpC;IAC5B,MAAMqC,SAASxC,gBAAgBgB;IAC/B,MAAM,CAACyB,UAAUC,YAAY,GAAG9B,SAAmB4B;IACnD,MAAM,CAACG,OAAOC,SAAS,GAAGhC,SAAS;IACnC,MAAM,CAACiC,QAAQC,UAAU,GAAGlC,SAAS;IACrC,wEAAwE;IACxE,wEAAwE;IACxE,4DAA4D;IAC5D,MAAMmC,YAAYP,OAAOQ,IAAI,CAAC;IAC9BrC,UAAU;QACR,IAAI,CAACgC,OAAOD,YAAYF;IACxB,uDAAuD;IACzD,GAAG;QAACO;QAAWJ;KAAM;IAErB,MAAMM,YAAYpD,4BAA4B2C,QAAQC;IACtD,MAAMS,QAAQf,gBAAgB,CAAClB,QAAQkC,MAAM;IAE7C,MAAMC,OAAO;QACX,IAAIH,WAAW;QACf,IAAI,CAACZ,OAAO;YACVE,gBAAgB,yDAAyD;gBACvEc,SAAS;gBACTC,SAAS;YACX;YACA;QACF;QACAR,UAAU;QACV,MAAMS,OAAOzD,wBAAwB2C;QACrC,MAAMe,UAAU,MAAMnD,mBACpB;YAAEoD,SAAS;YAAQvB;YAAWwB,YAAYzB,eAAe;QAAQ,GACjE;YACE,MAAMvB,UAAUF,IAAI8B,WAAW,QAAQD,OAAO,SAASL,SAAS;gBAC9D2B,aAAaJ;gBACbK,WAAWnD;YACb;QACF;QAEFqC,UAAU;QACV,IAAI,CAACU,QAAQK,EAAE,EAAE;gBACCL;YAAhBjB,iBAAgBiB,mBAAAA,QAAQM,OAAO,YAAfN,mBAAmB,kCAAkC;gBAAEH,SAAS;YAAU;YAC1F;QACF;QACAT,SAAS;QACTL,gBAAgB,gBAAgB;YAAEc,SAAS;YAAWC,SAAS;QAAM;QACrElB,2BAAAA,QAAU;YACR2B,OAAOR,KAAK7B,MAAM,CAAC,CAACF,KAAO,CAACgB,OAAOwB,QAAQ,CAACxC;YAC5CyC,SAASzB,OAAOd,MAAM,CAAC,CAACF,KAAO,CAAC+B,KAAKS,QAAQ,CAACxC;QAChD;IACF;IAEA,qBACE,KAACtB;QACCgE,QAAQ;QACRC,MAAMpE,eAAe,YAAY;YAAEqE,QAAQ;QAAgB;QAC3DC,cAAc;QACdC,cAAc;kBAEd,cAAA,MAAC/D;YAAMgE,SAAS;;8BACd,KAACtE;oBACCgB,SAASA;oBACTK,OAAOmB;oBACP+B,UAAU,CAACjB;wBACTb,YAAYa;wBACZX,SAAS;oBACX;oBACArB,OAAM;oBACNkD,YAAY3D;oBACZ4D,UAAU7B;oBACVK,OAAOA;oBACPyB,WAAU;;gBAEXzB,QAAQ,qBACP,KAAC3C;oBAAMqE,WAAU;8BACf,cAAA,KAACtE;wBACCuE,MAAK;wBACLxB,SAAQ;wBACRqB,UAAU7B,UAAUI;wBACpB6B,SAAS,IAAM,KAAK1B,OAAO2B,KAAK,CAAC,CAACC;gCAChCC,QAAQD,KAAK,CAACA;gCACdzC,gBACEyC,iBAAiBE,SAASF,MAAMlB,OAAO,GAAGkB,MAAMlB,OAAO,GAAG,yBAC1D;oCAAET,SAAS;oCAAS8B,gBAAgB;gCAAK;gCAE3CrC,UAAU;4BACZ;kCAECD,SAAS,YAAY;;;;;;AAOpC;AACAhB,kBAAkBuD,WAAW,GAAG;AAEhC,eAAevD,kBAAiB"}
|
|
@@ -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 {
|