@aglyn/plugins-crm 1.0.0-beta.152 → 1.0.0-beta.153
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/lib/components/lead-convert-dialog.js +4 -2
- package/src/lib/components/lead-convert-dialog.js.map +1 -1
- package/src/lib/components/lead-history-card.js +3 -0
- package/src/lib/components/lead-history-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +209 -1
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.d.ts +7 -3
- package/src/lib/components/lead-surfaces-note.js +6 -2
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/leads-section.js +114 -2
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-lead-drawer.d.ts +56 -0
- package/src/lib/components/new-lead-drawer.js +327 -0
- package/src/lib/components/new-lead-drawer.js.map +1 -0
- package/src/lib/components/use-crm-api.d.ts +1 -1
- package/src/lib/components/use-crm-api.js.map +1 -1
- package/src/lib/model/crm-lead-import.d.ts +19 -3
- package/src/lib/model/crm-lead-import.js +159 -6
- package/src/lib/model/crm-lead-import.js.map +1 -1
- package/src/lib/model/lead-company-suggestion.d.ts +18 -3
- package/src/lib/model/lead-company-suggestion.js +41 -11
- package/src/lib/model/lead-company-suggestion.js.map +1 -1
- package/src/lib/server/capture-contact.d.ts +28 -0
- package/src/lib/server/capture-contact.js +139 -0
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/convert-open-lead.d.ts +32 -0
- package/src/lib/server/convert-open-lead.js +91 -0
- package/src/lib/server/convert-open-lead.js.map +1 -0
- package/src/lib/server/lead-create.d.ts +76 -0
- package/src/lib/server/lead-create.js +227 -0
- package/src/lib/server/lead-create.js.map +1 -0
- package/src/lib/server/leads-import.js +7 -2
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server/record-timeline.js +4 -2
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server.js +3 -0
- package/src/lib/server.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.153",
|
|
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.153",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.153",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.153",
|
|
31
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.153",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.153",
|
|
33
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.153",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.153",
|
|
35
|
+
"@aglyn/shared-util-email": "1.0.0-beta.153",
|
|
36
|
+
"@aglyn/shared-util-http": "1.0.0-beta.153",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.153",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.153",
|
|
39
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.153",
|
|
40
40
|
"@dnd-kit/core": "^6.3.1",
|
|
41
41
|
"@dnd-kit/utilities": "^3.2.2",
|
|
42
42
|
"@swc/helpers": "0.5.23",
|
|
@@ -151,7 +151,8 @@ import { LeadOwnerSelect } from "./lead-owner-select.js";
|
|
|
151
151
|
if (!open || companiesStatus !== 'success') return;
|
|
152
152
|
if (seededFor.current === leadId) return;
|
|
153
153
|
seededFor.current = leadId;
|
|
154
|
-
|
|
154
|
+
// The lead's own company text first (AGL-3233), then the domain.
|
|
155
|
+
const suggestion = suggestCompanyForLead(email, companies, lead.company);
|
|
155
156
|
setCompanyMode(suggestion.mode);
|
|
156
157
|
if (suggestion.mode === 'existing') setCompanyId(suggestion.companyId);
|
|
157
158
|
if (suggestion.mode === 'new') {
|
|
@@ -163,7 +164,8 @@ import { LeadOwnerSelect } from "./lead-owner-select.js";
|
|
|
163
164
|
companiesStatus,
|
|
164
165
|
companies,
|
|
165
166
|
email,
|
|
166
|
-
leadId
|
|
167
|
+
leadId,
|
|
168
|
+
lead.company
|
|
167
169
|
]);
|
|
168
170
|
const amountCents = dollarsToCents(dealAmount);
|
|
169
171
|
const amountInvalid = amountCents === undefined;
|
|
@@ -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 const suggestion = suggestCompanyForLead(email, companies)\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])\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","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","company","$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 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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"}
|
|
@@ -35,6 +35,9 @@ import { Chip, Stack, Typography } from "@mui/material";
|
|
|
35
35
|
if (source === 'booking') return 'Booking';
|
|
36
36
|
if (source === 'import') return CONTACT_SOURCE_LABELS.import;
|
|
37
37
|
if (source === 'form') return CONTACT_SOURCE_LABELS.form;
|
|
38
|
+
// A lead entered by hand or over the REST API (AGL-3231).
|
|
39
|
+
if (source === 'manual') return 'Added by hand';
|
|
40
|
+
if (source === 'api') return 'API';
|
|
38
41
|
if (source.startsWith('form:')) return `Form ${source.slice('form:'.length)}`;
|
|
39
42
|
return source;
|
|
40
43
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-history-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 { CAPTURED_BY_HOST_FIELD, CONTACT_SOURCE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\n// The component path and NOT the marketing barrel, for the reason the Inbox\n// and the contacts list give: the barrel is the tenant loader's entry point\n// for the plugin's SITE half, and a console card named there ships to every\n// published page.\nimport {\n CrmRecordAttributionZone,\n useHasCrmRecordAttribution,\n} from './crm-attribution-zone'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Chip, Stack, Typography } from '@mui/material'\n\n/**\n * The surfaces `addHostLead` names — `signup`, `booking`, `form:{formId}`,\n * `import` — as they read on screen. A form's id is not a name, so it is\n * shown as the kind with the id beside it rather than as an opaque token.\n * The bare kind `form` is the lifecycle backfill's spelling for a person\n * whose timeline kept no form id (AGL-2631), and reads as the kind, the\n * way the contact's own source chip does.\n */\nexport function leadSourceLabel(source: string): string {\n if (source === 'signup') return 'Sign-up'\n if (source === 'booking') return 'Booking'\n if (source === 'import') return CONTACT_SOURCE_LABELS.import\n if (source === 'form') return CONTACT_SOURCE_LABELS.form\n if (source.startsWith('form:')) return `Form ${source.slice('form:'.length)}`\n return source\n}\n\n/** Every surface that produced a capture — the array, or the older single field. */\nexport function leadSources(lead: Record<string, unknown>): string[] {\n const sources = lead['sources']\n if (Array.isArray(sources) && sources.length) {\n return sources.map((source) => String(source))\n }\n return typeof lead['source'] === 'string' && lead['source'] ? [lead['source']] : []\n}\n\n/** Epoch millis or a Firestore timestamp, as a local date-time, or a dash. */\nexport function leadTimeLabel(value: unknown): string {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return new Date(value).toLocaleString()\n }\n const asDate = (value as { toDate?: () => Date } | null | undefined)?.toDate?.()\n return asDate ? asDate.toLocaleString() : '—'\n}\n\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 LeadHistoryCardProps {\n hostId: string\n leadId: string\n lead: Record<string, unknown>\n}\n\n/**\n * What the capture door recorded about this person (AGL-2608): every\n * surface that produced a capture, how many times, and when the first and\n * the latest happened — the fields `addHostLead` keeps on the one document\n * per person — plus the campaign the first capture is credited to.\n *\n * Read-only by construction. Nothing here is the team's to edit: it is what\n * the visitor did, and the CRM's working state lives on the card above it.\n */\nexport function LeadHistoryCard(props: LeadHistoryCardProps) {\n const { hostId, leadId, lead } = props\n // The caption introduces what another plugin draws; with none loaded it\n // would introduce nothing.\n const hasAttribution = useHasCrmRecordAttribution()\n const sources = leadSources(lead)\n const capturedBy = lead[CAPTURED_BY_HOST_FIELD]\n const count = Number(lead['submissionCount'] ?? 0) || (sources.length ? 1 : 0)\n return (\n <CardDisplay\n header={'Captured history'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={3}>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={3}>\n <Fact label=\"First seen\">\n {leadTimeLabel(lead['firstSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Last seen\">\n {leadTimeLabel(lead['lastSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Captures\">{count ? String(count) : '—'}</Fact>\n <Fact label=\"Captured on\">\n {Array.isArray(capturedBy) && capturedBy.length\n ? capturedBy.map(String).join(', ')\n : hostId}\n </Fact>\n </Stack>\n <Fact label=\"Sources\">\n {sources.length ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip key={source} size=\"small\" variant=\"outlined\" label={leadSourceLabel(source)} />\n ))}\n </Stack>\n ) : (\n '—'\n )}\n </Fact>\n {hasAttribution ? (\n <Stack spacing={1}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Where this lead came from'}\n </Typography>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"lead\"\n recordId={leadId}\n />\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadHistoryCard.displayName = 'LeadHistoryCard'\n\nexport default LeadHistoryCard\n"],"names":["CAPTURED_BY_HOST_FIELD","CONTACT_SOURCE_LABELS","pluginDocsHelp","CrmRecordAttributionZone","useHasCrmRecordAttribution","CardDisplay","Chip","Stack","Typography","leadSourceLabel","source","import","form","startsWith","slice","length","leadSources","lead","sources","Array","isArray","map","String","leadTimeLabel","value","Number","isFinite","Date","toLocaleString","asDate","toDate","Fact","props","spacing","variant","color","label","component","children","LeadHistoryCard","hostId","leadId","hasAttribution","capturedBy","count","header","help","anchor","contentGutterX","contentGutterY","direction","xs","md","join","sx","flexWrap","rowGap","size","recordKind","recordId","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-history-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 { CAPTURED_BY_HOST_FIELD, CONTACT_SOURCE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\n// The component path and NOT the marketing barrel, for the reason the Inbox\n// and the contacts list give: the barrel is the tenant loader's entry point\n// for the plugin's SITE half, and a console card named there ships to every\n// published page.\nimport {\n CrmRecordAttributionZone,\n useHasCrmRecordAttribution,\n} from './crm-attribution-zone'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Chip, Stack, Typography } from '@mui/material'\n\n/**\n * The surfaces `addHostLead` names — `signup`, `booking`, `form:{formId}`,\n * `import` — as they read on screen. A form's id is not a name, so it is\n * shown as the kind with the id beside it rather than as an opaque token.\n * The bare kind `form` is the lifecycle backfill's spelling for a person\n * whose timeline kept no form id (AGL-2631), and reads as the kind, the\n * way the contact's own source chip does.\n */\nexport function leadSourceLabel(source: string): string {\n if (source === 'signup') return 'Sign-up'\n if (source === 'booking') return 'Booking'\n if (source === 'import') return CONTACT_SOURCE_LABELS.import\n if (source === 'form') return CONTACT_SOURCE_LABELS.form\n // A lead entered by hand or over the REST API (AGL-3231).\n if (source === 'manual') return 'Added by hand'\n if (source === 'api') return 'API'\n if (source.startsWith('form:')) return `Form ${source.slice('form:'.length)}`\n return source\n}\n\n/** Every surface that produced a capture — the array, or the older single field. */\nexport function leadSources(lead: Record<string, unknown>): string[] {\n const sources = lead['sources']\n if (Array.isArray(sources) && sources.length) {\n return sources.map((source) => String(source))\n }\n return typeof lead['source'] === 'string' && lead['source'] ? [lead['source']] : []\n}\n\n/** Epoch millis or a Firestore timestamp, as a local date-time, or a dash. */\nexport function leadTimeLabel(value: unknown): string {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return new Date(value).toLocaleString()\n }\n const asDate = (value as { toDate?: () => Date } | null | undefined)?.toDate?.()\n return asDate ? asDate.toLocaleString() : '—'\n}\n\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 LeadHistoryCardProps {\n hostId: string\n leadId: string\n lead: Record<string, unknown>\n}\n\n/**\n * What the capture door recorded about this person (AGL-2608): every\n * surface that produced a capture, how many times, and when the first and\n * the latest happened — the fields `addHostLead` keeps on the one document\n * per person — plus the campaign the first capture is credited to.\n *\n * Read-only by construction. Nothing here is the team's to edit: it is what\n * the visitor did, and the CRM's working state lives on the card above it.\n */\nexport function LeadHistoryCard(props: LeadHistoryCardProps) {\n const { hostId, leadId, lead } = props\n // The caption introduces what another plugin draws; with none loaded it\n // would introduce nothing.\n const hasAttribution = useHasCrmRecordAttribution()\n const sources = leadSources(lead)\n const capturedBy = lead[CAPTURED_BY_HOST_FIELD]\n const count = Number(lead['submissionCount'] ?? 0) || (sources.length ? 1 : 0)\n return (\n <CardDisplay\n header={'Captured history'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={3}>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={3}>\n <Fact label=\"First seen\">\n {leadTimeLabel(lead['firstSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Last seen\">\n {leadTimeLabel(lead['lastSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Captures\">{count ? String(count) : '—'}</Fact>\n <Fact label=\"Captured on\">\n {Array.isArray(capturedBy) && capturedBy.length\n ? capturedBy.map(String).join(', ')\n : hostId}\n </Fact>\n </Stack>\n <Fact label=\"Sources\">\n {sources.length ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip key={source} size=\"small\" variant=\"outlined\" label={leadSourceLabel(source)} />\n ))}\n </Stack>\n ) : (\n '—'\n )}\n </Fact>\n {hasAttribution ? (\n <Stack spacing={1}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Where this lead came from'}\n </Typography>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"lead\"\n recordId={leadId}\n />\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadHistoryCard.displayName = 'LeadHistoryCard'\n\nexport default LeadHistoryCard\n"],"names":["CAPTURED_BY_HOST_FIELD","CONTACT_SOURCE_LABELS","pluginDocsHelp","CrmRecordAttributionZone","useHasCrmRecordAttribution","CardDisplay","Chip","Stack","Typography","leadSourceLabel","source","import","form","startsWith","slice","length","leadSources","lead","sources","Array","isArray","map","String","leadTimeLabel","value","Number","isFinite","Date","toLocaleString","asDate","toDate","Fact","props","spacing","variant","color","label","component","children","LeadHistoryCard","hostId","leadId","hasAttribution","capturedBy","count","header","help","anchor","contentGutterX","contentGutterY","direction","xs","md","join","sx","flexWrap","rowGap","size","recordKind","recordId","displayName"],"mappings":"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"}
|
|
@@ -25,12 +25,34 @@ import { Alert, Button, FormControl, InputLabel, MenuItem, Select, Stack, TextFi
|
|
|
25
25
|
import { deleteField, doc, serverTimestamp, updateDoc } from "firebase/firestore";
|
|
26
26
|
import { useEffect, useId, useState } from "react";
|
|
27
27
|
import { crmRoutes } from "../model/crm-routes.js";
|
|
28
|
+
import { addressDraftFrom, ContactAddressFields } from "./contact-address-fields.js";
|
|
28
29
|
import { CrmCallButton, CrmPhoneLink } from "./crm-call-actions.js";
|
|
29
30
|
import { CrmRecordChip, CrmRecordHeader } from "./crm-record-header.js";
|
|
30
31
|
import { CrmSendEmailButton } from "./crm-send-email-button.js";
|
|
31
32
|
import { LeadOwnerSelect } from "./lead-owner-select.js";
|
|
32
33
|
import { LeadStatusChip } from "./lead-status-chip.js";
|
|
33
|
-
const NOTES_MAX =
|
|
34
|
+
const NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX;
|
|
35
|
+
const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
36
|
+
/** The stored profile as a draft the fields can edit. */ function profileDraftFrom(lead) {
|
|
37
|
+
var _lead_company, _lead_jobTitle, _ref, _lead_phone, _lead_website, _lead_leadSource, _lead_tags, _lead_address;
|
|
38
|
+
return {
|
|
39
|
+
company: String((_lead_company = lead.company) != null ? _lead_company : ''),
|
|
40
|
+
jobTitle: String((_lead_jobTitle = lead.jobTitle) != null ? _lead_jobTitle : ''),
|
|
41
|
+
phone: String((_ref = (_lead_phone = lead.phone) != null ? _lead_phone : lead['phone']) != null ? _ref : ''),
|
|
42
|
+
website: String((_lead_website = lead.website) != null ? _lead_website : ''),
|
|
43
|
+
leadSource: String((_lead_leadSource = lead.leadSource) != null ? _lead_leadSource : ''),
|
|
44
|
+
tags: ((_lead_tags = lead.tags) != null ? _lead_tags : []).join(', '),
|
|
45
|
+
address: addressDraftFrom((_lead_address = lead.address) != null ? _lead_address : null)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** The patch as the document takes it: a cleared field is deleted, not blanked. */ function profileWrite(patch) {
|
|
49
|
+
const write = {};
|
|
50
|
+
for (const [key, value] of Object.entries(patch)){
|
|
51
|
+
if (value === undefined) continue;
|
|
52
|
+
write[key] = value === null ? deleteField() : value;
|
|
53
|
+
}
|
|
54
|
+
return write;
|
|
55
|
+
}
|
|
34
56
|
/**
|
|
35
57
|
* Why Convert is refused while an erasure waits on the person — the same
|
|
36
58
|
* sentence shape the overflow's items carry, so the two read as one state.
|
|
@@ -82,6 +104,36 @@ const NOTES_MAX = 4000;
|
|
|
82
104
|
const statusLabelId = useId();
|
|
83
105
|
const [notesDirty, setNotesDirty] = useState(false);
|
|
84
106
|
const [savingNotes, setSavingNotes] = useState(false);
|
|
107
|
+
/*
|
|
108
|
+
* THE LEAD'S OWN PROFILE (AGL-3231) — company, title, phone, website,
|
|
109
|
+
* address, tags, lead source — edited as one block with one Save, the
|
|
110
|
+
* way the contact's Properties card saves. Seeded from the document and
|
|
111
|
+
* guarded on save like the notes: a draft edited over a cached read must
|
|
112
|
+
* not overwrite a newer profile with an older one.
|
|
113
|
+
*/ const [profile, setProfile] = useState(()=>profileDraftFrom(lead));
|
|
114
|
+
const [profileDirty, setProfileDirty] = useState(false);
|
|
115
|
+
const [savingProfile, setSavingProfile] = useState(false);
|
|
116
|
+
const [profileErrors, setProfileErrors] = useState({});
|
|
117
|
+
useEffect(()=>{
|
|
118
|
+
if (!profileDirty) setProfile(profileDraftFrom(lead));
|
|
119
|
+
// The draft follows the document until it is edited; the fields are
|
|
120
|
+
// read one by one so a change to any of them reseeds.
|
|
121
|
+
}, [
|
|
122
|
+
profileDirty,
|
|
123
|
+
lead.company,
|
|
124
|
+
lead.jobTitle,
|
|
125
|
+
lead.phone,
|
|
126
|
+
lead.website,
|
|
127
|
+
lead.leadSource,
|
|
128
|
+
lead.tags,
|
|
129
|
+
lead.address
|
|
130
|
+
]);
|
|
131
|
+
const editProfile = (key, value)=>{
|
|
132
|
+
setProfile((current)=>_extends({}, current, {
|
|
133
|
+
[key]: value
|
|
134
|
+
}));
|
|
135
|
+
setProfileDirty(true);
|
|
136
|
+
};
|
|
85
137
|
// A newer note from the server replaces an UNEDITED draft; an edited one is
|
|
86
138
|
// the reader's, and the guard on save decides whether it may land.
|
|
87
139
|
useEffect(()=>{
|
|
@@ -125,6 +177,34 @@ const NOTES_MAX = 4000;
|
|
|
125
177
|
}
|
|
126
178
|
setNotesDirty(false);
|
|
127
179
|
};
|
|
180
|
+
const saveProfile = async ()=>{
|
|
181
|
+
const { patch, errors } = Aglyn.normalizeCrmLeadProfile({
|
|
182
|
+
company: profile.company,
|
|
183
|
+
jobTitle: profile.jobTitle,
|
|
184
|
+
phone: profile.phone,
|
|
185
|
+
website: profile.website,
|
|
186
|
+
leadSource: profile.leadSource,
|
|
187
|
+
tags: profile.tags,
|
|
188
|
+
address: profile.address
|
|
189
|
+
});
|
|
190
|
+
setProfileErrors(errors);
|
|
191
|
+
if (Object.keys(errors).length) return;
|
|
192
|
+
setSavingProfile(true);
|
|
193
|
+
const verdict = await writeGuardedBySeed({
|
|
194
|
+
subject: 'lead',
|
|
195
|
+
fromCache,
|
|
196
|
+
unreadable: leadStatus === 'error'
|
|
197
|
+
}, ()=>write(profileWrite(patch), 'Lead saved'));
|
|
198
|
+
setSavingProfile(false);
|
|
199
|
+
if (!verdict.ok) {
|
|
200
|
+
var _verdict_message;
|
|
201
|
+
enqueueSnackbar((_verdict_message = verdict.message) != null ? _verdict_message : 'The lead could not be saved.', {
|
|
202
|
+
variant: 'warning'
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
setProfileDirty(false);
|
|
207
|
+
};
|
|
128
208
|
const consent = Aglyn.readMarketingBasis(lead, Aglyn.soloConsentGroup(hostId));
|
|
129
209
|
const consentLine = consent.basis === 'granted' ? `Opted in to marketing${consent.basisAtMs ? ` on ${new Date(consent.basisAtMs).toLocaleDateString()}` : ''}` : consent.basis === 'declined' ? 'Declined marketing' : 'No marketing consent recorded — this lead cannot be emailed marketing';
|
|
130
210
|
return /*#__PURE__*/ _jsx(CrmRecordHeader, {
|
|
@@ -349,6 +429,134 @@ const NOTES_MAX = 4000;
|
|
|
349
429
|
})) : null
|
|
350
430
|
]
|
|
351
431
|
}) : null,
|
|
432
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
433
|
+
spacing: 2,
|
|
434
|
+
children: [
|
|
435
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
436
|
+
variant: "subtitle2",
|
|
437
|
+
children: 'Profile'
|
|
438
|
+
}),
|
|
439
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
440
|
+
direction: {
|
|
441
|
+
xs: 'column',
|
|
442
|
+
md: 'row'
|
|
443
|
+
},
|
|
444
|
+
spacing: 2,
|
|
445
|
+
children: [
|
|
446
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
447
|
+
size: "small",
|
|
448
|
+
label: "Company",
|
|
449
|
+
value: profile.company,
|
|
450
|
+
onChange: (event)=>editProfile('company', event.target.value),
|
|
451
|
+
disabled: converted,
|
|
452
|
+
slotProps: {
|
|
453
|
+
htmlInput: {
|
|
454
|
+
maxLength: TEXT_MAX
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
fullWidth: true
|
|
458
|
+
}),
|
|
459
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
460
|
+
size: "small",
|
|
461
|
+
label: "Job title",
|
|
462
|
+
value: profile.jobTitle,
|
|
463
|
+
onChange: (event)=>editProfile('jobTitle', event.target.value),
|
|
464
|
+
disabled: converted,
|
|
465
|
+
slotProps: {
|
|
466
|
+
htmlInput: {
|
|
467
|
+
maxLength: TEXT_MAX
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
fullWidth: true
|
|
471
|
+
})
|
|
472
|
+
]
|
|
473
|
+
}),
|
|
474
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
475
|
+
direction: {
|
|
476
|
+
xs: 'column',
|
|
477
|
+
md: 'row'
|
|
478
|
+
},
|
|
479
|
+
spacing: 2,
|
|
480
|
+
children: [
|
|
481
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
482
|
+
size: "small",
|
|
483
|
+
label: "Phone",
|
|
484
|
+
value: profile.phone,
|
|
485
|
+
onChange: (event)=>editProfile('phone', event.target.value),
|
|
486
|
+
disabled: converted,
|
|
487
|
+
error: Boolean(profileErrors['phone']),
|
|
488
|
+
helperText: profileErrors['phone'] || 'With the country code, like +1 512 555 0107',
|
|
489
|
+
fullWidth: true
|
|
490
|
+
}),
|
|
491
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
492
|
+
size: "small",
|
|
493
|
+
label: "Website",
|
|
494
|
+
value: profile.website,
|
|
495
|
+
onChange: (event)=>editProfile('website', event.target.value),
|
|
496
|
+
disabled: converted,
|
|
497
|
+
error: Boolean(profileErrors['website']),
|
|
498
|
+
helperText: profileErrors['website'] || 'Like acme.com',
|
|
499
|
+
fullWidth: true
|
|
500
|
+
})
|
|
501
|
+
]
|
|
502
|
+
}),
|
|
503
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
504
|
+
direction: {
|
|
505
|
+
xs: 'column',
|
|
506
|
+
md: 'row'
|
|
507
|
+
},
|
|
508
|
+
spacing: 2,
|
|
509
|
+
children: [
|
|
510
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
511
|
+
size: "small",
|
|
512
|
+
label: "Lead source",
|
|
513
|
+
value: profile.leadSource,
|
|
514
|
+
onChange: (event)=>editProfile('leadSource', event.target.value),
|
|
515
|
+
disabled: converted,
|
|
516
|
+
slotProps: {
|
|
517
|
+
htmlInput: {
|
|
518
|
+
maxLength: TEXT_MAX
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
fullWidth: true
|
|
522
|
+
}),
|
|
523
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
524
|
+
size: "small",
|
|
525
|
+
label: "Tags",
|
|
526
|
+
value: profile.tags,
|
|
527
|
+
onChange: (event)=>editProfile('tags', event.target.value),
|
|
528
|
+
disabled: converted,
|
|
529
|
+
helperText: "Comma-separated",
|
|
530
|
+
fullWidth: true
|
|
531
|
+
})
|
|
532
|
+
]
|
|
533
|
+
}),
|
|
534
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
535
|
+
variant: "caption",
|
|
536
|
+
color: "text.secondary",
|
|
537
|
+
children: 'Address'
|
|
538
|
+
}),
|
|
539
|
+
/*#__PURE__*/ _jsx(ContactAddressFields, {
|
|
540
|
+
value: profile.address,
|
|
541
|
+
onChange: (next)=>editProfile('address', next),
|
|
542
|
+
disabled: converted
|
|
543
|
+
}),
|
|
544
|
+
converted ? null : /*#__PURE__*/ _jsx(Stack, {
|
|
545
|
+
direction: "row",
|
|
546
|
+
spacing: 1,
|
|
547
|
+
sx: {
|
|
548
|
+
justifyContent: 'flex-end'
|
|
549
|
+
},
|
|
550
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
551
|
+
size: "small",
|
|
552
|
+
variant: "contained",
|
|
553
|
+
onClick: ()=>void saveProfile(),
|
|
554
|
+
disabled: !profileDirty || savingProfile,
|
|
555
|
+
children: 'Save'
|
|
556
|
+
})
|
|
557
|
+
})
|
|
558
|
+
]
|
|
559
|
+
}),
|
|
352
560
|
/*#__PURE__*/ _jsxs(Stack, {
|
|
353
561
|
spacing: 1,
|
|
354
562
|
children: [
|