@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.
Files changed (38) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/lead-convert-dialog.js +4 -2
  3. package/src/lib/components/lead-convert-dialog.js.map +1 -1
  4. package/src/lib/components/lead-history-card.js +3 -0
  5. package/src/lib/components/lead-history-card.js.map +1 -1
  6. package/src/lib/components/lead-properties-card.js +209 -1
  7. package/src/lib/components/lead-properties-card.js.map +1 -1
  8. package/src/lib/components/lead-surfaces-note.d.ts +7 -3
  9. package/src/lib/components/lead-surfaces-note.js +6 -2
  10. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  11. package/src/lib/components/leads-section.js +114 -2
  12. package/src/lib/components/leads-section.js.map +1 -1
  13. package/src/lib/components/new-lead-drawer.d.ts +56 -0
  14. package/src/lib/components/new-lead-drawer.js +327 -0
  15. package/src/lib/components/new-lead-drawer.js.map +1 -0
  16. package/src/lib/components/use-crm-api.d.ts +1 -1
  17. package/src/lib/components/use-crm-api.js.map +1 -1
  18. package/src/lib/model/crm-lead-import.d.ts +19 -3
  19. package/src/lib/model/crm-lead-import.js +159 -6
  20. package/src/lib/model/crm-lead-import.js.map +1 -1
  21. package/src/lib/model/lead-company-suggestion.d.ts +18 -3
  22. package/src/lib/model/lead-company-suggestion.js +41 -11
  23. package/src/lib/model/lead-company-suggestion.js.map +1 -1
  24. package/src/lib/server/capture-contact.d.ts +28 -0
  25. package/src/lib/server/capture-contact.js +139 -0
  26. package/src/lib/server/capture-contact.js.map +1 -1
  27. package/src/lib/server/convert-open-lead.d.ts +32 -0
  28. package/src/lib/server/convert-open-lead.js +91 -0
  29. package/src/lib/server/convert-open-lead.js.map +1 -0
  30. package/src/lib/server/lead-create.d.ts +76 -0
  31. package/src/lib/server/lead-create.js +227 -0
  32. package/src/lib/server/lead-create.js.map +1 -0
  33. package/src/lib/server/leads-import.js +7 -2
  34. package/src/lib/server/leads-import.js.map +1 -1
  35. package/src/lib/server/record-timeline.js +4 -2
  36. package/src/lib/server/record-timeline.js.map +1 -1
  37. package/src/lib/server.js +3 -0
  38. package/src/lib/server.js.map +1 -1
@@ -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 { AglynOrgBilling, CrmLeadFields, CrmLeadStatus } 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, useState } from 'react'\nimport { crmRoutes } from '../model/crm-routes'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\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 = 4000\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 hostId: string\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 * 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 leadId,\n lead,\n leadStatus,\n fromCache,\n basePath,\n roster,\n onConvert,\n onUnqualify,\n extraMenuItems = [],\n banner,\n erasurePending = false,\n org,\n } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const routes = crmRoutes(basePath)\n const ref = doc(firestore, 'hosts', hostId, 'leads', leadId)\n const status = Aglyn.crmLeadStatus(lead)\n const converted = Boolean(lead.convertedContactId)\n const open = Aglyn.isCrmLeadOpen(lead) && !converted\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 // 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 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 const consent = Aglyn.readMarketingBasis(lead, Aglyn.soloConsentGroup(hostId))\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 />\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 <CrmRecordChip\n label=\"Owner\"\n value={lead.ownerUid ? roster.labelFor(lead.ownerUid) : undefined}\n />\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 <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","useState","crmRoutes","CrmCallButton","CrmPhoneLink","CrmRecordChip","CrmRecordHeader","CrmSendEmailButton","LeadOwnerSelect","LeadStatusChip","NOTES_MAX","CONVERT_PENDING_ERASURE_REASON","Fact","props","spacing","variant","color","label","component","children","LeadPropertiesCard","lead","hostId","leadId","leadStatus","fromCache","basePath","roster","onConvert","onUnqualify","extraMenuItems","banner","erasurePending","org","firestore","enqueueSnackbar","routes","ref","status","crmLeadStatus","converted","Boolean","convertedContactId","open","isCrmLeadOpen","leadPhone","String","trim","notes","setNotes","statusLabelId","notesDirty","setNotesDirty","savingNotes","setSavingNotes","write","fields","done","updatedAt","persist","error","Error","message","saveNotes","verdict","subject","unreadable","slice","ok","consent","readMarketingBasis","soloConsentGroup","consentLine","basis","basisAtMs","Date","toLocaleDateString","kind","title","subtitle","undefined","help","pluginDocsHelp","anchor","backHref","section","backLabel","booking","recordId","actions","span","size","disabled","onClick","link","phone","email","name","menuItems","key","icon","path","destructive","chips","value","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","company","dealId","deal","multiline","minRows","slotProps","htmlInput","maxLength","justifyContent","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 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, useState } from 'react'\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 { 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 hostId: string\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 * 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 leadId,\n lead,\n leadStatus,\n fromCache,\n basePath,\n roster,\n onConvert,\n onUnqualify,\n extraMenuItems = [],\n banner,\n erasurePending = false,\n org,\n } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const routes = crmRoutes(basePath)\n const ref = doc(firestore, 'hosts', hostId, 'leads', leadId)\n const status = Aglyn.crmLeadStatus(lead)\n const converted = Boolean(lead.convertedContactId)\n const open = Aglyn.isCrmLeadOpen(lead) && !converted\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 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 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 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 setSavingProfile(true)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n () => write(profileWrite(patch), 'Lead saved'),\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 }\n\n const consent = Aglyn.readMarketingBasis(lead, Aglyn.soloConsentGroup(hostId))\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 />\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 <CrmRecordChip\n label=\"Owner\"\n value={lead.ownerUid ? roster.labelFor(lead.ownerUid) : undefined}\n />\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 {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={!profileDirty || 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","useState","crmRoutes","addressDraftFrom","ContactAddressFields","CrmCallButton","CrmPhoneLink","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","leadId","leadStatus","fromCache","basePath","roster","onConvert","onUnqualify","extraMenuItems","banner","erasurePending","org","firestore","enqueueSnackbar","routes","ref","status","crmLeadStatus","converted","Boolean","convertedContactId","open","isCrmLeadOpen","leadPhone","trim","notes","setNotes","statusLabelId","notesDirty","setNotesDirty","savingNotes","setSavingNotes","profile","setProfile","profileDirty","setProfileDirty","savingProfile","setSavingProfile","profileErrors","setProfileErrors","editProfile","current","fields","done","updatedAt","persist","error","Error","message","saveNotes","verdict","subject","unreadable","slice","ok","saveProfile","errors","normalizeCrmLeadProfile","keys","length","consent","readMarketingBasis","soloConsentGroup","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","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","justifyContent","multiline","minRows","displayName"],"mappings":"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"}
@@ -20,9 +20,13 @@ export interface LeadSurfacesNoteProps {
20
20
  hostId: string;
21
21
  }
22
22
  /** The always-on surfaces, named once wherever the note is drawn. */
23
- export declare const LEAD_SURFACES_INTRO = "Leads are created by member sign-ups, bookings, and forms with lead routing on";
24
- /** What every capture does whether or not it files a lead. */
25
- export declare const LEAD_SURFACES_CONTACTS_TOO = "Every submission with an email address also updates the contact in Contacts at stage Lead.";
23
+ export declare const LEAD_SURFACES_INTRO = "Leads are created by bookings, forms with lead routing on, imports and New lead";
24
+ /**
25
+ * The rule that keeps one person one record (AGL-3232): a lead is the
26
+ * whole record until it converts, and a person the workspace already holds
27
+ * as a contact is not filed as a lead again.
28
+ */
29
+ export declare const LEAD_SURFACES_CONTACTS_TOO = "A lead is its own record until it converts into a contact. Someone who is already a contact is not filed as a lead again \u2014 their submission lands on the contact.";
26
30
  /** One site's forms, sorted into the two answers the note gives. */
27
31
  export interface LeadSurfaceFormsResult {
28
32
  /** `routing.lead` is on: every submission with an address files a lead. */
@@ -23,8 +23,12 @@ import { Button, Stack, Tooltip, Typography } from "@mui/material";
23
23
  import { collection, doc, limit, orderBy, query, serverTimestamp, updateDoc } from "firebase/firestore";
24
24
  import { useCallback, useMemo, useState } from "react";
25
25
  import { LEAD_SURFACE_FORMS_WINDOW, leadSurfaceForms } from "../model/lead-surfaces.js";
26
- /** The always-on surfaces, named once wherever the note is drawn. */ export const LEAD_SURFACES_INTRO = 'Leads are created by member sign-ups, bookings, and forms with lead routing on';
27
- /** What every capture does whether or not it files a lead. */ export const LEAD_SURFACES_CONTACTS_TOO = 'Every submission with an email address also updates the contact in Contacts at stage Lead.';
26
+ /** The always-on surfaces, named once wherever the note is drawn. */ export const LEAD_SURFACES_INTRO = 'Leads are created by bookings, forms with lead routing on, imports and New lead';
27
+ /**
28
+ * The rule that keeps one person one record (AGL-3232): a lead is the
29
+ * whole record until it converts, and a person the workspace already holds
30
+ * as a contact is not filed as a lead again.
31
+ */ export const LEAD_SURFACES_CONTACTS_TOO = 'A lead is its own record until it converts into a contact. Someone who is already a contact is not filed as a lead again — their submission lands on the contact.';
28
32
  /**
29
33
  * The site's forms, ordered by id and bounded at one past the window — the
30
34
  * same read the Inbox's form picker makes, ordered by `__name__` because a
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-surfaces-note.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\n'use client'\n\nimport { buildRoute, Route } from '@aglyn/aglyn'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack, Tooltip, Typography } from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n updateDoc,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n LEAD_SURFACE_FORMS_WINDOW,\n type LeadSurfaceForm,\n leadSurfaceForms,\n} from '../model/lead-surfaces'\n\nexport interface LeadSurfacesNoteProps {\n hostId: string\n}\n\n/** The always-on surfaces, named once wherever the note is drawn. */\nexport const LEAD_SURFACES_INTRO =\n 'Leads are created by member sign-ups, bookings, and forms with lead routing on'\n/** What every capture does whether or not it files a lead. */\nexport const LEAD_SURFACES_CONTACTS_TOO =\n 'Every submission with an email address also updates the contact in Contacts at stage Lead.'\n\n/** One site's forms, sorted into the two answers the note gives. */\nexport interface LeadSurfaceFormsResult {\n /** `routing.lead` is on: every submission with an address files a lead. */\n routed: LeadSurfaceForm[]\n /** The rest, each with its verdict on whether the switch may be offered. */\n unrouted: LeadSurfaceForm[]\n /** The site has more forms than the window; the lists are not the whole answer. */\n truncated: boolean\n status: ReturnType<typeof useFirestoreCollection>['status']\n}\n\n/**\n * The site's forms, ordered by id and bounded at one past the window — the\n * same read the Inbox's form picker makes, ordered by `__name__` because a\n * form saved without a display name would otherwise fall out of an ordered\n * query and vanish from a list about routing. Host-scoped by path, so any\n * member of the site may read it and no scope predicate is needed.\n *\n * One listener per call, so a note that groups several sites mounts one\n * reader per site it shows and nothing for a site it has not opened.\n */\nexport function useLeadSurfaceForms(hostId: string): LeadSurfaceFormsResult {\n const firestore = useFirestore()\n const { data: formDocs, status } = useFirestoreCollection<\n Record<string, unknown> & { $id: string }\n >(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'forms'),\n orderBy('__name__'),\n limit(LEAD_SURFACE_FORMS_WINDOW + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n return useMemo(() => {\n const forms = leadSurfaceForms(formDocs.slice(0, LEAD_SURFACE_FORMS_WINDOW))\n return {\n routed: forms.filter((form) => form.routed),\n unrouted: forms.filter((form) => !form.routed),\n truncated: formDocs.length > LEAD_SURFACE_FORMS_WINDOW,\n status,\n }\n }, [formDocs, status])\n}\n\n/** The form a switch is mid-flight on — under which site, since a form id is only unique within one. */\nexport interface LeadRoutingTarget {\n hostId: string\n formId: string\n}\n\n/**\n * The switch: one dotted `updateDoc` on `routing.lead`, so a dataset binding\n * beside it is untouched, under the same host-content write the form's page\n * makes. One at a time, whichever site the form is on — two switches in\n * flight would be two toasts racing for the same reader.\n *\n * `siteName` rides into the toast when the note shows more than one site,\n * because \"Contact now files a lead\" is an incomplete sentence to a reader\n * who can see three forms called Contact.\n */\nexport function useTurnOnLeadRouting(): {\n turningOn: LeadRoutingTarget | null\n turnOn: (target: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => Promise<void>\n} {\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const [turningOn, setTurningOn] = useState<LeadRoutingTarget | null>(null)\n const turnOn = useCallback(\n async ({\n hostId,\n form,\n siteName,\n }: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => {\n if (turningOn) return\n setTurningOn({ hostId, formId: form.$id })\n const where = siteName ? ` on ${siteName}` : ''\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', form.$id), {\n 'routing.lead': true,\n updatedAt: serverTimestamp(),\n })\n enqueueSnackbar(\n `\"${form.displayName}\"${where} now files a lead from every submission ` +\n 'that carries an email address.',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar(`Lead routing could not be turned on${where}.`, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setTurningOn(null)\n }\n },\n [turningOn, firestore, enqueueSnackbar],\n )\n return { turningOn, turnOn }\n}\n\n/** A form by name, linked to its own page when the console can address it. */\nexport function LeadSurfaceFormName(props: {\n form: LeadSurfaceForm\n href: string | null\n}) {\n const { form, href } = props\n return href ? (\n <AppLink href={href}>{form.displayName}</AppLink>\n ) : (\n <span>{form.displayName}</span>\n )\n}\nLeadSurfaceFormName.displayName = 'LeadSurfaceFormName'\n\n/** The routed forms as one clause — \"A, B, and possibly more\" — for a sentence to end. */\nexport function LeadSurfaceFormList(props: {\n forms: LeadSurfaceForm[]\n truncated: boolean\n formHref: (formId: string) => string | null\n}) {\n const { forms, truncated, formHref } = props\n return (\n <>\n {forms.map((form, index) => (\n <span key={form.$id}>\n {index ? ', ' : ''}\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </span>\n ))}\n {truncated ? ', and possibly more' : ''}\n </>\n )\n}\nLeadSurfaceFormList.displayName = 'LeadSurfaceFormList'\n\n/**\n * \"Not routing leads:\" — every form that does not route, with the switch\n * beside the ones that could and the reason beside the ones that cannot.\n *\n * Offered only where `leadSurfaceForms` says the publish check would honor\n * it — a form with no email field, or one that records no consent, is\n * refused with the reason as the tooltip, because a switch that flips and\n * then fails at publish is the exact confusion this note exists to remove.\n */\nexport function UnroutedLeadSurfaces(props: {\n hostId: string\n forms: LeadSurfaceForm[]\n formHref: (formId: string) => string | null\n turningOn: LeadRoutingTarget | null\n onTurnOn: (form: LeadSurfaceForm) => void\n}) {\n const { hostId, forms, formHref, turningOn, onTurnOn } = props\n return (\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Not routing leads:'}\n </Typography>\n {forms.map((form) => {\n const pending =\n turningOn?.hostId === hostId && turningOn.formId === form.$id\n return (\n <Stack\n key={form.$id}\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\" component=\"span\">\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </Typography>\n {/*\n Disabled with the reason as its tooltip rather than absent:\n an absent control and an inapplicable one look alike, and\n only one of them says what to do about it.\n */}\n <Tooltip title={form.blocker ?? ''}>\n <span>\n <Button\n size=\"small\"\n variant=\"text\"\n disabled={!form.canRoute || turningOn !== null}\n onClick={() => onTurnOn(form)}\n >\n {pending ? 'Turning on…' : 'Turn on lead routing'}\n </Button>\n </span>\n </Tooltip>\n </Stack>\n )\n })}\n </Stack>\n )\n}\nUnroutedLeadSurfaces.displayName = 'UnroutedLeadSurfaces'\n\n/**\n * WHAT CREATES A LEAD ON THIS SITE (AGL-2612).\n *\n * Every capture lands in Contacts; a LEAD is filed only by a lead surface —\n * a member sign-up, a booking, or a form whose author declared\n * `routing.lead`. The first two are always on and the third is a switch on\n * each form's own page, which left the Leads list unable to say why one\n * form's people were in it and another's were not. This says so, by name,\n * and offers the switch here for the forms that could carry it.\n *\n * The reader, the switch and the two rows are shared with the\n * organization-level note (`OrgLeadSurfacesNote`), which draws them once\n * per site; this is the one-site composition. The form links resolve the\n * site's console route from the host index, because under a site that is\n * the only address the note is handed.\n */\nexport function LeadSurfacesNote(props: LeadSurfacesNoteProps) {\n const { hostId } = props\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const { routed, unrouted, truncated, status } = useLeadSurfaceForms(hostId)\n const { turningOn, turnOn } = useTurnOnLeadRouting()\n\n const formHref = useCallback(\n (formId: string) =>\n orgSlug && host\n ? buildRoute(Route.FORM_DETAILS, { orgSlug, host, formId })\n : null,\n [orgSlug, host],\n )\n\n return (\n <Stack spacing={0.5}>\n <Typography variant=\"body2\" color=\"text.secondary\" component=\"div\">\n {LEAD_SURFACES_INTRO}\n {status === 'loading' ? (\n '…'\n ) : routed.length ? (\n <>\n {': '}\n <LeadSurfaceFormList forms={routed} truncated={truncated} formHref={formHref} />\n {'.'}\n </>\n ) : (\n ' — no form routes leads yet.'\n )}\n {` ${LEAD_SURFACES_CONTACTS_TOO}`}\n </Typography>\n {unrouted.length ? (\n <UnroutedLeadSurfaces\n hostId={hostId}\n forms={unrouted}\n formHref={formHref}\n turningOn={turningOn}\n onTurnOn={(form) => void turnOn({ hostId, form })}\n />\n ) : null}\n </Stack>\n )\n}\nLeadSurfacesNote.displayName = 'LeadSurfacesNote'\n\nexport default LeadSurfacesNote\n"],"names":["buildRoute","Route","AppLink","useSnackbar","useConsoleHostRoute","useFirestore","useFirestoreCollection","Button","Stack","Tooltip","Typography","collection","doc","limit","orderBy","query","serverTimestamp","updateDoc","useCallback","useMemo","useState","LEAD_SURFACE_FORMS_WINDOW","leadSurfaceForms","LEAD_SURFACES_INTRO","LEAD_SURFACES_CONTACTS_TOO","useLeadSurfaceForms","hostId","firestore","data","formDocs","status","idField","forms","slice","routed","filter","form","unrouted","truncated","length","useTurnOnLeadRouting","enqueueSnackbar","turningOn","setTurningOn","turnOn","siteName","formId","$id","where","updatedAt","displayName","variant","persist","error","console","allowDuplicate","LeadSurfaceFormName","props","href","span","LeadSurfaceFormList","formHref","map","index","UnroutedLeadSurfaces","onTurnOn","direction","spacing","sx","alignItems","flexWrap","rowGap","color","pending","component","title","blocker","size","disabled","canRoute","onClick","LeadSurfacesNote","orgSlug","subdomain","host","FORM_DETAILS"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-surfaces-note.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\n'use client'\n\nimport { buildRoute, Route } from '@aglyn/aglyn'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack, Tooltip, Typography } from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n updateDoc,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n LEAD_SURFACE_FORMS_WINDOW,\n type LeadSurfaceForm,\n leadSurfaceForms,\n} from '../model/lead-surfaces'\n\nexport interface LeadSurfacesNoteProps {\n hostId: string\n}\n\n/** The always-on surfaces, named once wherever the note is drawn. */\nexport const LEAD_SURFACES_INTRO =\n 'Leads are created by bookings, forms with lead routing on, imports and New lead'\n/**\n * The rule that keeps one person one record (AGL-3232): a lead is the\n * whole record until it converts, and a person the workspace already holds\n * as a contact is not filed as a lead again.\n */\nexport const LEAD_SURFACES_CONTACTS_TOO =\n 'A lead is its own record until it converts into a contact. Someone who is already a contact is not filed as a lead again — their submission lands on the contact.'\n\n/** One site's forms, sorted into the two answers the note gives. */\nexport interface LeadSurfaceFormsResult {\n /** `routing.lead` is on: every submission with an address files a lead. */\n routed: LeadSurfaceForm[]\n /** The rest, each with its verdict on whether the switch may be offered. */\n unrouted: LeadSurfaceForm[]\n /** The site has more forms than the window; the lists are not the whole answer. */\n truncated: boolean\n status: ReturnType<typeof useFirestoreCollection>['status']\n}\n\n/**\n * The site's forms, ordered by id and bounded at one past the window — the\n * same read the Inbox's form picker makes, ordered by `__name__` because a\n * form saved without a display name would otherwise fall out of an ordered\n * query and vanish from a list about routing. Host-scoped by path, so any\n * member of the site may read it and no scope predicate is needed.\n *\n * One listener per call, so a note that groups several sites mounts one\n * reader per site it shows and nothing for a site it has not opened.\n */\nexport function useLeadSurfaceForms(hostId: string): LeadSurfaceFormsResult {\n const firestore = useFirestore()\n const { data: formDocs, status } = useFirestoreCollection<\n Record<string, unknown> & { $id: string }\n >(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'forms'),\n orderBy('__name__'),\n limit(LEAD_SURFACE_FORMS_WINDOW + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n return useMemo(() => {\n const forms = leadSurfaceForms(formDocs.slice(0, LEAD_SURFACE_FORMS_WINDOW))\n return {\n routed: forms.filter((form) => form.routed),\n unrouted: forms.filter((form) => !form.routed),\n truncated: formDocs.length > LEAD_SURFACE_FORMS_WINDOW,\n status,\n }\n }, [formDocs, status])\n}\n\n/** The form a switch is mid-flight on — under which site, since a form id is only unique within one. */\nexport interface LeadRoutingTarget {\n hostId: string\n formId: string\n}\n\n/**\n * The switch: one dotted `updateDoc` on `routing.lead`, so a dataset binding\n * beside it is untouched, under the same host-content write the form's page\n * makes. One at a time, whichever site the form is on — two switches in\n * flight would be two toasts racing for the same reader.\n *\n * `siteName` rides into the toast when the note shows more than one site,\n * because \"Contact now files a lead\" is an incomplete sentence to a reader\n * who can see three forms called Contact.\n */\nexport function useTurnOnLeadRouting(): {\n turningOn: LeadRoutingTarget | null\n turnOn: (target: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => Promise<void>\n} {\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const [turningOn, setTurningOn] = useState<LeadRoutingTarget | null>(null)\n const turnOn = useCallback(\n async ({\n hostId,\n form,\n siteName,\n }: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => {\n if (turningOn) return\n setTurningOn({ hostId, formId: form.$id })\n const where = siteName ? ` on ${siteName}` : ''\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', form.$id), {\n 'routing.lead': true,\n updatedAt: serverTimestamp(),\n })\n enqueueSnackbar(\n `\"${form.displayName}\"${where} now files a lead from every submission ` +\n 'that carries an email address.',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar(`Lead routing could not be turned on${where}.`, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setTurningOn(null)\n }\n },\n [turningOn, firestore, enqueueSnackbar],\n )\n return { turningOn, turnOn }\n}\n\n/** A form by name, linked to its own page when the console can address it. */\nexport function LeadSurfaceFormName(props: {\n form: LeadSurfaceForm\n href: string | null\n}) {\n const { form, href } = props\n return href ? (\n <AppLink href={href}>{form.displayName}</AppLink>\n ) : (\n <span>{form.displayName}</span>\n )\n}\nLeadSurfaceFormName.displayName = 'LeadSurfaceFormName'\n\n/** The routed forms as one clause — \"A, B, and possibly more\" — for a sentence to end. */\nexport function LeadSurfaceFormList(props: {\n forms: LeadSurfaceForm[]\n truncated: boolean\n formHref: (formId: string) => string | null\n}) {\n const { forms, truncated, formHref } = props\n return (\n <>\n {forms.map((form, index) => (\n <span key={form.$id}>\n {index ? ', ' : ''}\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </span>\n ))}\n {truncated ? ', and possibly more' : ''}\n </>\n )\n}\nLeadSurfaceFormList.displayName = 'LeadSurfaceFormList'\n\n/**\n * \"Not routing leads:\" — every form that does not route, with the switch\n * beside the ones that could and the reason beside the ones that cannot.\n *\n * Offered only where `leadSurfaceForms` says the publish check would honor\n * it — a form with no email field, or one that records no consent, is\n * refused with the reason as the tooltip, because a switch that flips and\n * then fails at publish is the exact confusion this note exists to remove.\n */\nexport function UnroutedLeadSurfaces(props: {\n hostId: string\n forms: LeadSurfaceForm[]\n formHref: (formId: string) => string | null\n turningOn: LeadRoutingTarget | null\n onTurnOn: (form: LeadSurfaceForm) => void\n}) {\n const { hostId, forms, formHref, turningOn, onTurnOn } = props\n return (\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Not routing leads:'}\n </Typography>\n {forms.map((form) => {\n const pending =\n turningOn?.hostId === hostId && turningOn.formId === form.$id\n return (\n <Stack\n key={form.$id}\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\" component=\"span\">\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </Typography>\n {/*\n Disabled with the reason as its tooltip rather than absent:\n an absent control and an inapplicable one look alike, and\n only one of them says what to do about it.\n */}\n <Tooltip title={form.blocker ?? ''}>\n <span>\n <Button\n size=\"small\"\n variant=\"text\"\n disabled={!form.canRoute || turningOn !== null}\n onClick={() => onTurnOn(form)}\n >\n {pending ? 'Turning on…' : 'Turn on lead routing'}\n </Button>\n </span>\n </Tooltip>\n </Stack>\n )\n })}\n </Stack>\n )\n}\nUnroutedLeadSurfaces.displayName = 'UnroutedLeadSurfaces'\n\n/**\n * WHAT CREATES A LEAD ON THIS SITE (AGL-2612).\n *\n * Every capture lands in Contacts; a LEAD is filed only by a lead surface —\n * a member sign-up, a booking, or a form whose author declared\n * `routing.lead`. The first two are always on and the third is a switch on\n * each form's own page, which left the Leads list unable to say why one\n * form's people were in it and another's were not. This says so, by name,\n * and offers the switch here for the forms that could carry it.\n *\n * The reader, the switch and the two rows are shared with the\n * organization-level note (`OrgLeadSurfacesNote`), which draws them once\n * per site; this is the one-site composition. The form links resolve the\n * site's console route from the host index, because under a site that is\n * the only address the note is handed.\n */\nexport function LeadSurfacesNote(props: LeadSurfacesNoteProps) {\n const { hostId } = props\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const { routed, unrouted, truncated, status } = useLeadSurfaceForms(hostId)\n const { turningOn, turnOn } = useTurnOnLeadRouting()\n\n const formHref = useCallback(\n (formId: string) =>\n orgSlug && host\n ? buildRoute(Route.FORM_DETAILS, { orgSlug, host, formId })\n : null,\n [orgSlug, host],\n )\n\n return (\n <Stack spacing={0.5}>\n <Typography variant=\"body2\" color=\"text.secondary\" component=\"div\">\n {LEAD_SURFACES_INTRO}\n {status === 'loading' ? (\n '…'\n ) : routed.length ? (\n <>\n {': '}\n <LeadSurfaceFormList forms={routed} truncated={truncated} formHref={formHref} />\n {'.'}\n </>\n ) : (\n ' — no form routes leads yet.'\n )}\n {` ${LEAD_SURFACES_CONTACTS_TOO}`}\n </Typography>\n {unrouted.length ? (\n <UnroutedLeadSurfaces\n hostId={hostId}\n forms={unrouted}\n formHref={formHref}\n turningOn={turningOn}\n onTurnOn={(form) => void turnOn({ hostId, form })}\n />\n ) : null}\n </Stack>\n )\n}\nLeadSurfacesNote.displayName = 'LeadSurfacesNote'\n\nexport default LeadSurfacesNote\n"],"names":["buildRoute","Route","AppLink","useSnackbar","useConsoleHostRoute","useFirestore","useFirestoreCollection","Button","Stack","Tooltip","Typography","collection","doc","limit","orderBy","query","serverTimestamp","updateDoc","useCallback","useMemo","useState","LEAD_SURFACE_FORMS_WINDOW","leadSurfaceForms","LEAD_SURFACES_INTRO","LEAD_SURFACES_CONTACTS_TOO","useLeadSurfaceForms","hostId","firestore","data","formDocs","status","idField","forms","slice","routed","filter","form","unrouted","truncated","length","useTurnOnLeadRouting","enqueueSnackbar","turningOn","setTurningOn","turnOn","siteName","formId","$id","where","updatedAt","displayName","variant","persist","error","console","allowDuplicate","LeadSurfaceFormName","props","href","span","LeadSurfaceFormList","formHref","map","index","UnroutedLeadSurfaces","onTurnOn","direction","spacing","sx","alignItems","flexWrap","rowGap","color","pending","component","title","blocker","size","disabled","canRoute","onClick","LeadSurfacesNote","orgSlug","subdomain","host","FORM_DETAILS"],"mappings":"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"}
@@ -45,6 +45,8 @@ import { leadsCsv } from "../model/leads-csv.js";
45
45
  import { LeadConvertDialog } from "./lead-convert-dialog.js";
46
46
  import { leadSourceLabel, leadSources, leadTimeLabel } from "./lead-history-card.js";
47
47
  import { LeadImportButton } from "./lead-import-drawer.js";
48
+ import NewLeadDrawer from "./new-lead-drawer.js";
49
+ import { useCrmApi } from "./use-crm-api.js";
48
50
  import { LeadOwnerSelect } from "./lead-owner-select.js";
49
51
  import { CONVERT_PENDING_ERASURE_REASON } from "./lead-properties-card.js";
50
52
  import { LeadStatusChip } from "./lead-status-chip.js";
@@ -96,7 +98,7 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
96
98
  const firestore = useFirestore();
97
99
  const router = useRouter();
98
100
  const { enqueueSnackbar } = useSnackbar();
99
- const { orgId } = useCrmScope({
101
+ const { orgId, createHostId } = useCrmScope({
100
102
  hostId,
101
103
  org
102
104
  });
@@ -214,6 +216,68 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
214
216
  // The row whose conversion dialog is open (AGL-2641) — the same dialog
215
217
  // the lead's page opens, fed the row so the list is one click shorter.
216
218
  const [converting, setConverting] = useState(null);
219
+ /*==========================================
220
+ * NEW LEAD (AGL-3231) — Salesforce's New Lead, in a drawer over the
221
+ * list. The route files the lead through the one lead door under the
222
+ * mounted site, or at the organization level under the site the drawer's
223
+ * picker named; the drawer holds its submit until one is known. It makes
224
+ * a lead and nothing else — the conversion is what makes the contact.
225
+ *=========================================*/ const crmApi = useCrmApi(createHostId);
226
+ const [createOpen, setCreateOpen] = useState(false);
227
+ const [createBusy, setCreateBusy] = useState(false);
228
+ const [createError, setCreateError] = useState(null);
229
+ const handleCreate = useCallback(async (values)=>{
230
+ setCreateBusy(true);
231
+ setCreateError(null);
232
+ try {
233
+ const { response, payload } = await crmApi('leads-create', _extends({
234
+ email: values.email
235
+ }, values.name ? {
236
+ name: values.name
237
+ } : {}, values.company ? {
238
+ company: values.company
239
+ } : {}, values.jobTitle ? {
240
+ jobTitle: values.jobTitle
241
+ } : {}, values.phone ? {
242
+ phone: values.phone
243
+ } : {}, values.website ? {
244
+ website: values.website
245
+ } : {}, values.leadSource ? {
246
+ leadSource: values.leadSource
247
+ } : {}, values.address ? {
248
+ address: values.address
249
+ } : {}, values.tags.length ? {
250
+ tags: values.tags
251
+ } : {}, values.ownerUid ? {
252
+ ownerUid: values.ownerUid
253
+ } : {}, values.notes ? {
254
+ notes: values.notes
255
+ } : {}, {
256
+ status: values.status
257
+ }));
258
+ if (!response.ok) {
259
+ var _payload_error;
260
+ // The route's own sentence, shown above the form unchanged.
261
+ setCreateError(String((_payload_error = payload['error']) != null ? _payload_error : 'The lead could not be added.'));
262
+ return;
263
+ }
264
+ // The activity entry is the route's: it verified the caller and
265
+ // performed the write.
266
+ enqueueSnackbar(payload['created'] ? 'Lead added' : 'This site already held a lead for that address — it was updated', {
267
+ variant: 'success',
268
+ persist: false
269
+ });
270
+ setCreateOpen(false);
271
+ } catch (error) {
272
+ console.error(error);
273
+ setCreateError('The lead could not be added.');
274
+ } finally{
275
+ setCreateBusy(false);
276
+ }
277
+ }, [
278
+ crmApi,
279
+ enqueueSnackbar
280
+ ]);
217
281
  const writeLead = useCallback(async (lead, fields, done)=>{
218
282
  try {
219
283
  await updateDoc(doc(firestore, 'hosts', lead.hostId, 'leads', lead.leadId), _extends({}, fields, {
@@ -264,6 +328,28 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
264
328
  });
265
329
  }
266
330
  },
331
+ // The lead's own profile (AGL-3231): the two facts a work queue is
332
+ // scanned by, beside the person.
333
+ {
334
+ field: 'company',
335
+ headerName: 'Company',
336
+ flex: 1,
337
+ minWidth: 140,
338
+ valueGetter: (_value, row)=>{
339
+ var _row_company;
340
+ return String((_row_company = row.company) != null ? _row_company : '');
341
+ }
342
+ },
343
+ {
344
+ field: 'jobTitle',
345
+ headerName: 'Title',
346
+ flex: 0.9,
347
+ minWidth: 130,
348
+ valueGetter: (_value, row)=>{
349
+ var _row_jobTitle;
350
+ return String((_row_jobTitle = row.jobTitle) != null ? _row_jobTitle : '');
351
+ }
352
+ },
267
353
  {
268
354
  field: 'status',
269
355
  headerName: 'Status',
@@ -312,6 +398,16 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
312
398
  minWidth: 140,
313
399
  valueGetter: (_value, row)=>leadSources(row).map(leadSourceLabel).join(', ')
314
400
  },
401
+ {
402
+ field: 'tags',
403
+ headerName: 'Tags',
404
+ flex: 0.9,
405
+ minWidth: 140,
406
+ valueGetter: (_value, row)=>{
407
+ var _row_tags;
408
+ return ((_row_tags = row.tags) != null ? _row_tags : []).join(', ');
409
+ }
410
+ },
315
411
  {
316
412
  field: 'lastSeenAtMs',
317
413
  headerName: 'Last seen',
@@ -450,6 +546,12 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
450
546
  onClick: handleExport,
451
547
  disabled: !rows.length,
452
548
  children: 'Export CSV'
549
+ }),
550
+ /*#__PURE__*/ _jsx(Button, {
551
+ size: "small",
552
+ variant: "contained",
553
+ onClick: ()=>setCreateOpen(true),
554
+ children: 'New lead'
453
555
  })
454
556
  ]
455
557
  }),
@@ -463,7 +565,7 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
463
565
  }) : /*#__PURE__*/ _jsx(OrgLeadSurfacesNote, {}),
464
566
  status === 'success' && window.length === 0 ? /*#__PURE__*/ _jsx(EmptyStateComponent, {
465
567
  label: 'No leads yet',
466
- description: 'Sign-ups, bookings and form submissions on your site become leads on their own — or bring a list in with Import CSV.'
568
+ description: 'Bookings and lead-routed forms on your site become leads on their own — or add one with New lead, or bring a list in with Import CSV.'
467
569
  }) : status === 'success' && rows.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
468
570
  variant: "body2",
469
571
  color: "text.secondary",
@@ -517,6 +619,16 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
517
619
  ]
518
620
  })
519
621
  }),
622
+ /*#__PURE__*/ _jsx(NewLeadDrawer, {
623
+ open: createOpen,
624
+ onClose: ()=>setCreateOpen(false),
625
+ hostId: hostId != null ? hostId : null,
626
+ org: org,
627
+ busy: createBusy,
628
+ error: createError,
629
+ roster: roster,
630
+ onSubmit: (values)=>void handleCreate(values)
631
+ }),
520
632
  /*#__PURE__*/ _jsx(AssignOwnerDialog, {
521
633
  lead: assigning,
522
634
  roster: roster,