@aglyn/plugins-crm 1.0.0-beta.178 → 1.0.0-beta.180
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/lib/components/contact-detail-page.js +1 -0
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-properties-card.d.ts +6 -0
- package/src/lib/components/contact-properties-card.js +25 -2
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/csv-import-drawer.d.ts +7 -0
- package/src/lib/components/csv-import-drawer.js +7 -0
- package/src/lib/components/csv-import-drawer.js.map +1 -1
- package/src/lib/components/fields-section.js +6 -0
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-import-drawer.d.ts +3 -0
- package/src/lib/components/lead-import-drawer.js +29 -6
- package/src/lib/components/lead-import-drawer.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +10 -12
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-source-select.d.ts +30 -0
- package/src/lib/components/lead-source-select.js +96 -0
- package/src/lib/components/lead-source-select.js.map +1 -0
- package/src/lib/components/lead-source-values-card.d.ts +27 -0
- package/src/lib/components/lead-source-values-card.js +606 -0
- package/src/lib/components/lead-source-values-card.js.map +1 -0
- package/src/lib/components/leads-section.js +100 -4
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-lead-drawer.js +27 -11
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/hooks/use-lead-source-picklist.d.ts +23 -0
- package/src/lib/hooks/use-lead-source-picklist.js +50 -0
- package/src/lib/hooks/use-lead-source-picklist.js.map +1 -0
- package/src/lib/model/contact-record.d.ts +2 -0
- package/src/lib/model/contact-record.js +2 -1
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-update.d.ts +6 -0
- package/src/lib/model/contact-update.js +1 -0
- package/src/lib/model/contact-update.js.map +1 -1
- package/src/lib/model/crm-lead-import.d.ts +10 -2
- package/src/lib/model/crm-lead-import.js +19 -1
- package/src/lib/model/crm-lead-import.js.map +1 -1
- package/src/lib/model/crm-record-routes.js +4 -4
- package/src/lib/model/crm-record-routes.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +13 -0
- package/src/lib/model/lead-filters.js +17 -1
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/lead-source-values.d.ts +82 -0
- package/src/lib/model/lead-source-values.js +149 -0
- package/src/lib/model/lead-source-values.js.map +1 -0
- package/src/lib/server/contact-update.js +25 -2
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/lead-create.js +19 -0
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-source-picklist.d.ts +42 -0
- package/src/lib/server/lead-source-picklist.js +52 -0
- package/src/lib/server/lead-source-picklist.js.map +1 -0
- package/src/lib/server/lead-source-values.d.ts +50 -0
- package/src/lib/server/lead-source-values.js +187 -0
- package/src/lib/server/lead-source-values.js.map +1 -0
- package/src/lib/server/leads-import.js +31 -2
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-import-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * IMPORTING A LEADS FILE — the lead vocabulary over the shared drawer\n * (AGL-2701).\n *\n * The server half is `server/leads-import.ts`. What is particular to\n * leads: the address is the one cell every row needs AND the document's\n * id, so a row is merged onto the person the site already holds rather\n * than filed twice; the file may set only the team's working state, since\n * the capture history is the capture door's; and there is no consent\n * column, because a spreadsheet is not a checkbox somebody ticked.\n *\n * ## The site is the whole scope, so the picker is the whole question\n *\n * A lead lives under `hosts/{hostId}/leads` and is private to that site by\n * path. At the organization level there is therefore no site to fall back\n * on and no scope token that could stand in for one, so the shared\n * drawer's site picker is not a convenience here — it is the only thing\n * that says where the file lands, and the import stays held until it is\n * answered.\n */\n\nimport { normalizeContactEmail, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n LEAD_IMPORT_CHUNK_SIZE,\n LEAD_IMPORT_FIELD_LABELS,\n LEAD_IMPORT_FIELDS,\n LEAD_IMPORT_MAX_ROWS,\n LEAD_IMPORT_PREVIEW_ROWS,\n LEAD_IMPORT_SKIP_LABELS,\n type LeadImportField,\n type LeadImportRawRow,\n type LeadImportSkippedRow,\n emptyLeadImportResult,\n guessLeadImportMapping,\n leadImportSkippedCsv,\n mapLeadImportRow,\n mergeLeadImportResults,\n} from '../model/crm-lead-import'\nimport { leadsCsv } from '../model/leads-csv'\nimport {\n CsvImportButton,\n CsvImportDrawer,\n type CsvImportVocabulary,\n} from './csv-import-drawer'\n\n/** The browser-side address of the route one chunk is posted to. */\nexport const LEADS_IMPORT_URL = '/api/crm/leads-import'\n\n/** Built once: nothing in it depends on a render. */\nexport const LEAD_IMPORT_VOCABULARY: CsvImportVocabulary<\n LeadImportField,\n LeadImportRawRow & Record<string, unknown>,\n LeadImportSkippedRow\n> = {\n title: 'Import leads from CSV',\n collection: 'leads',\n help: pluginDocsHelp('crmLeads', { anchor: '#import-from-csv' }),\n sitePickerHelperText:\n 'The site these leads are filed under — a lead is private to one site, ' +\n 'and this is the site that will hold them.',\n intro:\n 'A CSV with a header row. Match its columns to lead fields below, check ' +\n 'the preview, then import. A person this site has already met is ' +\n 'updated rather than added twice. Nobody is emailed, and no marketing ' +\n 'consent is recorded — a file is not a checkbox somebody ticked, so an ' +\n 'imported lead can be mailed only if this site already holds a consent ' +\n `for them. Up to ${LEAD_IMPORT_MAX_ROWS.toLocaleString()} rows per file ` +\n '— split a larger one.',\n fields: LEAD_IMPORT_FIELDS,\n fieldLabels: LEAD_IMPORT_FIELD_LABELS,\n requiredField: 'email',\n requiredWarning:\n 'Choose which column holds the email address. It is the one field every ' +\n 'row needs, and it is what tells one lead from another.',\n unusable: (cell) => !normalizeContactEmail(cell),\n unusableNotice: (count, total) =>\n `${count.toLocaleString()} of ${total.toLocaleString()} rows have no ` +\n 'usable email address and will be skipped. You can download them after ' +\n 'the import.',\n guessMapping: guessLeadImportMapping,\n mapRow: (cells, mapping) =>\n mapLeadImportRow(cells, mapping) as LeadImportRawRow & Record<string, unknown>,\n route: LEADS_IMPORT_URL,\n maxRows: LEAD_IMPORT_MAX_ROWS,\n chunkSize: LEAD_IMPORT_CHUNK_SIZE,\n previewRows: LEAD_IMPORT_PREVIEW_ROWS,\n emptyResult: emptyLeadImportResult,\n mergeResults: mergeLeadImportResults,\n skipLabels: LEAD_IMPORT_SKIP_LABELS,\n skippedCsv: leadImportSkippedCsv,\n skippedFileName: 'skipped-leads.csv',\n // The export's own header over no rows, so an export re-imports as is —\n // the columns a file cannot set land on \"Do not import\".\n templateCsv: () => leadsCsv([]),\n templateFileName: 'leads-template.csv',\n}\n\nexport interface LeadImportDrawerProps {\n open: boolean\n onClose: () => void\n /** The site the file is filed under, or `null` at the organization level. */\n hostId: string | null\n}\n\nexport function LeadImportDrawer(props: LeadImportDrawerProps) {\n const { open, onClose, hostId } = props\n return (\n <CsvImportDrawer\n open={open}\n onClose={onClose}\n hostId={hostId}\n vocabulary={LEAD_IMPORT_VOCABULARY}\n />\n )\n}\nLeadImportDrawer.displayName = 'LeadImportDrawer'\n\n/** The \"Import CSV\" action on the leads list, with the drawer it opens. */\nexport function LeadImportButton(props: { hostId: string | null }) {\n const { hostId } = props\n return (\n <CsvImportButton>\n {(open, onClose) => <LeadImportDrawer open={open} onClose={onClose} hostId={hostId} />}\n </CsvImportButton>\n )\n}\nLeadImportButton.displayName = 'LeadImportButton'\n\nexport default LeadImportDrawer\n"],"names":["normalizeContactEmail","pluginDocsHelp","LEAD_IMPORT_CHUNK_SIZE","LEAD_IMPORT_FIELD_LABELS","LEAD_IMPORT_FIELDS","LEAD_IMPORT_MAX_ROWS","LEAD_IMPORT_PREVIEW_ROWS","LEAD_IMPORT_SKIP_LABELS","emptyLeadImportResult","guessLeadImportMapping","leadImportSkippedCsv","mapLeadImportRow","mergeLeadImportResults","leadsCsv","CsvImportButton","CsvImportDrawer","LEADS_IMPORT_URL","LEAD_IMPORT_VOCABULARY","title","collection","help","anchor","sitePickerHelperText","intro","toLocaleString","fields","fieldLabels","requiredField","requiredWarning","unusable","cell","unusableNotice","count","total","guessMapping","mapRow","cells","mapping","route","maxRows","chunkSize","previewRows","emptyResult","mergeResults","skipLabels","skippedCsv","skippedFileName","templateCsv","templateFileName","LeadImportDrawer","props","open","onClose","hostId","vocabulary","displayName","LeadImportButton"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GAED,SAASA,qBAAqB,EAAEC,cAAc,QAAQ,eAAc;AACpE,SACEC,sBAAsB,EACtBC,wBAAwB,EACxBC,kBAAkB,EAClBC,oBAAoB,EACpBC,wBAAwB,EACxBC,uBAAuB,EAIvBC,qBAAqB,EACrBC,sBAAsB,EACtBC,oBAAoB,EACpBC,gBAAgB,EAChBC,sBAAsB,QACjB,8BAA0B;AACjC,SAASC,QAAQ,QAAQ,wBAAoB;AAC7C,SACEC,eAAe,EACfC,eAAe,QAEV,yBAAqB;AAE5B,kEAAkE,GAClE,OAAO,MAAMC,mBAAmB,wBAAuB;AAEvD,mDAAmD,GACnD,OAAO,MAAMC,yBAIT;IACFC,OAAO;IACPC,YAAY;IACZC,MAAMnB,eAAe,YAAY;QAAEoB,QAAQ;IAAmB;IAC9DC,sBACE,2EACA;IACFC,OACE,4EACA,qEACA,0EACA,2EACA,2EACA,CAAC,gBAAgB,EAAElB,qBAAqBmB,cAAc,GAAG,eAAe,CAAC,GACzE;IACFC,QAAQrB;IACRsB,aAAavB;IACbwB,eAAe;IACfC,iBACE,4EACA;IACFC,UAAU,CAACC,OAAS,CAAC9B,sBAAsB8B;IAC3CC,gBAAgB,CAACC,OAAOC,QACtB,GAAGD,MAAMR,cAAc,GAAG,IAAI,EAAES,MAAMT,cAAc,GAAG,cAAc,CAAC,GACtE,2EACA;IACFU,cAAczB;IACd0B,QAAQ,CAACC,OAAOC,UACd1B,iBAAiByB,OAAOC;IAC1BC,OAAOtB;IACPuB,SAASlC;IACTmC,WAAWtC;IACXuC,aAAanC;IACboC,aAAalC;IACbmC,cAAc/B;IACdgC,YAAYrC;IACZsC,YAAYnC;IACZoC,iBAAiB;IACjB,wEAAwE;IACxE,yDAAyD;IACzDC,aAAa,IAAMlC,SAAS,EAAE;IAC9BmC,kBAAkB;AACpB,EAAC;AASD,OAAO,SAASC,iBAAiBC,KAA4B;IAC3D,MAAM,EAAEC,IAAI,EAAEC,OAAO,EAAEC,MAAM,EAAE,GAAGH;IAClC,qBACE,KAACnC;QACCoC,MAAMA;QACNC,SAASA;QACTC,QAAQA;QACRC,YAAYrC;;AAGlB;AACAgC,iBAAiBM,WAAW,GAAG;AAE/B,yEAAyE,GACzE,OAAO,SAASC,iBAAiBN,KAAgC;IAC/D,MAAM,EAAEG,MAAM,EAAE,GAAGH;IACnB,qBACE,KAACpC;kBACE,CAACqC,MAAMC,wBAAY,KAACH;gBAAiBE,MAAMA;gBAAMC,SAASA;gBAASC,QAAQA;;;AAGlF;AACAG,iBAAiBD,WAAW,GAAG;AAE/B,eAAeN,iBAAgB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-import-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * IMPORTING A LEADS FILE — the lead vocabulary over the shared drawer\n * (AGL-2701).\n *\n * The server half is `server/leads-import.ts`. What is particular to\n * leads: the address is the one cell every row needs AND the document's\n * id, so a row is merged onto the person the site already holds rather\n * than filed twice; the file may set only the team's working state, since\n * the capture history is the capture door's; and there is no consent\n * column, because a spreadsheet is not a checkbox somebody ticked.\n *\n * ## The site is the whole scope, so the picker is the whole question\n *\n * A lead lives under `hosts/{hostId}/leads` and is private to that site by\n * path. At the organization level there is therefore no site to fall back\n * on and no scope token that could stand in for one, so the shared\n * drawer's site picker is not a convenience here — it is the only thing\n * that says where the file lands, and the import stays held until it is\n * answered.\n */\n\nimport { crmLeadSourceRefusal, normalizeContactEmail, pluginDocsHelp } from '@aglyn/aglyn'\nimport { Alert } from '@mui/material'\nimport { useMemo } from 'react'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\nimport {\n LEAD_IMPORT_CHUNK_SIZE,\n LEAD_IMPORT_FIELD_LABELS,\n LEAD_IMPORT_FIELDS,\n LEAD_IMPORT_MAX_ROWS,\n LEAD_IMPORT_PREVIEW_ROWS,\n LEAD_IMPORT_SKIP_LABELS,\n type LeadImportField,\n type LeadImportRawRow,\n type LeadImportSkippedRow,\n emptyLeadImportResult,\n guessLeadImportMapping,\n leadImportSkippedCsv,\n leadImportUnknownLeadSources,\n mapLeadImportRow,\n mergeLeadImportResults,\n} from '../model/crm-lead-import'\nimport { leadsCsv } from '../model/leads-csv'\nimport {\n CsvImportButton,\n CsvImportDrawer,\n type CsvImportVocabulary,\n} from './csv-import-drawer'\n\n/** The browser-side address of the route one chunk is posted to. */\nexport const LEADS_IMPORT_URL = '/api/crm/leads-import'\n\n/** Built once: nothing in it depends on a render. */\nexport const LEAD_IMPORT_VOCABULARY: CsvImportVocabulary<\n LeadImportField,\n LeadImportRawRow & Record<string, unknown>,\n LeadImportSkippedRow\n> = {\n title: 'Import leads from CSV',\n collection: 'leads',\n help: pluginDocsHelp('crmLeads', { anchor: '#import-from-csv' }),\n sitePickerHelperText:\n 'The site these leads are filed under — a lead is private to one site, ' +\n 'and this is the site that will hold them.',\n intro:\n 'A CSV with a header row. Match its columns to lead fields below, check ' +\n 'the preview, then import. A person this site has already met is ' +\n 'updated rather than added twice. Nobody is emailed, and no marketing ' +\n 'consent is recorded — a file is not a checkbox somebody ticked, so an ' +\n 'imported lead can be mailed only if this site already holds a consent ' +\n `for them. Up to ${LEAD_IMPORT_MAX_ROWS.toLocaleString()} rows per file ` +\n '— split a larger one.',\n fields: LEAD_IMPORT_FIELDS,\n fieldLabels: LEAD_IMPORT_FIELD_LABELS,\n requiredField: 'email',\n requiredWarning:\n 'Choose which column holds the email address. It is the one field every ' +\n 'row needs, and it is what tells one lead from another.',\n unusable: (cell) => !normalizeContactEmail(cell),\n unusableNotice: (count, total) =>\n `${count.toLocaleString()} of ${total.toLocaleString()} rows have no ` +\n 'usable email address and will be skipped. You can download them after ' +\n 'the import.',\n guessMapping: guessLeadImportMapping,\n mapRow: (cells, mapping) =>\n mapLeadImportRow(cells, mapping) as LeadImportRawRow & Record<string, unknown>,\n route: LEADS_IMPORT_URL,\n maxRows: LEAD_IMPORT_MAX_ROWS,\n chunkSize: LEAD_IMPORT_CHUNK_SIZE,\n previewRows: LEAD_IMPORT_PREVIEW_ROWS,\n emptyResult: emptyLeadImportResult,\n mergeResults: mergeLeadImportResults,\n skipLabels: LEAD_IMPORT_SKIP_LABELS,\n skippedCsv: leadImportSkippedCsv,\n skippedFileName: 'skipped-leads.csv',\n // The export's own header over no rows, so an export re-imports as is —\n // the columns a file cannot set land on \"Do not import\".\n templateCsv: () => leadsCsv([]),\n templateFileName: 'leads-template.csv',\n}\n\nexport interface LeadImportDrawerProps {\n open: boolean\n onClose: () => void\n /** The site the file is filed under, or `null` at the organization level. */\n hostId: string | null\n /** The org the lead source list is read from (AGL-3298); `null` while it settles. */\n orgId?: string | null\n}\n\nexport function LeadImportDrawer(props: LeadImportDrawerProps) {\n const { open, onClose, hostId, orgId = null } = props\n /*\n * THE ORG'S LEAD SOURCES (AGL-3298). The server refuses a row naming a\n * value outside the list; the notice says so before the file is sent,\n * naming the values it holds that the list does not and the ones it\n * allows, so the operator can fix the file or add the value first.\n */\n const { picklist } = useLeadSourcePicklist(orgId)\n const vocabulary = useMemo(\n () => ({\n ...LEAD_IMPORT_VOCABULARY,\n rowsNotice: (rows: readonly (LeadImportRawRow & Record<string, unknown>)[]) => {\n const unknown = leadImportUnknownLeadSources(rows, picklist)\n if (!unknown.length) return null\n return (\n <Alert severity=\"warning\">\n {`${unknown.length === 1 ? 'A lead source' : `${unknown.length} lead sources`} in this ` +\n `file ${unknown.length === 1 ? 'is' : 'are'} not in your list: ` +\n `${unknown.slice(0, 10).join(', ')}${unknown.length > 10 ? ', …' : ''}. ` +\n 'Those rows will be skipped. ' +\n crmLeadSourceRefusal(picklist) +\n ' Add a value under CRM › Fields › Leads to import it.'}\n </Alert>\n )\n },\n }),\n [picklist],\n )\n return (\n <CsvImportDrawer open={open} onClose={onClose} hostId={hostId} vocabulary={vocabulary} />\n )\n}\nLeadImportDrawer.displayName = 'LeadImportDrawer'\n\n/** The \"Import CSV\" action on the leads list, with the drawer it opens. */\nexport function LeadImportButton(props: { hostId: string | null; orgId?: string | null }) {\n const { hostId, orgId = null } = props\n return (\n <CsvImportButton>\n {(open, onClose) => (\n <LeadImportDrawer open={open} onClose={onClose} hostId={hostId} orgId={orgId} />\n )}\n </CsvImportButton>\n )\n}\nLeadImportButton.displayName = 'LeadImportButton'\n\nexport default LeadImportDrawer\n"],"names":["crmLeadSourceRefusal","normalizeContactEmail","pluginDocsHelp","Alert","useMemo","useLeadSourcePicklist","LEAD_IMPORT_CHUNK_SIZE","LEAD_IMPORT_FIELD_LABELS","LEAD_IMPORT_FIELDS","LEAD_IMPORT_MAX_ROWS","LEAD_IMPORT_PREVIEW_ROWS","LEAD_IMPORT_SKIP_LABELS","emptyLeadImportResult","guessLeadImportMapping","leadImportSkippedCsv","leadImportUnknownLeadSources","mapLeadImportRow","mergeLeadImportResults","leadsCsv","CsvImportButton","CsvImportDrawer","LEADS_IMPORT_URL","LEAD_IMPORT_VOCABULARY","title","collection","help","anchor","sitePickerHelperText","intro","toLocaleString","fields","fieldLabels","requiredField","requiredWarning","unusable","cell","unusableNotice","count","total","guessMapping","mapRow","cells","mapping","route","maxRows","chunkSize","previewRows","emptyResult","mergeResults","skipLabels","skippedCsv","skippedFileName","templateCsv","templateFileName","LeadImportDrawer","props","open","onClose","hostId","orgId","picklist","vocabulary","rowsNotice","rows","unknown","length","severity","slice","join","displayName","LeadImportButton"],"mappings":"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"}
|
|
@@ -25,6 +25,7 @@ import { Alert, Button, FormControl, InputLabel, MenuItem, Select, Stack, TextFi
|
|
|
25
25
|
import { deleteField, doc, serverTimestamp, updateDoc } from "firebase/firestore";
|
|
26
26
|
import { useEffect, useId, useMemo, useState } from "react";
|
|
27
27
|
import { useContactFieldDefinitions } from "../hooks/use-contact-field-definitions.js";
|
|
28
|
+
import { useLeadSourcePicklist } from "../hooks/use-lead-source-picklist.js";
|
|
28
29
|
import { crmCustomDraftChanges, crmCustomDraftMissingRequired, crmCustomDraftValue, crmCustomDraftWrites } from "../model/crm-custom-draft.js";
|
|
29
30
|
import { CrmCustomFieldControl } from "./crm-custom-field-control.js";
|
|
30
31
|
import { crmRoutes } from "../model/crm-routes.js";
|
|
@@ -34,6 +35,7 @@ import { CrmEmailStateChip } from "./crm-email-state-chip.js";
|
|
|
34
35
|
import { CrmRecordChip, CrmRecordHeader } from "./crm-record-header.js";
|
|
35
36
|
import { CrmSendEmailButton } from "./crm-send-email-button.js";
|
|
36
37
|
import { LeadOwnerSelect } from "./lead-owner-select.js";
|
|
38
|
+
import { LeadSourceSelect } from "./lead-source-select.js";
|
|
37
39
|
import { LeadStatusChip } from "./lead-status-chip.js";
|
|
38
40
|
const NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX;
|
|
39
41
|
const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
@@ -92,7 +94,7 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
92
94
|
* Converted leads are read-only here. Their status is the conversion, and
|
|
93
95
|
* the actions become links to what the conversion made.
|
|
94
96
|
*/ export function LeadPropertiesCard(props) {
|
|
95
|
-
var _lead_phone, _lead_notes, _lead_email, _lead_email1, _lead_name;
|
|
97
|
+
var _lead_phone, _lead_notes, _lead_email, _lead_email1, _lead_name, _lead_leadSource;
|
|
96
98
|
const { hostId, consentGroup, orgId, leadId, lead, leadStatus, fromCache, basePath, roster, onConvert, onUnqualify, extraMenuItems = [], banner, extraChips, erasurePending = false, org } = props;
|
|
97
99
|
const firestore = useFirestore();
|
|
98
100
|
const { enqueueSnackbar } = useSnackbar();
|
|
@@ -149,6 +151,8 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
149
151
|
* out every key this card did not show, which is what a retired field's
|
|
150
152
|
* values and an integration's writes sit under.
|
|
151
153
|
*/ const fields = useContactFieldDefinitions(orgId, 'lead');
|
|
154
|
+
// The org's lead source values (AGL-3298), for the select below.
|
|
155
|
+
const leadSources = useLeadSourcePicklist(orgId);
|
|
152
156
|
const storedCustom = useMemo(()=>{
|
|
153
157
|
var _lead_custom;
|
|
154
158
|
return (_lead_custom = lead.custom) != null ? _lead_custom : {};
|
|
@@ -568,18 +572,12 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
568
572
|
},
|
|
569
573
|
spacing: 2,
|
|
570
574
|
children: [
|
|
571
|
-
/*#__PURE__*/ _jsx(
|
|
572
|
-
|
|
573
|
-
label: "Lead source",
|
|
575
|
+
/*#__PURE__*/ _jsx(LeadSourceSelect, {
|
|
576
|
+
picklist: leadSources.picklist,
|
|
574
577
|
value: profile.leadSource,
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
htmlInput: {
|
|
579
|
-
maxLength: TEXT_MAX
|
|
580
|
-
}
|
|
581
|
-
},
|
|
582
|
-
fullWidth: true
|
|
578
|
+
stored: String((_lead_leadSource = lead.leadSource) != null ? _lead_leadSource : ''),
|
|
579
|
+
onChange: (label)=>editProfile('leadSource', label),
|
|
580
|
+
disabled: converted
|
|
583
581
|
}),
|
|
584
582
|
/*#__PURE__*/ _jsx(TextField, {
|
|
585
583
|
size: "small",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-properties-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 * as Aglyn from '@aglyn/aglyn'\nimport type {\n AglynOrgBilling,\n ConsentGroup,\n CrmLeadFields,\n CrmLeadProfilePatch,\n CrmLeadStatus,\n} from '@aglyn/aglyn'\nimport { mdiAccountCancelOutline } from '@aglyn/shared-data-mdi'\nimport { AppLink, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n FormControl,\n InputLabel,\n MenuItem,\n Select,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { deleteField, doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useId, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport {\n crmCustomDraftChanges,\n type CrmCustomDraft,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n addressDraftFrom,\n ContactAddressFields,\n type AddressDraft,\n} from './contact-address-fields'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { LeadStatusChip } from './lead-status-chip'\n\nconst NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX\nconst TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX\n\n/** The profile as the form holds it: every field a string, the address a draft. */\ninterface ProfileDraft {\n company: string\n jobTitle: string\n phone: string\n website: string\n leadSource: string\n tags: string\n address: AddressDraft\n}\n\n/** The stored profile as a draft the fields can edit. */\nfunction profileDraftFrom(lead: Record<string, unknown> & CrmLeadFields): ProfileDraft {\n return {\n company: String(lead.company ?? ''),\n jobTitle: String(lead.jobTitle ?? ''),\n phone: String(lead.phone ?? lead['phone'] ?? ''),\n website: String(lead.website ?? ''),\n leadSource: String(lead.leadSource ?? ''),\n tags: (lead.tags ?? []).join(', '),\n address: addressDraftFrom(lead.address ?? null),\n }\n}\n\n/** The patch as the document takes it: a cleared field is deleted, not blanked. */\nfunction profileWrite(patch: CrmLeadProfilePatch): Record<string, unknown> {\n const write: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n write[key] = value === null ? deleteField() : value\n }\n return write\n}\n\n/**\n * Why Convert is refused while an erasure waits on the person — the same\n * sentence shape the overflow's items carry, so the two read as one state.\n */\nexport const CONVERT_PENDING_ERASURE_REASON = 'An erasure is pending for this person'\n\n/** A label over a value — the record page's one row shape. */\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\nexport interface LeadPropertiesCardProps {\n /**\n * The site this lead is acted on as — the mounted one, or at the\n * organization level the lead's own first capturing site (AGL-3278).\n * `null` for a lead no site has captured: the booking door, the call and\n * the email close, because each of them is one site's to offer.\n */\n hostId: string | null\n /**\n * The group the consent basis is read against (AGL-3278): the site's own\n * declared group, resolved by the page. Not `soloConsentGroup(hostId)`,\n * which is what this card used to spell and which cannot see a refusal\n * recorded against a sibling brand — three sites presenting as one sender\n * are one sender to the person unsubscribing from them.\n */\n consentGroup: ConsentGroup\n /**\n * The org the site belongs to, as the page already resolved it — the\n * custom lead fields are ORG-wide (AGL-3272), and a second lookup here\n * would be one more read per record page for an answer already in hand.\n */\n orgId: string | null\n leadId: string\n lead: Record<string, unknown> & CrmLeadFields\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n basePath: string\n roster: OrgMemberOptions\n onConvert: () => void\n onUnqualify: () => void\n /**\n * Items the page adds to the overflow beside Unqualify — the privacy\n * erasure (AGL-2623) lives on the page, because it needs the workspace\n * role and the API, and this card owns the header it must appear in.\n */\n extraMenuItems?: RowActionsMenuItem[]\n /** What the page shows above the facts — the erasure-pending state. */\n banner?: React.ReactNode\n /**\n * Chips the page adds after the owner — the campaigns the lead is filed\n * under (AGL-3274), named from the containers the page reads once for\n * this header and the Campaigns card below it.\n */\n extraChips?: React.ReactNode\n /**\n * An erasure request is waiting on this person (AGL-2623). Convert stays\n * on the page but is refused with the reason, the way the overflow's items\n * are: a conversion filed now would reach the capture door only to be\n * refused there, and the lead itself goes when the request runs.\n */\n erasurePending?: boolean\n /**\n * The org the shell passed: the booking door reads whether the lead's site\n * runs Bookings and whether the plan is entitled to it (AGL-2660), and a\n * logged call reads the activity scope it belongs in (AGL-2661).\n */\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * What the team knows and decides about a lead: status, owner, notes, and\n * the identity and consent the capture recorded (AGL-2608).\n *\n * Status and owner are single-field client writes — the rules let a site\n * admin, editor or author update `hosts/{hostId}/leads`, and a one-field\n * `update` cannot roll anything else back. Notes are a text field seeded\n * from the document, so that save goes through `writeGuardedBySeed`: a draft\n * edited over a cached read would otherwise overwrite a newer note with an\n * older one plus a sentence.\n *\n * Converted leads are read-only here. Their status is the conversion, and\n * the actions become links to what the conversion made.\n */\nexport function LeadPropertiesCard(props: LeadPropertiesCardProps) {\n const {\n hostId,\n consentGroup,\n orgId,\n leadId,\n lead,\n leadStatus,\n fromCache,\n basePath,\n roster,\n onConvert,\n onUnqualify,\n extraMenuItems = [],\n banner,\n extraChips,\n erasurePending = false,\n org,\n } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const routes = crmRoutes(basePath)\n /*\n * Built at USE, not at render (AGL-3275). A lead is an org row, so the org\n * has to be known to address one — and `orgId` is null while the lookup\n * settles. Composing a ref from that null throws during render, which the\n * page shows as a 500 rather than as a card still loading.\n */\n const refFor = () => (orgId ? doc(firestore, 'orgs', orgId, 'leads', leadId) : null)\n const status = Aglyn.crmLeadStatus(lead)\n const converted = Boolean(lead.convertedContactId)\n const open = Aglyn.isCrmLeadOpen(lead) && !converted\n // The last verdict on the address (AGL-3245), as the platform stamped it.\n const emailState = Aglyn.readEmailState(lead)\n /** What a capture that took a number left on the document (AGL-2661). */\n const leadPhone = String(lead['phone'] ?? '').trim()\n\n const [notes, setNotes] = useState(String(lead.notes ?? ''))\n // The label's id, so the status combobox is named \"Status\" rather than\n // after the status it shows — see `LeadOwnerSelect`.\n const statusLabelId = useId()\n const [notesDirty, setNotesDirty] = useState(false)\n const [savingNotes, setSavingNotes] = useState(false)\n /*\n * THE LEAD'S OWN PROFILE (AGL-3231) — company, title, phone, website,\n * address, tags, lead source — edited as one block with one Save, the\n * way the contact's Properties card saves. Seeded from the document and\n * guarded on save like the notes: a draft edited over a cached read must\n * not overwrite a newer profile with an older one.\n */\n const [profile, setProfile] = useState<ProfileDraft>(() => profileDraftFrom(lead))\n const [profileDirty, setProfileDirty] = useState(false)\n const [savingProfile, setSavingProfile] = useState(false)\n const [profileErrors, setProfileErrors] = useState<Record<string, string>>({})\n useEffect(() => {\n if (!profileDirty) setProfile(profileDraftFrom(lead))\n // The draft follows the document until it is edited; the fields are\n // read one by one so a change to any of them reseeds.\n }, [\n profileDirty,\n lead.company,\n lead.jobTitle,\n lead.phone,\n lead.website,\n lead.leadSource,\n lead.tags,\n lead.address,\n ])\n /*\n * THE ORG'S OWN LEAD FIELDS (AGL-3272), edited under the same Save as\n * the profile: one button over one card, so a person filling a lead in\n * does not have to find two.\n *\n * The draft holds only the keys the reader touched, and Save writes the\n * difference as dotted paths — a `custom` map written whole would take\n * out every key this card did not show, which is what a retired field's\n * values and an integration's writes sit under.\n */\n const fields = useContactFieldDefinitions(orgId, 'lead')\n const storedCustom = useMemo(() => lead.custom ?? {}, [lead.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n const editProfile = <K extends keyof ProfileDraft>(key: K, value: ProfileDraft[K]) => {\n setProfile((current) => ({ ...current, [key]: value }))\n setProfileDirty(true)\n }\n // A newer note from the server replaces an UNEDITED draft; an edited one is\n // the reader's, and the guard on save decides whether it may land.\n useEffect(() => {\n if (!notesDirty) setNotes(String(lead.notes ?? ''))\n }, [lead.notes, notesDirty])\n\n const write = async (fields: Record<string, unknown>, done: string) => {\n try {\n const ref = refFor()\n if (!ref) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n await updateDoc(ref, { ...fields, updatedAt: serverTimestamp() })\n enqueueSnackbar(done, { variant: 'success', persist: false })\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n }\n }\n\n const saveNotes = async () => {\n setSavingNotes(true)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n () => write({ notes: notes.trim().slice(0, NOTES_MAX) }, 'Notes saved'),\n )\n setSavingNotes(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The notes could not be saved.', {\n variant: 'warning',\n })\n return\n }\n setNotesDirty(false)\n }\n\n /*\n * What Save is offered for: a profile field edited, or a custom value\n * that differs from the stored map. Touched-and-put-back does not count\n * — the draft keeps the key either way, and a Save that wrote nothing\n * would still bump `updatedAt`.\n */\n const dirty =\n profileDirty || crmCustomDraftChanges(storedCustom, custom).length > 0\n\n const saveProfile = async () => {\n const { patch, errors } = Aglyn.normalizeCrmLeadProfile({\n company: profile.company,\n jobTitle: profile.jobTitle,\n phone: profile.phone,\n website: profile.website,\n leadSource: profile.leadSource,\n tags: profile.tags,\n address: profile.address,\n })\n setProfileErrors(errors)\n if (Object.keys(errors).length) return\n /*\n * A required field the reader CLEARED is refused; one the lead has\n * always lacked is not this save's to demand, or a field added after\n * the lead was captured would block every later edit to its company.\n */\n const missing = crmCustomDraftMissingRequired(\n fields.active,\n storedCustom,\n custom,\n 'edit',\n )\n if (missing.length) {\n enqueueSnackbar(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`, {\n variant: 'warning',\n })\n return\n }\n setSavingProfile(true)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n () =>\n write(\n { ...profileWrite(patch), ...crmCustomDraftWrites(storedCustom, custom) },\n 'Lead saved',\n ),\n )\n setSavingProfile(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The lead could not be saved.', {\n variant: 'warning',\n })\n return\n }\n setProfileDirty(false)\n // The draft is spent: what it held is stored, and the controls read\n // the document again.\n setCustom({})\n }\n\n const consent = Aglyn.readMarketingBasis(lead, consentGroup)\n const consentLine =\n consent.basis === 'granted'\n ? `Opted in to marketing${\n consent.basisAtMs ? ` on ${new Date(consent.basisAtMs).toLocaleDateString()}` : ''\n }`\n : consent.basis === 'declined'\n ? 'Declined marketing'\n : 'No marketing consent recorded — this lead cannot be emailed marketing'\n\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={String(lead['name'] || lead['email'] || leadId)}\n // The name is the heading; the address is the one line under it,\n // unless the address IS the name, in which case there is no second fact.\n subtitle={lead['name'] ? String(lead['email'] ?? '') : undefined}\n help={Aglyn.pluginDocsHelp('crmLeads', { anchor: '#working-a-lead-from-the-row' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n // The booking door (AGL-2660), while the lead is still the record\n // being worked: once converted, the contact is where a meeting is\n // booked from, and the links below lead there.\n booking={converted ? undefined : { hostId, org, kind: 'lead', recordId: leadId }}\n actions={\n <>\n {converted ? null : erasurePending ? (\n <Tooltip title={CONVERT_PENDING_ERASURE_REASON}>\n {/* A disabled button receives no pointer events, so the\n tooltip anchors on the span around it. */}\n <span>\n <Button size=\"small\" variant=\"contained\" disabled>\n {'Convert'}\n </Button>\n </span>\n </Tooltip>\n ) : (\n <Button size=\"small\" variant=\"contained\" onClick={onConvert}>\n {'Convert'}\n </Button>\n )}\n {/* Dial the number the capture carried, and log the call (AGL-2661). */}\n <CrmCallButton\n hostId={hostId}\n org={org}\n link={{ leadId }}\n phone={leadPhone}\n />\n <CrmSendEmailButton\n hostId={hostId}\n leadId={leadId}\n email={String(lead['email'] ?? '')}\n name={String(lead['name'] ?? '')}\n emailState={emailState}\n />\n </>\n }\n menuItems={[\n ...(open\n ? [\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />,\n destructive: true,\n onClick: onUnqualify,\n } satisfies RowActionsMenuItem,\n ]\n : []),\n ...extraMenuItems,\n ]}\n chips={\n <>\n <LeadStatusChip lead={lead} />\n {/* The verdict on the address (AGL-3245), beside the status: a\n bounce does not move New or Working, but it is the first\n thing a person deciding whether to write must see. */}\n <CrmEmailStateChip state={emailState} />\n <CrmRecordChip\n label=\"Owner\"\n value={lead.ownerUid ? roster.labelFor(lead.ownerUid) : undefined}\n />\n {extraChips}\n </>\n }\n >\n <Stack spacing={3}>\n {banner}\n {/*\n Only when the capture carried one (AGL-2661): the sign-up and\n booking doors write no phone, so a row for every lead would be a\n permanent blank. A form that captures one fills this.\n */}\n {leadPhone ? (\n <Fact label=\"Phone\">\n <CrmPhoneLink phone={leadPhone} />\n </Fact>\n ) : null}\n <Fact label=\"Marketing consent\">{consentLine}</Fact>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n {converted ? (\n <Fact label=\"Status\">\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <LeadStatusChip lead={lead} />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {lead.convertedAtMs\n ? `Converted ${new Date(lead.convertedAtMs).toLocaleString()}`\n : 'Converted'}\n </Typography>\n </Stack>\n </Fact>\n ) : (\n <FormControl size=\"small\" sx={{ minWidth: 200 }}>\n <InputLabel id={statusLabelId}>{'Status'}</InputLabel>\n <Select\n labelId={statusLabelId}\n label=\"Status\"\n value={status === 'unqualified' ? 'unqualified' : status}\n onChange={(event) => {\n const next = String(event.target.value) as CrmLeadStatus\n if (next === 'unqualified') {\n onUnqualify()\n return\n }\n // Reopening drops the reason with the closed state: a lead\n // being worked again is not \"unqualified because …\".\n void write(\n {\n status: next,\n ...(status === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n },\n 'Status updated',\n )\n }}\n >\n <MenuItem value=\"new\">{Aglyn.CRM_LEAD_STATUS_LABELS.new}</MenuItem>\n <MenuItem value=\"working\">{Aglyn.CRM_LEAD_STATUS_LABELS.working}</MenuItem>\n <MenuItem value=\"unqualified\">\n {`${Aglyn.CRM_LEAD_STATUS_LABELS.unqualified}…`}\n </MenuItem>\n </Select>\n </FormControl>\n )}\n <LeadOwnerSelect\n value={lead.ownerUid}\n roster={roster}\n fullWidth={false}\n onChange={(uid) =>\n void write({ ownerUid: uid || deleteField() }, uid ? 'Owner assigned' : 'Owner cleared')\n }\n />\n </Stack>\n {status === 'unqualified' && lead.unqualifiedReason ? (\n <Alert severity=\"info\">{`Unqualified: ${lead.unqualifiedReason}`}</Alert>\n ) : null}\n {converted ? (\n <Stack direction=\"row\" spacing={1} sx={{ flexWrap: 'wrap', rowGap: 1 }}>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.contact(String(lead.convertedContactId))}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open contact'}\n </Button>\n {lead.companyId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.company(lead.companyId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open company'}\n </Button>\n ) : null}\n {lead.dealId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.deal(lead.dealId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open deal'}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n {/*\n The profile (AGL-3231): what Salesforce keeps on a lead and hands\n to the contact and the account on convert. Read-only once\n converted — the contact is the record then, and the links above\n lead there.\n */}\n <Stack spacing={2}>\n <Typography variant=\"subtitle2\">{'Profile'}</Typography>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"Company\"\n value={profile.company}\n onChange={(event) => editProfile('company', event.target.value)}\n disabled={converted}\n slotProps={{ htmlInput: { maxLength: TEXT_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={profile.jobTitle}\n onChange={(event) => editProfile('jobTitle', event.target.value)}\n disabled={converted}\n slotProps={{ htmlInput: { maxLength: TEXT_MAX } }}\n fullWidth\n />\n </Stack>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={profile.phone}\n onChange={(event) => editProfile('phone', event.target.value)}\n disabled={converted}\n error={Boolean(profileErrors['phone'])}\n helperText={profileErrors['phone'] || 'With the country code, like +1 512 555 0107'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Website\"\n value={profile.website}\n onChange={(event) => editProfile('website', event.target.value)}\n disabled={converted}\n error={Boolean(profileErrors['website'])}\n helperText={profileErrors['website'] || 'Like acme.com'}\n fullWidth\n />\n </Stack>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"Lead source\"\n value={profile.leadSource}\n onChange={(event) => editProfile('leadSource', event.target.value)}\n disabled={converted}\n slotProps={{ htmlInput: { maxLength: TEXT_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Tags\"\n value={profile.tags}\n onChange={(event) => editProfile('tags', event.target.value)}\n disabled={converted}\n helperText=\"Comma-separated\"\n fullWidth\n />\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Address'}\n </Typography>\n <ContactAddressFields\n value={profile.address}\n onChange={(next) => editProfile('address', next)}\n disabled={converted}\n />\n {/*\n The org's own lead fields (AGL-3272), under the profile because\n they describe the same record and save with it. A converted\n lead's are read-only with the rest of the card.\n */}\n {fields.active.length ? (\n <>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Custom fields'}\n </Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={converted || savingProfile}\n />\n ))}\n </>\n ) : null}\n {converted ? null : (\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void saveProfile()}\n disabled={!dirty || savingProfile}\n >\n {'Save'}\n </Button>\n </Stack>\n )}\n </Stack>\n <Stack spacing={1}>\n <TextField\n size=\"small\"\n label=\"Notes\"\n value={notes}\n onChange={(event) => {\n setNotes(event.target.value)\n setNotesDirty(true)\n }}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: NOTES_MAX } }}\n />\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void saveNotes()}\n disabled={!notesDirty || savingNotes}\n >\n {'Save notes'}\n </Button>\n </Stack>\n </Stack>\n </Stack>\n </CrmRecordHeader>\n )\n}\nLeadPropertiesCard.displayName = 'LeadPropertiesCard'\n\nexport default LeadPropertiesCard\n"],"names":["Aglyn","mdiAccountCancelOutline","AppLink","MdiIcon","useSnackbar","useFirestore","writeGuardedBySeed","Alert","Button","FormControl","InputLabel","MenuItem","Select","Stack","TextField","Tooltip","Typography","deleteField","doc","serverTimestamp","updateDoc","useEffect","useId","useMemo","useState","useContactFieldDefinitions","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","crmRoutes","addressDraftFrom","ContactAddressFields","CrmCallButton","CrmPhoneLink","CrmEmailStateChip","CrmRecordChip","CrmRecordHeader","CrmSendEmailButton","LeadOwnerSelect","LeadStatusChip","NOTES_MAX","CRM_LEAD_NOTES_MAX","TEXT_MAX","CRM_LEAD_TEXT_MAX","profileDraftFrom","lead","company","String","jobTitle","phone","website","leadSource","tags","join","address","profileWrite","patch","write","key","value","Object","entries","undefined","CONVERT_PENDING_ERASURE_REASON","Fact","props","spacing","variant","color","label","component","children","LeadPropertiesCard","hostId","consentGroup","orgId","leadId","leadStatus","fromCache","basePath","roster","onConvert","onUnqualify","extraMenuItems","banner","extraChips","erasurePending","org","firestore","enqueueSnackbar","routes","refFor","status","crmLeadStatus","converted","Boolean","convertedContactId","open","isCrmLeadOpen","emailState","readEmailState","leadPhone","trim","notes","setNotes","statusLabelId","notesDirty","setNotesDirty","savingNotes","setSavingNotes","profile","setProfile","profileDirty","setProfileDirty","savingProfile","setSavingProfile","profileErrors","setProfileErrors","fields","storedCustom","custom","setCustom","editProfile","current","done","ref","persist","updatedAt","error","Error","message","saveNotes","verdict","subject","unreadable","slice","ok","dirty","length","saveProfile","errors","normalizeCrmLeadProfile","keys","missing","active","consent","readMarketingBasis","consentLine","basis","basisAtMs","Date","toLocaleDateString","kind","title","subtitle","help","pluginDocsHelp","anchor","backHref","section","backLabel","booking","recordId","actions","span","size","disabled","onClick","link","email","name","menuItems","icon","path","destructive","chips","state","ownerUid","labelFor","direction","xs","md","sx","alignItems","convertedAtMs","toLocaleString","minWidth","id","labelId","onChange","event","next","target","unqualifiedReason","CRM_LEAD_STATUS_LABELS","new","working","unqualified","fullWidth","uid","severity","flexWrap","rowGap","componentVariant","nativeButton","href","contact","companyId","dealId","deal","slotProps","htmlInput","maxLength","helperText","map","definition","$id","justifyContent","multiline","minRows","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-properties-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 * as Aglyn from '@aglyn/aglyn'\nimport type {\n AglynOrgBilling,\n ConsentGroup,\n CrmLeadFields,\n CrmLeadProfilePatch,\n CrmLeadStatus,\n} from '@aglyn/aglyn'\nimport { mdiAccountCancelOutline } from '@aglyn/shared-data-mdi'\nimport { AppLink, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n FormControl,\n InputLabel,\n MenuItem,\n Select,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { deleteField, doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useId, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\nimport {\n crmCustomDraftChanges,\n type CrmCustomDraft,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n addressDraftFrom,\n ContactAddressFields,\n type AddressDraft,\n} from './contact-address-fields'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { LeadSourceSelect } from './lead-source-select'\nimport { LeadStatusChip } from './lead-status-chip'\n\nconst NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX\nconst TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX\n\n/** The profile as the form holds it: every field a string, the address a draft. */\ninterface ProfileDraft {\n company: string\n jobTitle: string\n phone: string\n website: string\n leadSource: string\n tags: string\n address: AddressDraft\n}\n\n/** The stored profile as a draft the fields can edit. */\nfunction profileDraftFrom(lead: Record<string, unknown> & CrmLeadFields): ProfileDraft {\n return {\n company: String(lead.company ?? ''),\n jobTitle: String(lead.jobTitle ?? ''),\n phone: String(lead.phone ?? lead['phone'] ?? ''),\n website: String(lead.website ?? ''),\n leadSource: String(lead.leadSource ?? ''),\n tags: (lead.tags ?? []).join(', '),\n address: addressDraftFrom(lead.address ?? null),\n }\n}\n\n/** The patch as the document takes it: a cleared field is deleted, not blanked. */\nfunction profileWrite(patch: CrmLeadProfilePatch): Record<string, unknown> {\n const write: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n write[key] = value === null ? deleteField() : value\n }\n return write\n}\n\n/**\n * Why Convert is refused while an erasure waits on the person — the same\n * sentence shape the overflow's items carry, so the two read as one state.\n */\nexport const CONVERT_PENDING_ERASURE_REASON = 'An erasure is pending for this person'\n\n/** A label over a value — the record page's one row shape. */\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\nexport interface LeadPropertiesCardProps {\n /**\n * The site this lead is acted on as — the mounted one, or at the\n * organization level the lead's own first capturing site (AGL-3278).\n * `null` for a lead no site has captured: the booking door, the call and\n * the email close, because each of them is one site's to offer.\n */\n hostId: string | null\n /**\n * The group the consent basis is read against (AGL-3278): the site's own\n * declared group, resolved by the page. Not `soloConsentGroup(hostId)`,\n * which is what this card used to spell and which cannot see a refusal\n * recorded against a sibling brand — three sites presenting as one sender\n * are one sender to the person unsubscribing from them.\n */\n consentGroup: ConsentGroup\n /**\n * The org the site belongs to, as the page already resolved it — the\n * custom lead fields are ORG-wide (AGL-3272), and a second lookup here\n * would be one more read per record page for an answer already in hand.\n */\n orgId: string | null\n leadId: string\n lead: Record<string, unknown> & CrmLeadFields\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n basePath: string\n roster: OrgMemberOptions\n onConvert: () => void\n onUnqualify: () => void\n /**\n * Items the page adds to the overflow beside Unqualify — the privacy\n * erasure (AGL-2623) lives on the page, because it needs the workspace\n * role and the API, and this card owns the header it must appear in.\n */\n extraMenuItems?: RowActionsMenuItem[]\n /** What the page shows above the facts — the erasure-pending state. */\n banner?: React.ReactNode\n /**\n * Chips the page adds after the owner — the campaigns the lead is filed\n * under (AGL-3274), named from the containers the page reads once for\n * this header and the Campaigns card below it.\n */\n extraChips?: React.ReactNode\n /**\n * An erasure request is waiting on this person (AGL-2623). Convert stays\n * on the page but is refused with the reason, the way the overflow's items\n * are: a conversion filed now would reach the capture door only to be\n * refused there, and the lead itself goes when the request runs.\n */\n erasurePending?: boolean\n /**\n * The org the shell passed: the booking door reads whether the lead's site\n * runs Bookings and whether the plan is entitled to it (AGL-2660), and a\n * logged call reads the activity scope it belongs in (AGL-2661).\n */\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * What the team knows and decides about a lead: status, owner, notes, and\n * the identity and consent the capture recorded (AGL-2608).\n *\n * Status and owner are single-field client writes — the rules let a site\n * admin, editor or author update `hosts/{hostId}/leads`, and a one-field\n * `update` cannot roll anything else back. Notes are a text field seeded\n * from the document, so that save goes through `writeGuardedBySeed`: a draft\n * edited over a cached read would otherwise overwrite a newer note with an\n * older one plus a sentence.\n *\n * Converted leads are read-only here. Their status is the conversion, and\n * the actions become links to what the conversion made.\n */\nexport function LeadPropertiesCard(props: LeadPropertiesCardProps) {\n const {\n hostId,\n consentGroup,\n orgId,\n leadId,\n lead,\n leadStatus,\n fromCache,\n basePath,\n roster,\n onConvert,\n onUnqualify,\n extraMenuItems = [],\n banner,\n extraChips,\n erasurePending = false,\n org,\n } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const routes = crmRoutes(basePath)\n /*\n * Built at USE, not at render (AGL-3275). A lead is an org row, so the org\n * has to be known to address one — and `orgId` is null while the lookup\n * settles. Composing a ref from that null throws during render, which the\n * page shows as a 500 rather than as a card still loading.\n */\n const refFor = () => (orgId ? doc(firestore, 'orgs', orgId, 'leads', leadId) : null)\n const status = Aglyn.crmLeadStatus(lead)\n const converted = Boolean(lead.convertedContactId)\n const open = Aglyn.isCrmLeadOpen(lead) && !converted\n // The last verdict on the address (AGL-3245), as the platform stamped it.\n const emailState = Aglyn.readEmailState(lead)\n /** What a capture that took a number left on the document (AGL-2661). */\n const leadPhone = String(lead['phone'] ?? '').trim()\n\n const [notes, setNotes] = useState(String(lead.notes ?? ''))\n // The label's id, so the status combobox is named \"Status\" rather than\n // after the status it shows — see `LeadOwnerSelect`.\n const statusLabelId = useId()\n const [notesDirty, setNotesDirty] = useState(false)\n const [savingNotes, setSavingNotes] = useState(false)\n /*\n * THE LEAD'S OWN PROFILE (AGL-3231) — company, title, phone, website,\n * address, tags, lead source — edited as one block with one Save, the\n * way the contact's Properties card saves. Seeded from the document and\n * guarded on save like the notes: a draft edited over a cached read must\n * not overwrite a newer profile with an older one.\n */\n const [profile, setProfile] = useState<ProfileDraft>(() => profileDraftFrom(lead))\n const [profileDirty, setProfileDirty] = useState(false)\n const [savingProfile, setSavingProfile] = useState(false)\n const [profileErrors, setProfileErrors] = useState<Record<string, string>>({})\n useEffect(() => {\n if (!profileDirty) setProfile(profileDraftFrom(lead))\n // The draft follows the document until it is edited; the fields are\n // read one by one so a change to any of them reseeds.\n }, [\n profileDirty,\n lead.company,\n lead.jobTitle,\n lead.phone,\n lead.website,\n lead.leadSource,\n lead.tags,\n lead.address,\n ])\n /*\n * THE ORG'S OWN LEAD FIELDS (AGL-3272), edited under the same Save as\n * the profile: one button over one card, so a person filling a lead in\n * does not have to find two.\n *\n * The draft holds only the keys the reader touched, and Save writes the\n * difference as dotted paths — a `custom` map written whole would take\n * out every key this card did not show, which is what a retired field's\n * values and an integration's writes sit under.\n */\n const fields = useContactFieldDefinitions(orgId, 'lead')\n // The org's lead source values (AGL-3298), for the select below.\n const leadSources = useLeadSourcePicklist(orgId)\n const storedCustom = useMemo(() => lead.custom ?? {}, [lead.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n const editProfile = <K extends keyof ProfileDraft>(key: K, value: ProfileDraft[K]) => {\n setProfile((current) => ({ ...current, [key]: value }))\n setProfileDirty(true)\n }\n // A newer note from the server replaces an UNEDITED draft; an edited one is\n // the reader's, and the guard on save decides whether it may land.\n useEffect(() => {\n if (!notesDirty) setNotes(String(lead.notes ?? ''))\n }, [lead.notes, notesDirty])\n\n const write = async (fields: Record<string, unknown>, done: string) => {\n try {\n const ref = refFor()\n if (!ref) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n await updateDoc(ref, { ...fields, updatedAt: serverTimestamp() })\n enqueueSnackbar(done, { variant: 'success', persist: false })\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n }\n }\n\n const saveNotes = async () => {\n setSavingNotes(true)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n () => write({ notes: notes.trim().slice(0, NOTES_MAX) }, 'Notes saved'),\n )\n setSavingNotes(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The notes could not be saved.', {\n variant: 'warning',\n })\n return\n }\n setNotesDirty(false)\n }\n\n /*\n * What Save is offered for: a profile field edited, or a custom value\n * that differs from the stored map. Touched-and-put-back does not count\n * — the draft keeps the key either way, and a Save that wrote nothing\n * would still bump `updatedAt`.\n */\n const dirty =\n profileDirty || crmCustomDraftChanges(storedCustom, custom).length > 0\n\n const saveProfile = async () => {\n const { patch, errors } = Aglyn.normalizeCrmLeadProfile({\n company: profile.company,\n jobTitle: profile.jobTitle,\n phone: profile.phone,\n website: profile.website,\n leadSource: profile.leadSource,\n tags: profile.tags,\n address: profile.address,\n })\n setProfileErrors(errors)\n if (Object.keys(errors).length) return\n /*\n * A required field the reader CLEARED is refused; one the lead has\n * always lacked is not this save's to demand, or a field added after\n * the lead was captured would block every later edit to its company.\n */\n const missing = crmCustomDraftMissingRequired(\n fields.active,\n storedCustom,\n custom,\n 'edit',\n )\n if (missing.length) {\n enqueueSnackbar(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`, {\n variant: 'warning',\n })\n return\n }\n setSavingProfile(true)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n () =>\n write(\n { ...profileWrite(patch), ...crmCustomDraftWrites(storedCustom, custom) },\n 'Lead saved',\n ),\n )\n setSavingProfile(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The lead could not be saved.', {\n variant: 'warning',\n })\n return\n }\n setProfileDirty(false)\n // The draft is spent: what it held is stored, and the controls read\n // the document again.\n setCustom({})\n }\n\n const consent = Aglyn.readMarketingBasis(lead, consentGroup)\n const consentLine =\n consent.basis === 'granted'\n ? `Opted in to marketing${\n consent.basisAtMs ? ` on ${new Date(consent.basisAtMs).toLocaleDateString()}` : ''\n }`\n : consent.basis === 'declined'\n ? 'Declined marketing'\n : 'No marketing consent recorded — this lead cannot be emailed marketing'\n\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={String(lead['name'] || lead['email'] || leadId)}\n // The name is the heading; the address is the one line under it,\n // unless the address IS the name, in which case there is no second fact.\n subtitle={lead['name'] ? String(lead['email'] ?? '') : undefined}\n help={Aglyn.pluginDocsHelp('crmLeads', { anchor: '#working-a-lead-from-the-row' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n // The booking door (AGL-2660), while the lead is still the record\n // being worked: once converted, the contact is where a meeting is\n // booked from, and the links below lead there.\n booking={converted ? undefined : { hostId, org, kind: 'lead', recordId: leadId }}\n actions={\n <>\n {converted ? null : erasurePending ? (\n <Tooltip title={CONVERT_PENDING_ERASURE_REASON}>\n {/* A disabled button receives no pointer events, so the\n tooltip anchors on the span around it. */}\n <span>\n <Button size=\"small\" variant=\"contained\" disabled>\n {'Convert'}\n </Button>\n </span>\n </Tooltip>\n ) : (\n <Button size=\"small\" variant=\"contained\" onClick={onConvert}>\n {'Convert'}\n </Button>\n )}\n {/* Dial the number the capture carried, and log the call (AGL-2661). */}\n <CrmCallButton\n hostId={hostId}\n org={org}\n link={{ leadId }}\n phone={leadPhone}\n />\n <CrmSendEmailButton\n hostId={hostId}\n leadId={leadId}\n email={String(lead['email'] ?? '')}\n name={String(lead['name'] ?? '')}\n emailState={emailState}\n />\n </>\n }\n menuItems={[\n ...(open\n ? [\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />,\n destructive: true,\n onClick: onUnqualify,\n } satisfies RowActionsMenuItem,\n ]\n : []),\n ...extraMenuItems,\n ]}\n chips={\n <>\n <LeadStatusChip lead={lead} />\n {/* The verdict on the address (AGL-3245), beside the status: a\n bounce does not move New or Working, but it is the first\n thing a person deciding whether to write must see. */}\n <CrmEmailStateChip state={emailState} />\n <CrmRecordChip\n label=\"Owner\"\n value={lead.ownerUid ? roster.labelFor(lead.ownerUid) : undefined}\n />\n {extraChips}\n </>\n }\n >\n <Stack spacing={3}>\n {banner}\n {/*\n Only when the capture carried one (AGL-2661): the sign-up and\n booking doors write no phone, so a row for every lead would be a\n permanent blank. A form that captures one fills this.\n */}\n {leadPhone ? (\n <Fact label=\"Phone\">\n <CrmPhoneLink phone={leadPhone} />\n </Fact>\n ) : null}\n <Fact label=\"Marketing consent\">{consentLine}</Fact>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n {converted ? (\n <Fact label=\"Status\">\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <LeadStatusChip lead={lead} />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {lead.convertedAtMs\n ? `Converted ${new Date(lead.convertedAtMs).toLocaleString()}`\n : 'Converted'}\n </Typography>\n </Stack>\n </Fact>\n ) : (\n <FormControl size=\"small\" sx={{ minWidth: 200 }}>\n <InputLabel id={statusLabelId}>{'Status'}</InputLabel>\n <Select\n labelId={statusLabelId}\n label=\"Status\"\n value={status === 'unqualified' ? 'unqualified' : status}\n onChange={(event) => {\n const next = String(event.target.value) as CrmLeadStatus\n if (next === 'unqualified') {\n onUnqualify()\n return\n }\n // Reopening drops the reason with the closed state: a lead\n // being worked again is not \"unqualified because …\".\n void write(\n {\n status: next,\n ...(status === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n },\n 'Status updated',\n )\n }}\n >\n <MenuItem value=\"new\">{Aglyn.CRM_LEAD_STATUS_LABELS.new}</MenuItem>\n <MenuItem value=\"working\">{Aglyn.CRM_LEAD_STATUS_LABELS.working}</MenuItem>\n <MenuItem value=\"unqualified\">\n {`${Aglyn.CRM_LEAD_STATUS_LABELS.unqualified}…`}\n </MenuItem>\n </Select>\n </FormControl>\n )}\n <LeadOwnerSelect\n value={lead.ownerUid}\n roster={roster}\n fullWidth={false}\n onChange={(uid) =>\n void write({ ownerUid: uid || deleteField() }, uid ? 'Owner assigned' : 'Owner cleared')\n }\n />\n </Stack>\n {status === 'unqualified' && lead.unqualifiedReason ? (\n <Alert severity=\"info\">{`Unqualified: ${lead.unqualifiedReason}`}</Alert>\n ) : null}\n {converted ? (\n <Stack direction=\"row\" spacing={1} sx={{ flexWrap: 'wrap', rowGap: 1 }}>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.contact(String(lead.convertedContactId))}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open contact'}\n </Button>\n {lead.companyId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.company(lead.companyId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open company'}\n </Button>\n ) : null}\n {lead.dealId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.deal(lead.dealId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open deal'}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n {/*\n The profile (AGL-3231): what Salesforce keeps on a lead and hands\n to the contact and the account on convert. Read-only once\n converted — the contact is the record then, and the links above\n lead there.\n */}\n <Stack spacing={2}>\n <Typography variant=\"subtitle2\">{'Profile'}</Typography>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"Company\"\n value={profile.company}\n onChange={(event) => editProfile('company', event.target.value)}\n disabled={converted}\n slotProps={{ htmlInput: { maxLength: TEXT_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={profile.jobTitle}\n onChange={(event) => editProfile('jobTitle', event.target.value)}\n disabled={converted}\n slotProps={{ htmlInput: { maxLength: TEXT_MAX } }}\n fullWidth\n />\n </Stack>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={profile.phone}\n onChange={(event) => editProfile('phone', event.target.value)}\n disabled={converted}\n error={Boolean(profileErrors['phone'])}\n helperText={profileErrors['phone'] || 'With the country code, like +1 512 555 0107'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Website\"\n value={profile.website}\n onChange={(event) => editProfile('website', event.target.value)}\n disabled={converted}\n error={Boolean(profileErrors['website'])}\n helperText={profileErrors['website'] || 'Like acme.com'}\n fullWidth\n />\n </Stack>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n {/* The org's own values (AGL-3298); the stored one stays shown when the list no longer offers it. */}\n <LeadSourceSelect\n picklist={leadSources.picklist}\n value={profile.leadSource}\n stored={String(lead.leadSource ?? '')}\n onChange={(label) => editProfile('leadSource', label)}\n disabled={converted}\n />\n <TextField\n size=\"small\"\n label=\"Tags\"\n value={profile.tags}\n onChange={(event) => editProfile('tags', event.target.value)}\n disabled={converted}\n helperText=\"Comma-separated\"\n fullWidth\n />\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Address'}\n </Typography>\n <ContactAddressFields\n value={profile.address}\n onChange={(next) => editProfile('address', next)}\n disabled={converted}\n />\n {/*\n The org's own lead fields (AGL-3272), under the profile because\n they describe the same record and save with it. A converted\n lead's are read-only with the rest of the card.\n */}\n {fields.active.length ? (\n <>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Custom fields'}\n </Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={converted || savingProfile}\n />\n ))}\n </>\n ) : null}\n {converted ? null : (\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void saveProfile()}\n disabled={!dirty || savingProfile}\n >\n {'Save'}\n </Button>\n </Stack>\n )}\n </Stack>\n <Stack spacing={1}>\n <TextField\n size=\"small\"\n label=\"Notes\"\n value={notes}\n onChange={(event) => {\n setNotes(event.target.value)\n setNotesDirty(true)\n }}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: NOTES_MAX } }}\n />\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void saveNotes()}\n disabled={!notesDirty || savingNotes}\n >\n {'Save notes'}\n </Button>\n </Stack>\n </Stack>\n </Stack>\n </CrmRecordHeader>\n )\n}\nLeadPropertiesCard.displayName = 'LeadPropertiesCard'\n\nexport default LeadPropertiesCard\n"],"names":["Aglyn","mdiAccountCancelOutline","AppLink","MdiIcon","useSnackbar","useFirestore","writeGuardedBySeed","Alert","Button","FormControl","InputLabel","MenuItem","Select","Stack","TextField","Tooltip","Typography","deleteField","doc","serverTimestamp","updateDoc","useEffect","useId","useMemo","useState","useContactFieldDefinitions","useLeadSourcePicklist","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","crmRoutes","addressDraftFrom","ContactAddressFields","CrmCallButton","CrmPhoneLink","CrmEmailStateChip","CrmRecordChip","CrmRecordHeader","CrmSendEmailButton","LeadOwnerSelect","LeadSourceSelect","LeadStatusChip","NOTES_MAX","CRM_LEAD_NOTES_MAX","TEXT_MAX","CRM_LEAD_TEXT_MAX","profileDraftFrom","lead","company","String","jobTitle","phone","website","leadSource","tags","join","address","profileWrite","patch","write","key","value","Object","entries","undefined","CONVERT_PENDING_ERASURE_REASON","Fact","props","spacing","variant","color","label","component","children","LeadPropertiesCard","hostId","consentGroup","orgId","leadId","leadStatus","fromCache","basePath","roster","onConvert","onUnqualify","extraMenuItems","banner","extraChips","erasurePending","org","firestore","enqueueSnackbar","routes","refFor","status","crmLeadStatus","converted","Boolean","convertedContactId","open","isCrmLeadOpen","emailState","readEmailState","leadPhone","trim","notes","setNotes","statusLabelId","notesDirty","setNotesDirty","savingNotes","setSavingNotes","profile","setProfile","profileDirty","setProfileDirty","savingProfile","setSavingProfile","profileErrors","setProfileErrors","fields","leadSources","storedCustom","custom","setCustom","editProfile","current","done","ref","persist","updatedAt","error","Error","message","saveNotes","verdict","subject","unreadable","slice","ok","dirty","length","saveProfile","errors","normalizeCrmLeadProfile","keys","missing","active","consent","readMarketingBasis","consentLine","basis","basisAtMs","Date","toLocaleDateString","kind","title","subtitle","help","pluginDocsHelp","anchor","backHref","section","backLabel","booking","recordId","actions","span","size","disabled","onClick","link","email","name","menuItems","icon","path","destructive","chips","state","ownerUid","labelFor","direction","xs","md","sx","alignItems","convertedAtMs","toLocaleString","minWidth","id","labelId","onChange","event","next","target","unqualifiedReason","CRM_LEAD_STATUS_LABELS","new","working","unqualified","fullWidth","uid","severity","flexWrap","rowGap","componentVariant","nativeButton","href","contact","companyId","dealId","deal","slotProps","htmlInput","maxLength","helperText","picklist","stored","map","definition","$id","justifyContent","multiline","minRows","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type CrmPicklist } from '@aglyn/aglyn';
|
|
2
|
+
export interface LeadSourceSelectProps {
|
|
3
|
+
picklist: CrmPicklist;
|
|
4
|
+
/** The label the record holds, or `''` for none. */
|
|
5
|
+
value: string;
|
|
6
|
+
onChange: (label: string) => void;
|
|
7
|
+
/**
|
|
8
|
+
* What the record held when the form opened. Kept in the menu while the
|
|
9
|
+
* list no longer offers it — deactivated, or written before the list
|
|
10
|
+
* existed — so the select shows what is stored until somebody changes it.
|
|
11
|
+
*/
|
|
12
|
+
stored?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
helperText?: string;
|
|
15
|
+
error?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The Lead source select (AGL-3298): the org's active values in the order
|
|
19
|
+
* the Fields page keeps them, a "None" to clear, and the record's own value
|
|
20
|
+
* when the list would not otherwise show it — marked, and selectable only
|
|
21
|
+
* as what it already is.
|
|
22
|
+
*
|
|
23
|
+
* MUI's select `TextField`, the control every CRM form in this plugin uses
|
|
24
|
+
* for a fixed choice, so it sizes, labels and disables like its neighbors.
|
|
25
|
+
*/
|
|
26
|
+
export declare function LeadSourceSelect(props: LeadSourceSelectProps): import("react").JSX.Element;
|
|
27
|
+
export declare namespace LeadSourceSelect {
|
|
28
|
+
var displayName: string;
|
|
29
|
+
}
|
|
30
|
+
export default LeadSourceSelect;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { crmPicklistOptions } from "@aglyn/aglyn";
|
|
19
|
+
import { ListItemText, MenuItem, TextField } from "@mui/material";
|
|
20
|
+
import { useMemo } from "react";
|
|
21
|
+
/** How a kept value that the list does not offer reads in the menu and the field. */ function optionText(label, inactive, unlisted) {
|
|
22
|
+
if (inactive) return `${label} (inactive)`;
|
|
23
|
+
if (unlisted) return `${label} (not in the list)`;
|
|
24
|
+
return label;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The Lead source select (AGL-3298): the org's active values in the order
|
|
28
|
+
* the Fields page keeps them, a "None" to clear, and the record's own value
|
|
29
|
+
* when the list would not otherwise show it — marked, and selectable only
|
|
30
|
+
* as what it already is.
|
|
31
|
+
*
|
|
32
|
+
* MUI's select `TextField`, the control every CRM form in this plugin uses
|
|
33
|
+
* for a fixed choice, so it sizes, labels and disables like its neighbors.
|
|
34
|
+
*/ export function LeadSourceSelect(props) {
|
|
35
|
+
const { picklist, value, onChange, stored, disabled, helperText, error } = props;
|
|
36
|
+
const options = useMemo(()=>crmPicklistOptions(picklist, stored || value), [
|
|
37
|
+
picklist,
|
|
38
|
+
stored,
|
|
39
|
+
value
|
|
40
|
+
]);
|
|
41
|
+
const selected = options.find((option)=>option.label === value);
|
|
42
|
+
return /*#__PURE__*/ _jsxs(TextField, {
|
|
43
|
+
select: true,
|
|
44
|
+
size: "small",
|
|
45
|
+
label: "Lead source",
|
|
46
|
+
value: selected ? value : '',
|
|
47
|
+
onChange: (event)=>onChange(String(event.target.value)),
|
|
48
|
+
disabled: disabled,
|
|
49
|
+
helperText: helperText,
|
|
50
|
+
error: error,
|
|
51
|
+
fullWidth: true,
|
|
52
|
+
slotProps: {
|
|
53
|
+
select: {
|
|
54
|
+
displayEmpty: true,
|
|
55
|
+
renderValue: (current)=>{
|
|
56
|
+
const option = options.find((entry)=>entry.label === current);
|
|
57
|
+
return option ? optionText(option.label, option.inactive, option.unlisted) : 'None';
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
inputLabel: {
|
|
61
|
+
shrink: true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
children: [
|
|
65
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
66
|
+
value: "",
|
|
67
|
+
children: /*#__PURE__*/ _jsx(ListItemText, {
|
|
68
|
+
primary: "None",
|
|
69
|
+
slotProps: {
|
|
70
|
+
primary: {
|
|
71
|
+
color: 'text.secondary'
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
}),
|
|
76
|
+
options.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
77
|
+
value: option.label,
|
|
78
|
+
// A kept value is shown so the record reads true, not offered as a
|
|
79
|
+
// choice for a record that does not hold it already.
|
|
80
|
+
disabled: (option.inactive || option.unlisted) && option.label !== (stored || value),
|
|
81
|
+
children: /*#__PURE__*/ _jsx(ListItemText, {
|
|
82
|
+
primary: optionText(option.label, option.inactive, option.unlisted),
|
|
83
|
+
slotProps: option.inactive || option.unlisted ? {
|
|
84
|
+
primary: {
|
|
85
|
+
color: 'text.secondary'
|
|
86
|
+
}
|
|
87
|
+
} : undefined
|
|
88
|
+
})
|
|
89
|
+
}, option.label))
|
|
90
|
+
]
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
LeadSourceSelect.displayName = 'LeadSourceSelect';
|
|
94
|
+
export default LeadSourceSelect;
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=lead-source-select.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-source-select.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 CrmPicklist, crmPicklistOptions } from '@aglyn/aglyn'\nimport { ListItemText, MenuItem, TextField } from '@mui/material'\nimport { useMemo } from 'react'\n\nexport interface LeadSourceSelectProps {\n picklist: CrmPicklist\n /** The label the record holds, or `''` for none. */\n value: string\n onChange: (label: string) => void\n /**\n * What the record held when the form opened. Kept in the menu while the\n * list no longer offers it — deactivated, or written before the list\n * existed — so the select shows what is stored until somebody changes it.\n */\n stored?: string\n disabled?: boolean\n helperText?: string\n error?: boolean\n}\n\n/** How a kept value that the list does not offer reads in the menu and the field. */\nfunction optionText(label: string, inactive: boolean, unlisted: boolean): string {\n if (inactive) return `${label} (inactive)`\n if (unlisted) return `${label} (not in the list)`\n return label\n}\n\n/**\n * The Lead source select (AGL-3298): the org's active values in the order\n * the Fields page keeps them, a \"None\" to clear, and the record's own value\n * when the list would not otherwise show it — marked, and selectable only\n * as what it already is.\n *\n * MUI's select `TextField`, the control every CRM form in this plugin uses\n * for a fixed choice, so it sizes, labels and disables like its neighbors.\n */\nexport function LeadSourceSelect(props: LeadSourceSelectProps) {\n const { picklist, value, onChange, stored, disabled, helperText, error } = props\n const options = useMemo(\n () => crmPicklistOptions(picklist, stored || value),\n [picklist, stored, value],\n )\n const selected = options.find((option) => option.label === value)\n return (\n <TextField\n select\n size=\"small\"\n label=\"Lead source\"\n value={selected ? value : ''}\n onChange={(event) => onChange(String(event.target.value))}\n disabled={disabled}\n helperText={helperText}\n error={error}\n fullWidth\n slotProps={{\n select: {\n displayEmpty: true,\n renderValue: (current) => {\n const option = options.find((entry) => entry.label === current)\n return option ? optionText(option.label, option.inactive, option.unlisted) : 'None'\n },\n },\n inputLabel: { shrink: true },\n }}\n >\n <MenuItem value=\"\">\n <ListItemText primary=\"None\" slotProps={{ primary: { color: 'text.secondary' } }} />\n </MenuItem>\n {options.map((option) => (\n <MenuItem\n key={option.label}\n value={option.label}\n // A kept value is shown so the record reads true, not offered as a\n // choice for a record that does not hold it already.\n disabled={(option.inactive || option.unlisted) && option.label !== (stored || value)}\n >\n <ListItemText\n primary={optionText(option.label, option.inactive, option.unlisted)}\n slotProps={\n option.inactive || option.unlisted\n ? { primary: { color: 'text.secondary' } }\n : undefined\n }\n />\n </MenuItem>\n ))}\n </TextField>\n )\n}\nLeadSourceSelect.displayName = 'LeadSourceSelect'\n\nexport default LeadSourceSelect\n"],"names":["crmPicklistOptions","ListItemText","MenuItem","TextField","useMemo","optionText","label","inactive","unlisted","LeadSourceSelect","props","picklist","value","onChange","stored","disabled","helperText","error","options","selected","find","option","select","size","event","String","target","fullWidth","slotProps","displayEmpty","renderValue","current","entry","inputLabel","shrink","primary","color","map","undefined","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAA2BA,kBAAkB,QAAQ,eAAc;AACnE,SAASC,YAAY,EAAEC,QAAQ,EAAEC,SAAS,QAAQ,gBAAe;AACjE,SAASC,OAAO,QAAQ,QAAO;AAkB/B,mFAAmF,GACnF,SAASC,WAAWC,KAAa,EAAEC,QAAiB,EAAEC,QAAiB;IACrE,IAAID,UAAU,OAAO,GAAGD,MAAM,WAAW,CAAC;IAC1C,IAAIE,UAAU,OAAO,GAAGF,MAAM,kBAAkB,CAAC;IACjD,OAAOA;AACT;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASG,iBAAiBC,KAA4B;IAC3D,MAAM,EAAEC,QAAQ,EAAEC,KAAK,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,QAAQ,EAAEC,UAAU,EAAEC,KAAK,EAAE,GAAGP;IAC3E,MAAMQ,UAAUd,QACd,IAAMJ,mBAAmBW,UAAUG,UAAUF,QAC7C;QAACD;QAAUG;QAAQF;KAAM;IAE3B,MAAMO,WAAWD,QAAQE,IAAI,CAAC,CAACC,SAAWA,OAAOf,KAAK,KAAKM;IAC3D,qBACE,MAACT;QACCmB,MAAM;QACNC,MAAK;QACLjB,OAAM;QACNM,OAAOO,WAAWP,QAAQ;QAC1BC,UAAU,CAACW,QAAUX,SAASY,OAAOD,MAAME,MAAM,CAACd,KAAK;QACvDG,UAAUA;QACVC,YAAYA;QACZC,OAAOA;QACPU,SAAS;QACTC,WAAW;YACTN,QAAQ;gBACNO,cAAc;gBACdC,aAAa,CAACC;oBACZ,MAAMV,SAASH,QAAQE,IAAI,CAAC,CAACY,QAAUA,MAAM1B,KAAK,KAAKyB;oBACvD,OAAOV,SAAShB,WAAWgB,OAAOf,KAAK,EAAEe,OAAOd,QAAQ,EAAEc,OAAOb,QAAQ,IAAI;gBAC/E;YACF;YACAyB,YAAY;gBAAEC,QAAQ;YAAK;QAC7B;;0BAEA,KAAChC;gBAASU,OAAM;0BACd,cAAA,KAACX;oBAAakC,SAAQ;oBAAOP,WAAW;wBAAEO,SAAS;4BAAEC,OAAO;wBAAiB;oBAAE;;;YAEhFlB,QAAQmB,GAAG,CAAC,CAAChB,uBACZ,KAACnB;oBAECU,OAAOS,OAAOf,KAAK;oBACnB,mEAAmE;oBACnE,qDAAqD;oBACrDS,UAAU,AAACM,CAAAA,OAAOd,QAAQ,IAAIc,OAAOb,QAAQ,AAAD,KAAMa,OAAOf,KAAK,KAAMQ,CAAAA,UAAUF,KAAI;8BAElF,cAAA,KAACX;wBACCkC,SAAS9B,WAAWgB,OAAOf,KAAK,EAAEe,OAAOd,QAAQ,EAAEc,OAAOb,QAAQ;wBAClEoB,WACEP,OAAOd,QAAQ,IAAIc,OAAOb,QAAQ,GAC9B;4BAAE2B,SAAS;gCAAEC,OAAO;4BAAiB;wBAAE,IACvCE;;mBAXHjB,OAAOf,KAAK;;;AAkB3B;AACAG,iBAAiB8B,WAAW,GAAG;AAE/B,eAAe9B,iBAAgB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface LeadSourceValuesCardProps {
|
|
2
|
+
/** The mounted site, or `null` at the organization level. */
|
|
3
|
+
hostId: string | null;
|
|
4
|
+
/** The org whose list this is; `null` while it settles. */
|
|
5
|
+
orgId: string | null;
|
|
6
|
+
/** The site a newly written list records as its definer — the Fields page's own. */
|
|
7
|
+
createHostId: string | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* THE LEAD SOURCE VALUES (AGL-3298) — Salesforce's Lead Source picklist,
|
|
11
|
+
* managed where the org's lead fields are.
|
|
12
|
+
*
|
|
13
|
+
* Every move that touches only the LIST — add, reorder by drag or arrows,
|
|
14
|
+
* sort A–Z, activate, deactivate, default — is one client write of the
|
|
15
|
+
* whole document, guarded against a cached read like every seeded save on
|
|
16
|
+
* the Fields page. The two that change RECORDS — rename, and delete with a
|
|
17
|
+
* replacement — go to `crm/lead-source-values`, which rewrites the list and
|
|
18
|
+
* every lead and contact holding the old label together; see that route.
|
|
19
|
+
*
|
|
20
|
+
* An org that has never edited its list reads the starter set, and the
|
|
21
|
+
* first move here writes it down as the org's own.
|
|
22
|
+
*/
|
|
23
|
+
export declare function LeadSourceValuesCard(props: LeadSourceValuesCardProps): import("react").JSX.Element;
|
|
24
|
+
export declare namespace LeadSourceValuesCard {
|
|
25
|
+
var displayName: string;
|
|
26
|
+
}
|
|
27
|
+
export default LeadSourceValuesCard;
|