@aglyn/plugins-crm 1.0.0-beta.163 → 1.0.0-beta.165
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/lib/components/contact-associations-card.js +14 -4
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-field-drawer.js +19 -2
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/components/fields-section.d.ts +2 -1
- package/src/lib/components/fields-section.js +6 -3
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.d.ts +59 -0
- package/src/lib/components/lead-campaigns-card.js +160 -0
- package/src/lib/components/lead-campaigns-card.js.map +1 -0
- package/src/lib/components/lead-detail-page.js +53 -3
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-history-card.js +5 -0
- package/src/lib/components/lead-history-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.d.ts +12 -0
- package/src/lib/components/lead-properties-card.js +63 -5
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.d.ts +6 -0
- package/src/lib/components/leads-bulk-bar.js +34 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +16 -2
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-lead-drawer.d.ts +14 -1
- package/src/lib/components/new-lead-drawer.js +41 -3
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/hooks/use-campaign-filing-log.d.ts +29 -0
- package/src/lib/hooks/use-campaign-filing-log.js +91 -0
- package/src/lib/hooks/use-campaign-filing-log.js.map +1 -0
- package/src/lib/model/campaign-filing-activity.d.ts +82 -0
- package/src/lib/model/campaign-filing-activity.js +88 -0
- package/src/lib/model/campaign-filing-activity.js.map +1 -0
- package/src/lib/server/campaign-filing-activity.d.ts +79 -0
- package/src/lib/server/campaign-filing-activity.js +93 -0
- package/src/lib/server/campaign-filing-activity.js.map +1 -0
- package/src/lib/server/lead-campaign-carry.js +25 -0
- package/src/lib/server/lead-campaign-carry.js.map +1 -1
- package/src/lib/server/lead-create.d.ts +15 -4
- package/src/lib/server/lead-create.js +68 -10
- package/src/lib/server/lead-create.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 {\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 { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { LeadStatusChip } from './lead-status-chip'\n\nconst NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX\nconst TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX\n\n/** The profile as the form holds it: every field a string, the address a draft. */\ninterface ProfileDraft {\n company: string\n jobTitle: string\n phone: string\n website: string\n leadSource: string\n tags: string\n address: AddressDraft\n}\n\n/** The stored profile as a draft the fields can edit. */\nfunction profileDraftFrom(lead: Record<string, unknown> & CrmLeadFields): ProfileDraft {\n return {\n company: String(lead.company ?? ''),\n jobTitle: String(lead.jobTitle ?? ''),\n phone: String(lead.phone ?? lead['phone'] ?? ''),\n website: String(lead.website ?? ''),\n leadSource: String(lead.leadSource ?? ''),\n tags: (lead.tags ?? []).join(', '),\n address: addressDraftFrom(lead.address ?? null),\n }\n}\n\n/** The patch as the document takes it: a cleared field is deleted, not blanked. */\nfunction profileWrite(patch: CrmLeadProfilePatch): Record<string, unknown> {\n const write: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n write[key] = value === null ? deleteField() : value\n }\n return write\n}\n\n/**\n * Why Convert is refused while an erasure waits on the person — the same\n * sentence shape the overflow's items carry, so the two read as one state.\n */\nexport const CONVERT_PENDING_ERASURE_REASON = 'An erasure is pending for this person'\n\n/** A label over a value — the record page's one row shape. */\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\nexport interface LeadPropertiesCardProps {\n 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 // The last verdict on the address (AGL-3245), as the platform stamped it.\n const emailState = Aglyn.readEmailState(lead)\n /** What a capture that took a number left on the document (AGL-2661). */\n const leadPhone = String(lead['phone'] ?? '').trim()\n\n const [notes, setNotes] = useState(String(lead.notes ?? ''))\n // The label's id, so the status combobox is named \"Status\" rather than\n // after the status it shows — see `LeadOwnerSelect`.\n const statusLabelId = useId()\n const [notesDirty, setNotesDirty] = useState(false)\n const [savingNotes, setSavingNotes] = useState(false)\n /*\n * THE LEAD'S OWN PROFILE (AGL-3231) — company, title, phone, website,\n * address, tags, lead source — edited as one block with one Save, the\n * way the contact's Properties card saves. Seeded from the document and\n * guarded on save like the notes: a draft edited over a cached read must\n * not overwrite a newer profile with an older one.\n */\n const [profile, setProfile] = useState<ProfileDraft>(() => profileDraftFrom(lead))\n const [profileDirty, setProfileDirty] = useState(false)\n const [savingProfile, setSavingProfile] = useState(false)\n const [profileErrors, setProfileErrors] = useState<Record<string, string>>({})\n useEffect(() => {\n if (!profileDirty) setProfile(profileDraftFrom(lead))\n // The draft follows the document until it is edited; the fields are\n // read one by one so a change to any of them reseeds.\n }, [\n profileDirty,\n lead.company,\n lead.jobTitle,\n lead.phone,\n lead.website,\n lead.leadSource,\n lead.tags,\n lead.address,\n ])\n 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 emailState={emailState}\n />\n </>\n }\n menuItems={[\n ...(open\n ? [\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />,\n destructive: true,\n onClick: onUnqualify,\n } satisfies RowActionsMenuItem,\n ]\n : []),\n ...extraMenuItems,\n ]}\n chips={\n <>\n <LeadStatusChip lead={lead} />\n {/* The verdict on the address (AGL-3245), beside the status: a\n bounce does not move New or Working, but it is the first\n thing a person deciding whether to write must see. */}\n <CrmEmailStateChip state={emailState} />\n <CrmRecordChip\n label=\"Owner\"\n value={lead.ownerUid ? roster.labelFor(lead.ownerUid) : undefined}\n />\n </>\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","CrmEmailStateChip","CrmRecordChip","CrmRecordHeader","CrmSendEmailButton","LeadOwnerSelect","LeadStatusChip","NOTES_MAX","CRM_LEAD_NOTES_MAX","TEXT_MAX","CRM_LEAD_TEXT_MAX","profileDraftFrom","lead","company","String","jobTitle","phone","website","leadSource","tags","join","address","profileWrite","patch","write","key","value","Object","entries","undefined","CONVERT_PENDING_ERASURE_REASON","Fact","props","spacing","variant","color","label","component","children","LeadPropertiesCard","hostId","leadId","leadStatus","fromCache","basePath","roster","onConvert","onUnqualify","extraMenuItems","banner","erasurePending","org","firestore","enqueueSnackbar","routes","ref","status","crmLeadStatus","converted","Boolean","convertedContactId","open","isCrmLeadOpen","emailState","readEmailState","leadPhone","trim","notes","setNotes","statusLabelId","notesDirty","setNotesDirty","savingNotes","setSavingNotes","profile","setProfile","profileDirty","setProfileDirty","savingProfile","setSavingProfile","profileErrors","setProfileErrors","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","state","ownerUid","labelFor","direction","xs","md","sx","alignItems","convertedAtMs","toLocaleString","minWidth","id","labelId","onChange","event","next","target","unqualifiedReason","CRM_LEAD_STATUS_LABELS","new","working","unqualified","fullWidth","uid","severity","flexWrap","rowGap","componentVariant","nativeButton","href","contact","companyId","dealId","deal","slotProps","htmlInput","maxLength","helperText","justifyContent","multiline","minRows","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-properties-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type {\n AglynOrgBilling,\n CrmLeadFields,\n CrmLeadProfilePatch,\n CrmLeadStatus,\n} from '@aglyn/aglyn'\nimport { mdiAccountCancelOutline } from '@aglyn/shared-data-mdi'\nimport { AppLink, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n FormControl,\n InputLabel,\n MenuItem,\n Select,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { deleteField, doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useId, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport {\n crmCustomDraftChanges,\n type CrmCustomDraft,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n addressDraftFrom,\n ContactAddressFields,\n type AddressDraft,\n} from './contact-address-fields'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { LeadStatusChip } from './lead-status-chip'\n\nconst NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX\nconst TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX\n\n/** The profile as the form holds it: every field a string, the address a draft. */\ninterface ProfileDraft {\n company: string\n jobTitle: string\n phone: string\n website: string\n leadSource: string\n tags: string\n address: AddressDraft\n}\n\n/** The stored profile as a draft the fields can edit. */\nfunction profileDraftFrom(lead: Record<string, unknown> & CrmLeadFields): ProfileDraft {\n return {\n company: String(lead.company ?? ''),\n jobTitle: String(lead.jobTitle ?? ''),\n phone: String(lead.phone ?? lead['phone'] ?? ''),\n website: String(lead.website ?? ''),\n leadSource: String(lead.leadSource ?? ''),\n tags: (lead.tags ?? []).join(', '),\n address: addressDraftFrom(lead.address ?? null),\n }\n}\n\n/** The patch as the document takes it: a cleared field is deleted, not blanked. */\nfunction profileWrite(patch: CrmLeadProfilePatch): Record<string, unknown> {\n const write: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n write[key] = value === null ? deleteField() : value\n }\n return write\n}\n\n/**\n * Why Convert is refused while an erasure waits on the person — the same\n * sentence shape the overflow's items carry, so the two read as one state.\n */\nexport const CONVERT_PENDING_ERASURE_REASON = 'An erasure is pending for this person'\n\n/** A label over a value — the record page's one row shape. */\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\nexport interface LeadPropertiesCardProps {\n hostId: string\n /**\n * The org the site belongs to, as the page already resolved it — the\n * custom lead fields are ORG-wide (AGL-3272), and a second lookup here\n * would be one more read per record page for an answer already in hand.\n */\n orgId: string | null\n leadId: string\n lead: Record<string, unknown> & CrmLeadFields\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n basePath: string\n roster: OrgMemberOptions\n onConvert: () => void\n onUnqualify: () => void\n /**\n * Items the page adds to the overflow beside Unqualify — the privacy\n * erasure (AGL-2623) lives on the page, because it needs the workspace\n * role and the API, and this card owns the header it must appear in.\n */\n extraMenuItems?: RowActionsMenuItem[]\n /** What the page shows above the facts — the erasure-pending state. */\n banner?: React.ReactNode\n /**\n * Chips the page adds after the owner — the campaigns the lead is filed\n * under (AGL-3274), named from the containers the page reads once for\n * this header and the Campaigns card below it.\n */\n extraChips?: React.ReactNode\n /**\n * An erasure request is waiting on this person (AGL-2623). Convert stays\n * on the page but is refused with the reason, the way the overflow's items\n * are: a conversion filed now would reach the capture door only to be\n * refused there, and the lead itself goes when the request runs.\n */\n erasurePending?: boolean\n /**\n * The org the shell passed: the booking door reads whether the lead's site\n * runs Bookings and whether the plan is entitled to it (AGL-2660), and a\n * logged call reads the activity scope it belongs in (AGL-2661).\n */\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * What the team knows and decides about a lead: status, owner, notes, and\n * the identity and consent the capture recorded (AGL-2608).\n *\n * Status and owner are single-field client writes — the rules let a site\n * admin, editor or author update `hosts/{hostId}/leads`, and a one-field\n * `update` cannot roll anything else back. Notes are a text field seeded\n * from the document, so that save goes through `writeGuardedBySeed`: a draft\n * edited over a cached read would otherwise overwrite a newer note with an\n * older one plus a sentence.\n *\n * Converted leads are read-only here. Their status is the conversion, and\n * the actions become links to what the conversion made.\n */\nexport function LeadPropertiesCard(props: LeadPropertiesCardProps) {\n const {\n hostId,\n orgId,\n leadId,\n lead,\n leadStatus,\n fromCache,\n basePath,\n roster,\n onConvert,\n onUnqualify,\n extraMenuItems = [],\n banner,\n extraChips,\n erasurePending = false,\n org,\n } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const routes = crmRoutes(basePath)\n 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 // The last verdict on the address (AGL-3245), as the platform stamped it.\n const emailState = Aglyn.readEmailState(lead)\n /** What a capture that took a number left on the document (AGL-2661). */\n const leadPhone = String(lead['phone'] ?? '').trim()\n\n const [notes, setNotes] = useState(String(lead.notes ?? ''))\n // The label's id, so the status combobox is named \"Status\" rather than\n // after the status it shows — see `LeadOwnerSelect`.\n const statusLabelId = useId()\n const [notesDirty, setNotesDirty] = useState(false)\n const [savingNotes, setSavingNotes] = useState(false)\n /*\n * THE LEAD'S OWN PROFILE (AGL-3231) — company, title, phone, website,\n * address, tags, lead source — edited as one block with one Save, the\n * way the contact's Properties card saves. Seeded from the document and\n * guarded on save like the notes: a draft edited over a cached read must\n * not overwrite a newer profile with an older one.\n */\n const [profile, setProfile] = useState<ProfileDraft>(() => profileDraftFrom(lead))\n const [profileDirty, setProfileDirty] = useState(false)\n const [savingProfile, setSavingProfile] = useState(false)\n const [profileErrors, setProfileErrors] = useState<Record<string, string>>({})\n useEffect(() => {\n if (!profileDirty) setProfile(profileDraftFrom(lead))\n // The draft follows the document until it is edited; the fields are\n // read one by one so a change to any of them reseeds.\n }, [\n profileDirty,\n lead.company,\n lead.jobTitle,\n lead.phone,\n lead.website,\n lead.leadSource,\n lead.tags,\n lead.address,\n ])\n /*\n * THE ORG'S OWN LEAD FIELDS (AGL-3272), edited under the same Save as\n * the profile: one button over one card, so a person filling a lead in\n * does not have to find two.\n *\n * The draft holds only the keys the reader touched, and Save writes the\n * difference as dotted paths — a `custom` map written whole would take\n * out every key this card did not show, which is what a retired field's\n * values and an integration's writes sit under.\n */\n const fields = useContactFieldDefinitions(orgId, 'lead')\n const storedCustom = useMemo(() => lead.custom ?? {}, [lead.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n const editProfile = <K extends keyof ProfileDraft>(key: K, value: ProfileDraft[K]) => {\n setProfile((current) => ({ ...current, [key]: value }))\n setProfileDirty(true)\n }\n // A newer note from the server replaces an UNEDITED draft; an edited one is\n // the reader's, and the guard on save decides whether it may land.\n useEffect(() => {\n if (!notesDirty) setNotes(String(lead.notes ?? ''))\n }, [lead.notes, notesDirty])\n\n const write = async (fields: Record<string, unknown>, done: string) => {\n try {\n await updateDoc(ref, { ...fields, updatedAt: serverTimestamp() })\n enqueueSnackbar(done, { variant: 'success', persist: false })\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n }\n }\n\n const saveNotes = async () => {\n setSavingNotes(true)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n () => write({ notes: notes.trim().slice(0, NOTES_MAX) }, 'Notes saved'),\n )\n setSavingNotes(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The notes could not be saved.', {\n variant: 'warning',\n })\n return\n }\n setNotesDirty(false)\n }\n\n /*\n * What Save is offered for: a profile field edited, or a custom value\n * that differs from the stored map. Touched-and-put-back does not count\n * — the draft keeps the key either way, and a Save that wrote nothing\n * would still bump `updatedAt`.\n */\n const dirty =\n profileDirty || crmCustomDraftChanges(storedCustom, custom).length > 0\n\n const saveProfile = async () => {\n const { patch, errors } = Aglyn.normalizeCrmLeadProfile({\n company: profile.company,\n jobTitle: profile.jobTitle,\n phone: profile.phone,\n website: profile.website,\n leadSource: profile.leadSource,\n tags: profile.tags,\n address: profile.address,\n })\n setProfileErrors(errors)\n if (Object.keys(errors).length) return\n /*\n * A required field the reader CLEARED is refused; one the lead has\n * always lacked is not this save's to demand, or a field added after\n * the lead was captured would block every later edit to its company.\n */\n const missing = crmCustomDraftMissingRequired(\n fields.active,\n storedCustom,\n custom,\n 'edit',\n )\n if (missing.length) {\n enqueueSnackbar(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`, {\n variant: 'warning',\n })\n return\n }\n setSavingProfile(true)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n () =>\n write(\n { ...profileWrite(patch), ...crmCustomDraftWrites(storedCustom, custom) },\n 'Lead saved',\n ),\n )\n setSavingProfile(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The lead could not be saved.', {\n variant: 'warning',\n })\n return\n }\n setProfileDirty(false)\n // The draft is spent: what it held is stored, and the controls read\n // the document again.\n setCustom({})\n }\n\n const consent = Aglyn.readMarketingBasis(lead, 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 emailState={emailState}\n />\n </>\n }\n menuItems={[\n ...(open\n ? [\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />,\n destructive: true,\n onClick: onUnqualify,\n } satisfies RowActionsMenuItem,\n ]\n : []),\n ...extraMenuItems,\n ]}\n chips={\n <>\n <LeadStatusChip lead={lead} />\n {/* The verdict on the address (AGL-3245), beside the status: a\n bounce does not move New or Working, but it is the first\n thing a person deciding whether to write must see. */}\n <CrmEmailStateChip state={emailState} />\n <CrmRecordChip\n label=\"Owner\"\n value={lead.ownerUid ? roster.labelFor(lead.ownerUid) : undefined}\n />\n {extraChips}\n </>\n }\n >\n <Stack spacing={3}>\n {banner}\n {/*\n Only when the capture carried one (AGL-2661): the sign-up and\n booking doors write no phone, so a row for every lead would be a\n permanent blank. A form that captures one fills this.\n */}\n {leadPhone ? (\n <Fact label=\"Phone\">\n <CrmPhoneLink phone={leadPhone} />\n </Fact>\n ) : null}\n <Fact label=\"Marketing consent\">{consentLine}</Fact>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n {converted ? (\n <Fact label=\"Status\">\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <LeadStatusChip lead={lead} />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {lead.convertedAtMs\n ? `Converted ${new Date(lead.convertedAtMs).toLocaleString()}`\n : 'Converted'}\n </Typography>\n </Stack>\n </Fact>\n ) : (\n <FormControl size=\"small\" sx={{ minWidth: 200 }}>\n <InputLabel id={statusLabelId}>{'Status'}</InputLabel>\n <Select\n labelId={statusLabelId}\n label=\"Status\"\n value={status === 'unqualified' ? 'unqualified' : status}\n onChange={(event) => {\n const next = String(event.target.value) as CrmLeadStatus\n if (next === 'unqualified') {\n onUnqualify()\n return\n }\n // Reopening drops the reason with the closed state: a lead\n // being worked again is not \"unqualified because …\".\n void write(\n {\n status: next,\n ...(status === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n },\n 'Status updated',\n )\n }}\n >\n <MenuItem value=\"new\">{Aglyn.CRM_LEAD_STATUS_LABELS.new}</MenuItem>\n <MenuItem value=\"working\">{Aglyn.CRM_LEAD_STATUS_LABELS.working}</MenuItem>\n <MenuItem value=\"unqualified\">\n {`${Aglyn.CRM_LEAD_STATUS_LABELS.unqualified}…`}\n </MenuItem>\n </Select>\n </FormControl>\n )}\n <LeadOwnerSelect\n value={lead.ownerUid}\n roster={roster}\n fullWidth={false}\n onChange={(uid) =>\n void write({ ownerUid: uid || deleteField() }, uid ? 'Owner assigned' : 'Owner cleared')\n }\n />\n </Stack>\n {status === 'unqualified' && lead.unqualifiedReason ? (\n <Alert severity=\"info\">{`Unqualified: ${lead.unqualifiedReason}`}</Alert>\n ) : null}\n {converted ? (\n <Stack direction=\"row\" spacing={1} sx={{ flexWrap: 'wrap', rowGap: 1 }}>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.contact(String(lead.convertedContactId))}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open contact'}\n </Button>\n {lead.companyId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.company(lead.companyId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open company'}\n </Button>\n ) : null}\n {lead.dealId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.deal(lead.dealId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open deal'}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n {/*\n The profile (AGL-3231): what Salesforce keeps on a lead and hands\n to the contact and the account on convert. Read-only once\n converted — the contact is the record then, and the links above\n lead there.\n */}\n <Stack spacing={2}>\n <Typography variant=\"subtitle2\">{'Profile'}</Typography>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"Company\"\n value={profile.company}\n onChange={(event) => editProfile('company', event.target.value)}\n disabled={converted}\n slotProps={{ htmlInput: { maxLength: TEXT_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={profile.jobTitle}\n onChange={(event) => editProfile('jobTitle', event.target.value)}\n disabled={converted}\n slotProps={{ htmlInput: { maxLength: TEXT_MAX } }}\n fullWidth\n />\n </Stack>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={profile.phone}\n onChange={(event) => editProfile('phone', event.target.value)}\n disabled={converted}\n error={Boolean(profileErrors['phone'])}\n helperText={profileErrors['phone'] || 'With the country code, like +1 512 555 0107'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Website\"\n value={profile.website}\n onChange={(event) => editProfile('website', event.target.value)}\n disabled={converted}\n error={Boolean(profileErrors['website'])}\n helperText={profileErrors['website'] || 'Like acme.com'}\n fullWidth\n />\n </Stack>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"Lead source\"\n value={profile.leadSource}\n onChange={(event) => editProfile('leadSource', event.target.value)}\n disabled={converted}\n slotProps={{ htmlInput: { maxLength: TEXT_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Tags\"\n value={profile.tags}\n onChange={(event) => editProfile('tags', event.target.value)}\n disabled={converted}\n helperText=\"Comma-separated\"\n fullWidth\n />\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Address'}\n </Typography>\n <ContactAddressFields\n value={profile.address}\n onChange={(next) => editProfile('address', next)}\n disabled={converted}\n />\n {/*\n The org's own lead fields (AGL-3272), under the profile because\n they describe the same record and save with it. A converted\n lead's are read-only with the rest of the card.\n */}\n {fields.active.length ? (\n <>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Custom fields'}\n </Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={converted || savingProfile}\n />\n ))}\n </>\n ) : null}\n {converted ? null : (\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void saveProfile()}\n disabled={!dirty || savingProfile}\n >\n {'Save'}\n </Button>\n </Stack>\n )}\n </Stack>\n <Stack spacing={1}>\n <TextField\n size=\"small\"\n label=\"Notes\"\n value={notes}\n onChange={(event) => {\n setNotes(event.target.value)\n setNotesDirty(true)\n }}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: NOTES_MAX } }}\n />\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void saveNotes()}\n disabled={!notesDirty || savingNotes}\n >\n {'Save notes'}\n </Button>\n </Stack>\n </Stack>\n </Stack>\n </CrmRecordHeader>\n )\n}\nLeadPropertiesCard.displayName = 'LeadPropertiesCard'\n\nexport default LeadPropertiesCard\n"],"names":["Aglyn","mdiAccountCancelOutline","AppLink","MdiIcon","useSnackbar","useFirestore","writeGuardedBySeed","Alert","Button","FormControl","InputLabel","MenuItem","Select","Stack","TextField","Tooltip","Typography","deleteField","doc","serverTimestamp","updateDoc","useEffect","useId","useMemo","useState","useContactFieldDefinitions","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","crmRoutes","addressDraftFrom","ContactAddressFields","CrmCallButton","CrmPhoneLink","CrmEmailStateChip","CrmRecordChip","CrmRecordHeader","CrmSendEmailButton","LeadOwnerSelect","LeadStatusChip","NOTES_MAX","CRM_LEAD_NOTES_MAX","TEXT_MAX","CRM_LEAD_TEXT_MAX","profileDraftFrom","lead","company","String","jobTitle","phone","website","leadSource","tags","join","address","profileWrite","patch","write","key","value","Object","entries","undefined","CONVERT_PENDING_ERASURE_REASON","Fact","props","spacing","variant","color","label","component","children","LeadPropertiesCard","hostId","orgId","leadId","leadStatus","fromCache","basePath","roster","onConvert","onUnqualify","extraMenuItems","banner","extraChips","erasurePending","org","firestore","enqueueSnackbar","routes","ref","status","crmLeadStatus","converted","Boolean","convertedContactId","open","isCrmLeadOpen","emailState","readEmailState","leadPhone","trim","notes","setNotes","statusLabelId","notesDirty","setNotesDirty","savingNotes","setSavingNotes","profile","setProfile","profileDirty","setProfileDirty","savingProfile","setSavingProfile","profileErrors","setProfileErrors","fields","storedCustom","custom","setCustom","editProfile","current","done","updatedAt","persist","error","Error","message","saveNotes","verdict","subject","unreadable","slice","ok","dirty","length","saveProfile","errors","normalizeCrmLeadProfile","keys","missing","active","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","state","ownerUid","labelFor","direction","xs","md","sx","alignItems","convertedAtMs","toLocaleString","minWidth","id","labelId","onChange","event","next","target","unqualifiedReason","CRM_LEAD_STATUS_LABELS","new","working","unqualified","fullWidth","uid","severity","flexWrap","rowGap","componentVariant","nativeButton","href","contact","companyId","dealId","deal","slotProps","htmlInput","maxLength","helperText","map","definition","$id","justifyContent","multiline","minRows","displayName"],"mappings":"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"}
|
|
@@ -50,6 +50,12 @@ export interface LeadsBulkBarProps {
|
|
|
50
50
|
* where the complete export spans every site (AGL-2662).
|
|
51
51
|
*/
|
|
52
52
|
hostId?: string | null;
|
|
53
|
+
/**
|
|
54
|
+
* The org the shell passed, for the scope a filing entry carries
|
|
55
|
+
* (AGL-3274): Add to campaign writes one on each lead's Activity, and
|
|
56
|
+
* the entry is visible to whoever sees a record made on this site.
|
|
57
|
+
*/
|
|
58
|
+
org?: Record<string, unknown> | null;
|
|
53
59
|
}
|
|
54
60
|
export declare function LeadsBulkBar(props: LeadsBulkBarProps): import("react").JSX.Element;
|
|
55
61
|
export declare namespace LeadsBulkBar {
|
|
@@ -40,12 +40,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
40
40
|
* menu and the inline select do. A lead already closed is not unqualified
|
|
41
41
|
* twice. A lead already at the asked status is skipped rather than rewritten
|
|
42
42
|
* so "Nothing to change" means what it says.
|
|
43
|
-
*/ import { CRM_LEAD_STATUS_LABELS, crmLeadStatus, isCrmLeadOpen } from "@aglyn/aglyn";
|
|
43
|
+
*/ import { CRM_LEAD_STATUS_LABELS, crmLeadStatus, isCrmLeadOpen, readCampaignIds } from "@aglyn/aglyn";
|
|
44
44
|
import CampaignPicker from "@aglyn/shared-ui-email-campaigns/components/campaign-picker.component";
|
|
45
45
|
import { useFirestore, useHostCampaigns } from "@aglyn/tenant-feature-instance";
|
|
46
46
|
import { Button, MenuItem, TextField } from "@mui/material";
|
|
47
47
|
import { arrayUnion, deleteField, doc, serverTimestamp } from "firebase/firestore";
|
|
48
48
|
import { useCallback, useMemo, useState } from "react";
|
|
49
|
+
import { useCampaignFilingLog } from "../hooks/use-campaign-filing-log.js";
|
|
49
50
|
import { useCrmBulkApply } from "../hooks/use-crm-bulk-apply.js";
|
|
50
51
|
import { downloadTextFile } from "../model/contacts-csv.js";
|
|
51
52
|
import { crmBulkWriters, runCrmBulkWrites } from "../model/crm-bulk-writes.js";
|
|
@@ -101,6 +102,11 @@ function LeadsBulkBarBody(props) {
|
|
|
101
102
|
const campaigns = useHostCampaigns(hostId != null ? hostId : undefined, {
|
|
102
103
|
enabled: pending === 'campaign' && Boolean(hostId)
|
|
103
104
|
});
|
|
105
|
+
const logFiling = useCampaignFilingLog({
|
|
106
|
+
orgId,
|
|
107
|
+
hostId,
|
|
108
|
+
org: props.org
|
|
109
|
+
});
|
|
104
110
|
// The reference a write names is the row's own site and document.
|
|
105
111
|
const writers = useMemo(()=>{
|
|
106
112
|
const byId = new Map(rows.map((row)=>[
|
|
@@ -168,10 +174,33 @@ function LeadsBulkBarBody(props) {
|
|
|
168
174
|
}
|
|
169
175
|
});
|
|
170
176
|
}
|
|
171
|
-
await runPlan({
|
|
177
|
+
const outcome = await runPlan({
|
|
172
178
|
writes,
|
|
173
179
|
skipped
|
|
174
180
|
}, (count)=>`Added ${countNoun(count, NOUN)} to ${campaignIds.length === 1 ? 'the campaign' : `${campaignIds.length} campaigns`}`);
|
|
181
|
+
// What landed is written on each lead's Activity (AGL-3274): one
|
|
182
|
+
// "Filed under" per campaign the lead was not already in, and none
|
|
183
|
+
// for a lead the store refused — the row's own document is the
|
|
184
|
+
// membership, and the entry only records the act.
|
|
185
|
+
const refused = new Set(outcome.refused.map((entry)=>entry.label));
|
|
186
|
+
const named = campaignIds.map((campaignId)=>{
|
|
187
|
+
var _ref;
|
|
188
|
+
var _campaigns_options_find;
|
|
189
|
+
return {
|
|
190
|
+
id: campaignId,
|
|
191
|
+
name: (_ref = (_campaigns_options_find = campaigns.options.find((option)=>option.value === campaignId)) == null ? void 0 : _campaigns_options_find.label) != null ? _ref : ''
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
for (const lead of selectedRows){
|
|
195
|
+
if (refused.has(labelOf(lead))) continue;
|
|
196
|
+
const already = readCampaignIds(lead);
|
|
197
|
+
const filed = named.filter((campaign)=>!already.includes(campaign.id));
|
|
198
|
+
if (filed.length) await logFiling({
|
|
199
|
+
leadId: lead.leadId
|
|
200
|
+
}, {
|
|
201
|
+
filed
|
|
202
|
+
});
|
|
203
|
+
}
|
|
175
204
|
return;
|
|
176
205
|
}
|
|
177
206
|
if (action === 'status') {
|
|
@@ -242,8 +271,10 @@ function LeadsBulkBarBody(props) {
|
|
|
242
271
|
pending,
|
|
243
272
|
value,
|
|
244
273
|
campaignIds,
|
|
274
|
+
campaigns.options,
|
|
245
275
|
selectedRows,
|
|
246
|
-
runPlan
|
|
276
|
+
runPlan,
|
|
277
|
+
logFiling
|
|
247
278
|
]);
|
|
248
279
|
const handleExport = useCallback(()=>{
|
|
249
280
|
downloadTextFile('leads-selected.csv', 'text/csv', leadsCsv(selectedRows, csv));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/leads-bulk-bar.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * The bar over the Leads table, for whatever rows are ticked (AGL-2662).\n *\n * Hand them to an owner, set their status, unqualify them with one reason,\n * or take them into a spreadsheet. Every act here is a document write and\n * nothing more — a lead's status and owner are the team's own notes on a\n * capture, with no event and no notification behind them, which is why the\n * row's inline select writes them client-direct — so the whole bar goes\n * through the shared batched runner under the same rules.\n *\n * ## A row names its own site\n *\n * A lead lives at `hosts/{hostId}/leads/{leadId}`, and at the organization\n * level the selection spans sites, so the reference for a write is built\n * from the ROW rather than from a scope the bar was handed: the row knows\n * its site, the bar need not. Under a site every row names the same one.\n *\n * ## What is declined, by name\n *\n * A converted lead's status IS its conversion, stamped by the route, and\n * the bar leaves it alone under Set status and Unqualify the way the row\n * menu and the inline select do. A lead already closed is not unqualified\n * twice. A lead already at the asked status is skipped rather than rewritten\n * so \"Nothing to change\" means what it says.\n */\n\nimport {\n CRM_LEAD_STATUS_LABELS,\n type CrmLeadFields,\n type CrmLeadStatus,\n crmLeadStatus,\n isCrmLeadOpen,\n} from '@aglyn/aglyn'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { useFirestore, useHostCampaigns } from '@aglyn/tenant-feature-instance'\nimport { Button, MenuItem, TextField } from '@mui/material'\nimport { arrayUnion, deleteField, doc, serverTimestamp } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport {\n type CrmBulkPlan,\n type CrmBulkSkip,\n type CrmBulkWrite,\n crmBulkWriters,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport { type LeadCsvOptions, leadsCsv } from '../model/leads-csv'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { UNQUALIFY_REASON_MAX } from './lead-unqualify-dialog'\n\n/**\n * One row of the Leads list as the bar needs it: the document's fields,\n * the grid's key, and which document under which site a write names.\n */\nexport type LeadBulkRow = Record<string, unknown> &\n CrmLeadFields & { $id: string; leadId: string; hostId: string }\n\nexport interface LeadsBulkBarProps {\n rows: readonly LeadBulkRow[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /** The section's roster — already read for the Owner column. */\n roster: OrgMemberOptions\n /** How the export names the owner and, at the org level, the site — the list's own. */\n csv?: LeadCsvOptions\n /** The organization these leads belong to; null while it is unresolved. */\n orgId?: string | null\n /**\n * The site the list is read under, or `null` at the organization level\n * where the complete export spans every site (AGL-2662).\n */\n hostId?: string | null\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'lead', plural: 'leads' }\n\ntype PendingAction = 'owner' | 'status' | 'unqualify' | 'campaign'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n owner: 'Set the owner',\n status: 'Set the status',\n unqualify: 'Unqualify',\n campaign: 'Add to campaign',\n}\n\n/** The statuses the bar can set — the open ones; closing goes through Unqualify. */\nconst SETTABLE_STATUSES: readonly CrmLeadStatus[] = ['new', 'working']\n\n/** The label a report lists a lead under — its name, else its address. */\nconst labelOf = (lead: LeadBulkRow): string =>\n String(lead['name'] || lead['email'] || lead.leadId)\n\nexport function LeadsBulkBar(props: LeadsBulkBarProps) {\n if (!props.selected.length) return null\n return <LeadsBulkBarBody {...props} />\n}\nLeadsBulkBar.displayName = 'LeadsBulkBar'\n\nfunction LeadsBulkBarBody(props: LeadsBulkBarProps) {\n const { rows, selected, onSelectedChange, roster, csv } = props\n const orgId = props.orgId ?? null\n const hostId = props.hostId ?? null\n const firestore = useFirestore()\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'lead' })\n\n const selectedRows = useMemo(() => {\n const chosen = new Set(selected)\n return rows.filter((row) => chosen.has(row.$id))\n }, [rows, selected])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n /*\n * The campaigns to add the selection to (AGL-3254), and the site's\n * containers the picker offers — read only while that dialog is open.\n * Under a site alone: a campaign belongs to one site, and at the\n * organization level a selection spans them.\n */\n const [campaignIds, setCampaignIds] = useState<string[]>([])\n const campaigns = useHostCampaigns(hostId ?? undefined, {\n enabled: pending === 'campaign' && Boolean(hostId),\n })\n\n // The reference a write names is the row's own site and document.\n const writers = useMemo(() => {\n const byId = new Map(rows.map((row) => [row.$id, row]))\n return crmBulkWriters(firestore, (id) => {\n const row = byId.get(id)\n return doc(firestore, 'hosts', row?.hostId ?? '', 'leads', row?.leadId ?? id)\n })\n }, [firestore, rows])\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setCampaignIds([])\n setPending(action)\n }\n\n const runPlan = useCallback(\n (plan: CrmBulkPlan, done: (count: number) => string) =>\n apply({\n attempted: plan.writes.length,\n skipped: plan.skipped,\n job: () => runCrmBulkWrites(writers, plan.writes, (write) => write.label),\n done,\n }),\n [apply, writers],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending) return\n const action = pending\n setPending(null)\n const writes: CrmBulkWrite[] = []\n const skipped: CrmBulkSkip[] = []\n if (action === 'owner') {\n for (const lead of selectedRows) {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: { ownerUid: value ? value : deleteField(), updatedAt: serverTimestamp() },\n })\n }\n await runPlan(\n { writes, skipped },\n (count) => (value ? `Owner set on ${countNoun(count, NOUN)}` : `Owner cleared on ${countNoun(count, NOUN)}`),\n )\n return\n }\n if (action === 'campaign') {\n // Added to what each lead carries — `arrayUnion`, the way a sequence's\n // enroll and the import add one — never in place of it.\n for (const lead of selectedRows) {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: { campaignIds: arrayUnion(...campaignIds), updatedAt: serverTimestamp() },\n })\n }\n await runPlan(\n { writes, skipped },\n (count) =>\n `Added ${countNoun(count, NOUN)} to ${campaignIds.length === 1 ? 'the campaign' : `${campaignIds.length} campaigns`}`,\n )\n return\n }\n if (action === 'status') {\n const status = value as CrmLeadStatus\n for (const lead of selectedRows) {\n const current = crmLeadStatus(lead)\n if (lead.convertedContactId) {\n skipped.push({ label: labelOf(lead), reason: 'was converted' })\n } else if (current === status) {\n skipped.push({\n label: labelOf(lead),\n reason: `already ${CRM_LEAD_STATUS_LABELS[status]}`,\n })\n } else {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n status,\n // Reopening an unqualified lead clears its reason, as the row does.\n ...(current === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n updatedAt: serverTimestamp(),\n },\n })\n }\n }\n await runPlan(\n { writes, skipped },\n (count) => `Status set on ${countNoun(count, NOUN)}`,\n )\n return\n }\n const reason = value.trim().slice(0, UNQUALIFY_REASON_MAX)\n for (const lead of selectedRows) {\n if (lead.convertedContactId) {\n skipped.push({ label: labelOf(lead), reason: 'was converted' })\n } else if (!isCrmLeadOpen(lead)) {\n skipped.push({ label: labelOf(lead), reason: 'is already closed' })\n } else {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n status: 'unqualified' satisfies CrmLeadStatus,\n unqualifiedReason: reason,\n updatedAt: serverTimestamp(),\n },\n })\n }\n }\n await runPlan(\n { writes, skipped },\n (count) => `Marked ${countNoun(count, NOUN)} unqualified`,\n )\n }, [pending, value, campaignIds, selectedRows, runPlan])\n\n const handleExport = useCallback(() => {\n downloadTextFile('leads-selected.csv', 'text/csv', leadsCsv(selectedRows, csv))\n }, [selectedRows, csv])\n\n const canApply =\n pending === 'owner' ||\n (pending === 'campaign'\n ? campaignIds.length > 0\n : pending === 'status'\n ? Boolean(value)\n : Boolean(value.trim()))\n\n return (\n <CrmBulkBarFrame\n count={selected.length}\n noun={NOUN}\n busy={busy}\n onClear={() => onSelectedChange([])}\n report={report}\n onDismissReport={dismissReport}\n extras={\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={canApply}\n applyLabel={pending === 'unqualify' ? 'Unqualify' : pending === 'campaign' ? 'Add' : undefined}\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'owner' ? (\n <LeadOwnerSelect value={value} onChange={setValue} roster={roster} size=\"medium\" />\n ) : pending === 'status' ? (\n <TextField\n select\n size=\"small\"\n label=\"Status\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText=\"Closing a lead goes through Unqualify, which asks why.\"\n >\n {SETTABLE_STATUSES.map((status) => (\n <MenuItem key={status} value={status}>\n {CRM_LEAD_STATUS_LABELS[status]}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'unqualify' ? (\n <TextField\n label=\"Reason\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n multiline\n minRows={2}\n autoFocus\n helperText=\"One reason for every selected lead, kept on each so it can be counted later.\"\n slotProps={{ htmlInput: { maxLength: UNQUALIFY_REASON_MAX } }}\n />\n ) : pending === 'campaign' ? (\n <CampaignPicker\n options={campaigns.options}\n value={campaignIds}\n onChange={setCampaignIds}\n helperText=\"Added to the campaigns each selected lead is already in. It does not decide who a campaign mails.\"\n empty={campaigns.ready && !campaigns.options.length}\n emptyText=\"This site has no campaigns yet. Create one from Marketing to file leads under it.\"\n />\n ) : null}\n </CrmBulkValueDialog>\n }\n >\n <Button size=\"small\" disabled={busy} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('status')}>\n {'Set status'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('unqualify')}>\n {'Unqualify'}\n </Button>\n {hostId ? (\n <Button size=\"small\" disabled={busy} onClick={() => openAction('campaign')}>\n {'Add to campaign'}\n </Button>\n ) : null}\n <Button size=\"small\" disabled={busy} onClick={handleExport}>\n {'Export CSV'}\n </Button>\n {/*\n The selection's file is the rows on screen; this one is the whole\n collection, streamed by the server (AGL-2662).\n */}\n <CrmExportAllButton\n resource=\"leads\"\n orgId={orgId}\n hostId={hostId}\n disabled={busy}\n />\n </CrmBulkBarFrame>\n )\n}\nLeadsBulkBarBody.displayName = 'LeadsBulkBarBody'\n\nexport default LeadsBulkBar\n"],"names":["CRM_LEAD_STATUS_LABELS","crmLeadStatus","isCrmLeadOpen","CampaignPicker","useFirestore","useHostCampaigns","Button","MenuItem","TextField","arrayUnion","deleteField","doc","serverTimestamp","useCallback","useMemo","useState","useCrmBulkApply","downloadTextFile","crmBulkWriters","runCrmBulkWrites","leadsCsv","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","LeadOwnerSelect","UNQUALIFY_REASON_MAX","NOUN","singular","plural","ACTION_TITLES","owner","status","unqualify","campaign","SETTABLE_STATUSES","labelOf","lead","String","leadId","LeadsBulkBar","props","selected","length","LeadsBulkBarBody","displayName","rows","onSelectedChange","roster","csv","orgId","hostId","firestore","busy","report","apply","dismissReport","recordKind","selectedRows","chosen","Set","filter","row","has","$id","pending","setPending","value","setValue","campaignIds","setCampaignIds","campaigns","undefined","enabled","Boolean","writers","byId","Map","map","id","get","openAction","action","runPlan","plan","done","attempted","writes","skipped","job","write","label","handleApply","push","kind","data","ownerUid","updatedAt","count","current","convertedContactId","reason","unqualifiedReason","trim","slice","handleExport","canApply","noun","onClear","onDismissReport","extras","open","title","applyLabel","onClose","onApply","onChange","size","select","event","target","helperText","multiline","minRows","autoFocus","slotProps","htmlInput","maxLength","options","empty","ready","emptyText","disabled","onClick","resource"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/leads-bulk-bar.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * The bar over the Leads table, for whatever rows are ticked (AGL-2662).\n *\n * Hand them to an owner, set their status, unqualify them with one reason,\n * or take them into a spreadsheet. Every act here is a document write and\n * nothing more — a lead's status and owner are the team's own notes on a\n * capture, with no event and no notification behind them, which is why the\n * row's inline select writes them client-direct — so the whole bar goes\n * through the shared batched runner under the same rules.\n *\n * ## A row names its own site\n *\n * A lead lives at `hosts/{hostId}/leads/{leadId}`, and at the organization\n * level the selection spans sites, so the reference for a write is built\n * from the ROW rather than from a scope the bar was handed: the row knows\n * its site, the bar need not. Under a site every row names the same one.\n *\n * ## What is declined, by name\n *\n * A converted lead's status IS its conversion, stamped by the route, and\n * the bar leaves it alone under Set status and Unqualify the way the row\n * menu and the inline select do. A lead already closed is not unqualified\n * twice. A lead already at the asked status is skipped rather than rewritten\n * so \"Nothing to change\" means what it says.\n */\n\nimport {\n CRM_LEAD_STATUS_LABELS,\n type CrmLeadFields,\n type CrmLeadStatus,\n crmLeadStatus,\n isCrmLeadOpen,\n readCampaignIds,\n} from '@aglyn/aglyn'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { useFirestore, useHostCampaigns } from '@aglyn/tenant-feature-instance'\nimport { Button, MenuItem, TextField } from '@mui/material'\nimport { arrayUnion, deleteField, doc, serverTimestamp } from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { useCampaignFilingLog } from '../hooks/use-campaign-filing-log'\nimport { useCrmBulkApply } from '../hooks/use-crm-bulk-apply'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport {\n type CrmBulkPlan,\n type CrmBulkSkip,\n type CrmBulkWrite,\n crmBulkWriters,\n runCrmBulkWrites,\n} from '../model/crm-bulk-writes'\nimport { type LeadCsvOptions, leadsCsv } from '../model/leads-csv'\nimport {\n type CrmBulkNoun,\n CrmBulkBarFrame,\n CrmBulkValueDialog,\n countNoun,\n} from './crm-bulk-bar-frame'\nimport CrmExportAllButton from './crm-export-all-button'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { UNQUALIFY_REASON_MAX } from './lead-unqualify-dialog'\n\n/**\n * One row of the Leads list as the bar needs it: the document's fields,\n * the grid's key, and which document under which site a write names.\n */\nexport type LeadBulkRow = Record<string, unknown> &\n CrmLeadFields & { $id: string; leadId: string; hostId: string }\n\nexport interface LeadsBulkBarProps {\n rows: readonly LeadBulkRow[]\n selected: readonly string[]\n onSelectedChange: (ids: string[]) => void\n /** The section's roster — already read for the Owner column. */\n roster: OrgMemberOptions\n /** How the export names the owner and, at the org level, the site — the list's own. */\n csv?: LeadCsvOptions\n /** The organization these leads belong to; null while it is unresolved. */\n orgId?: string | null\n /**\n * The site the list is read under, or `null` at the organization level\n * where the complete export spans every site (AGL-2662).\n */\n hostId?: string | null\n /**\n * The org the shell passed, for the scope a filing entry carries\n * (AGL-3274): Add to campaign writes one on each lead's Activity, and\n * the entry is visible to whoever sees a record made on this site.\n */\n org?: Record<string, unknown> | null\n}\n\nconst NOUN: CrmBulkNoun = { singular: 'lead', plural: 'leads' }\n\ntype PendingAction = 'owner' | 'status' | 'unqualify' | 'campaign'\n\nconst ACTION_TITLES: Record<PendingAction, string> = {\n owner: 'Set the owner',\n status: 'Set the status',\n unqualify: 'Unqualify',\n campaign: 'Add to campaign',\n}\n\n/** The statuses the bar can set — the open ones; closing goes through Unqualify. */\nconst SETTABLE_STATUSES: readonly CrmLeadStatus[] = ['new', 'working']\n\n/** The label a report lists a lead under — its name, else its address. */\nconst labelOf = (lead: LeadBulkRow): string =>\n String(lead['name'] || lead['email'] || lead.leadId)\n\nexport function LeadsBulkBar(props: LeadsBulkBarProps) {\n if (!props.selected.length) return null\n return <LeadsBulkBarBody {...props} />\n}\nLeadsBulkBar.displayName = 'LeadsBulkBar'\n\nfunction LeadsBulkBarBody(props: LeadsBulkBarProps) {\n const { rows, selected, onSelectedChange, roster, csv } = props\n const orgId = props.orgId ?? null\n const hostId = props.hostId ?? null\n const firestore = useFirestore()\n const { busy, report, apply, dismissReport } = useCrmBulkApply({ recordKind: 'lead' })\n\n const selectedRows = useMemo(() => {\n const chosen = new Set(selected)\n return rows.filter((row) => chosen.has(row.$id))\n }, [rows, selected])\n\n const [pending, setPending] = useState<PendingAction | null>(null)\n const [value, setValue] = useState('')\n /*\n * The campaigns to add the selection to (AGL-3254), and the site's\n * containers the picker offers — read only while that dialog is open.\n * Under a site alone: a campaign belongs to one site, and at the\n * organization level a selection spans them.\n */\n const [campaignIds, setCampaignIds] = useState<string[]>([])\n const campaigns = useHostCampaigns(hostId ?? undefined, {\n enabled: pending === 'campaign' && Boolean(hostId),\n })\n const logFiling = useCampaignFilingLog({ orgId, hostId, org: props.org })\n\n // The reference a write names is the row's own site and document.\n const writers = useMemo(() => {\n const byId = new Map(rows.map((row) => [row.$id, row]))\n return crmBulkWriters(firestore, (id) => {\n const row = byId.get(id)\n return doc(firestore, 'hosts', row?.hostId ?? '', 'leads', row?.leadId ?? id)\n })\n }, [firestore, rows])\n\n const openAction = (action: PendingAction) => {\n setValue('')\n setCampaignIds([])\n setPending(action)\n }\n\n const runPlan = useCallback(\n (plan: CrmBulkPlan, done: (count: number) => string) =>\n apply({\n attempted: plan.writes.length,\n skipped: plan.skipped,\n job: () => runCrmBulkWrites(writers, plan.writes, (write) => write.label),\n done,\n }),\n [apply, writers],\n )\n\n const handleApply = useCallback(async () => {\n if (!pending) return\n const action = pending\n setPending(null)\n const writes: CrmBulkWrite[] = []\n const skipped: CrmBulkSkip[] = []\n if (action === 'owner') {\n for (const lead of selectedRows) {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: { ownerUid: value ? value : deleteField(), updatedAt: serverTimestamp() },\n })\n }\n await runPlan(\n { writes, skipped },\n (count) => (value ? `Owner set on ${countNoun(count, NOUN)}` : `Owner cleared on ${countNoun(count, NOUN)}`),\n )\n return\n }\n if (action === 'campaign') {\n // Added to what each lead carries — `arrayUnion`, the way a sequence's\n // enroll and the import add one — never in place of it.\n for (const lead of selectedRows) {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: { campaignIds: arrayUnion(...campaignIds), updatedAt: serverTimestamp() },\n })\n }\n const outcome = await runPlan(\n { writes, skipped },\n (count) =>\n `Added ${countNoun(count, NOUN)} to ${campaignIds.length === 1 ? 'the campaign' : `${campaignIds.length} campaigns`}`,\n )\n // What landed is written on each lead's Activity (AGL-3274): one\n // \"Filed under\" per campaign the lead was not already in, and none\n // for a lead the store refused — the row's own document is the\n // membership, and the entry only records the act.\n const refused = new Set(outcome.refused.map((entry) => entry.label))\n const named = campaignIds.map((campaignId) => ({\n id: campaignId,\n name: campaigns.options.find((option) => option.value === campaignId)?.label ?? '',\n }))\n for (const lead of selectedRows) {\n if (refused.has(labelOf(lead))) continue\n const already = readCampaignIds(lead)\n const filed = named.filter((campaign) => !already.includes(campaign.id))\n if (filed.length) await logFiling({ leadId: lead.leadId }, { filed })\n }\n return\n }\n if (action === 'status') {\n const status = value as CrmLeadStatus\n for (const lead of selectedRows) {\n const current = crmLeadStatus(lead)\n if (lead.convertedContactId) {\n skipped.push({ label: labelOf(lead), reason: 'was converted' })\n } else if (current === status) {\n skipped.push({\n label: labelOf(lead),\n reason: `already ${CRM_LEAD_STATUS_LABELS[status]}`,\n })\n } else {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n status,\n // Reopening an unqualified lead clears its reason, as the row does.\n ...(current === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n updatedAt: serverTimestamp(),\n },\n })\n }\n }\n await runPlan(\n { writes, skipped },\n (count) => `Status set on ${countNoun(count, NOUN)}`,\n )\n return\n }\n const reason = value.trim().slice(0, UNQUALIFY_REASON_MAX)\n for (const lead of selectedRows) {\n if (lead.convertedContactId) {\n skipped.push({ label: labelOf(lead), reason: 'was converted' })\n } else if (!isCrmLeadOpen(lead)) {\n skipped.push({ label: labelOf(lead), reason: 'is already closed' })\n } else {\n writes.push({\n id: lead.$id,\n label: labelOf(lead),\n kind: 'update',\n data: {\n status: 'unqualified' satisfies CrmLeadStatus,\n unqualifiedReason: reason,\n updatedAt: serverTimestamp(),\n },\n })\n }\n }\n await runPlan(\n { writes, skipped },\n (count) => `Marked ${countNoun(count, NOUN)} unqualified`,\n )\n }, [pending, value, campaignIds, campaigns.options, selectedRows, runPlan, logFiling])\n\n const handleExport = useCallback(() => {\n downloadTextFile('leads-selected.csv', 'text/csv', leadsCsv(selectedRows, csv))\n }, [selectedRows, csv])\n\n const canApply =\n pending === 'owner' ||\n (pending === 'campaign'\n ? campaignIds.length > 0\n : pending === 'status'\n ? Boolean(value)\n : Boolean(value.trim()))\n\n return (\n <CrmBulkBarFrame\n count={selected.length}\n noun={NOUN}\n busy={busy}\n onClear={() => onSelectedChange([])}\n report={report}\n onDismissReport={dismissReport}\n extras={\n <CrmBulkValueDialog\n open={pending !== null}\n title={pending ? ACTION_TITLES[pending] : ''}\n count={selected.length}\n noun={NOUN}\n busy={busy}\n canApply={canApply}\n applyLabel={pending === 'unqualify' ? 'Unqualify' : pending === 'campaign' ? 'Add' : undefined}\n onClose={() => setPending(null)}\n onApply={() => void handleApply()}\n >\n {pending === 'owner' ? (\n <LeadOwnerSelect value={value} onChange={setValue} roster={roster} size=\"medium\" />\n ) : pending === 'status' ? (\n <TextField\n select\n size=\"small\"\n label=\"Status\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n helperText=\"Closing a lead goes through Unqualify, which asks why.\"\n >\n {SETTABLE_STATUSES.map((status) => (\n <MenuItem key={status} value={status}>\n {CRM_LEAD_STATUS_LABELS[status]}\n </MenuItem>\n ))}\n </TextField>\n ) : pending === 'unqualify' ? (\n <TextField\n label=\"Reason\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n multiline\n minRows={2}\n autoFocus\n helperText=\"One reason for every selected lead, kept on each so it can be counted later.\"\n slotProps={{ htmlInput: { maxLength: UNQUALIFY_REASON_MAX } }}\n />\n ) : pending === 'campaign' ? (\n <CampaignPicker\n options={campaigns.options}\n value={campaignIds}\n onChange={setCampaignIds}\n helperText=\"Added to the campaigns each selected lead is already in. It does not decide who a campaign mails.\"\n empty={campaigns.ready && !campaigns.options.length}\n emptyText=\"This site has no campaigns yet. Create one from Marketing to file leads under it.\"\n />\n ) : null}\n </CrmBulkValueDialog>\n }\n >\n <Button size=\"small\" disabled={busy} onClick={() => openAction('owner')}>\n {'Set owner'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('status')}>\n {'Set status'}\n </Button>\n <Button size=\"small\" disabled={busy} onClick={() => openAction('unqualify')}>\n {'Unqualify'}\n </Button>\n {hostId ? (\n <Button size=\"small\" disabled={busy} onClick={() => openAction('campaign')}>\n {'Add to campaign'}\n </Button>\n ) : null}\n <Button size=\"small\" disabled={busy} onClick={handleExport}>\n {'Export CSV'}\n </Button>\n {/*\n The selection's file is the rows on screen; this one is the whole\n collection, streamed by the server (AGL-2662).\n */}\n <CrmExportAllButton\n resource=\"leads\"\n orgId={orgId}\n hostId={hostId}\n disabled={busy}\n />\n </CrmBulkBarFrame>\n )\n}\nLeadsBulkBarBody.displayName = 'LeadsBulkBarBody'\n\nexport default LeadsBulkBar\n"],"names":["CRM_LEAD_STATUS_LABELS","crmLeadStatus","isCrmLeadOpen","readCampaignIds","CampaignPicker","useFirestore","useHostCampaigns","Button","MenuItem","TextField","arrayUnion","deleteField","doc","serverTimestamp","useCallback","useMemo","useState","useCampaignFilingLog","useCrmBulkApply","downloadTextFile","crmBulkWriters","runCrmBulkWrites","leadsCsv","CrmBulkBarFrame","CrmBulkValueDialog","countNoun","CrmExportAllButton","LeadOwnerSelect","UNQUALIFY_REASON_MAX","NOUN","singular","plural","ACTION_TITLES","owner","status","unqualify","campaign","SETTABLE_STATUSES","labelOf","lead","String","leadId","LeadsBulkBar","props","selected","length","LeadsBulkBarBody","displayName","rows","onSelectedChange","roster","csv","orgId","hostId","firestore","busy","report","apply","dismissReport","recordKind","selectedRows","chosen","Set","filter","row","has","$id","pending","setPending","value","setValue","campaignIds","setCampaignIds","campaigns","undefined","enabled","Boolean","logFiling","org","writers","byId","Map","map","id","get","openAction","action","runPlan","plan","done","attempted","writes","skipped","job","write","label","handleApply","push","kind","data","ownerUid","updatedAt","count","outcome","refused","entry","named","campaignId","name","options","find","option","already","filed","includes","current","convertedContactId","reason","unqualifiedReason","trim","slice","handleExport","canApply","noun","onClear","onDismissReport","extras","open","title","applyLabel","onClose","onApply","onChange","size","select","event","target","helperText","multiline","minRows","autoFocus","slotProps","htmlInput","maxLength","empty","ready","emptyText","disabled","onClick","resource"],"mappings":"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"}
|
|
@@ -25,6 +25,8 @@ import { useOrgMemberOptions } from "../hooks/use-org-member-options.js";
|
|
|
25
25
|
import { useCrmOrgMount } from "../hooks/use-crm-org-mount.js";
|
|
26
26
|
import { useCrmSavedView } from "../hooks/use-crm-saved-view.js";
|
|
27
27
|
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
28
|
+
import { useContactFieldDefinitions } from "../hooks/use-contact-field-definitions.js";
|
|
29
|
+
import { customFieldColumns } from "./contact-custom-columns.js";
|
|
28
30
|
import { useCrmViewGrid } from "../hooks/use-crm-view-grid.js";
|
|
29
31
|
import { CRM_LIST_SLOTS, CrmColumnOrderProvider } from "./crm-column-menu.js";
|
|
30
32
|
import { useOrgLeads } from "../hooks/use-org-leads.js";
|
|
@@ -105,6 +107,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
105
107
|
});
|
|
106
108
|
const mount = useCrmOrgMount();
|
|
107
109
|
const roster = useOrgMemberOptions(orgId);
|
|
110
|
+
// The org's lead fields, for the optional columns below (AGL-3272).
|
|
111
|
+
const leadFields = useContactFieldDefinitions(orgId, 'lead');
|
|
108
112
|
const routes = crmRoutes(basePath != null ? basePath : '');
|
|
109
113
|
// Under a site: the site's own window, rows keyed by document id.
|
|
110
114
|
const site = useFirestoreCollection(()=>hostId ? query(collection(firestore, 'hosts', hostId, 'leads'), orderBy('lastSeenAtMs', 'desc'), limit(LEADS_WINDOW + 1)) : null, [
|
|
@@ -341,6 +345,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
341
345
|
ownerUid: values.ownerUid
|
|
342
346
|
} : {}, values.notes ? {
|
|
343
347
|
notes: values.notes
|
|
348
|
+
} : {}, values.custom ? {
|
|
349
|
+
custom: values.custom
|
|
344
350
|
} : {}, {
|
|
345
351
|
status: values.status
|
|
346
352
|
}));
|
|
@@ -542,6 +548,11 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
542
548
|
return leadTimeLabel((_row_lastSeenAtMs = row['lastSeenAtMs']) != null ? _row_lastSeenAtMs : row['createdAt']);
|
|
543
549
|
}
|
|
544
550
|
},
|
|
551
|
+
// The org's lead fields as optional columns (AGL-3272), read off the
|
|
552
|
+
// row's own `custom` map the way the contacts list reads its own.
|
|
553
|
+
// Ahead of the row menu, so the overflow stays at the right edge
|
|
554
|
+
// however many fields the org has defined.
|
|
555
|
+
...customFieldColumns(leadFields.active),
|
|
545
556
|
{
|
|
546
557
|
field: 'actions',
|
|
547
558
|
headerName: '',
|
|
@@ -620,7 +631,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
620
631
|
writeLead,
|
|
621
632
|
hostId,
|
|
622
633
|
mount,
|
|
623
|
-
campaignName
|
|
634
|
+
campaignName,
|
|
635
|
+
leadFields.active
|
|
624
636
|
]);
|
|
625
637
|
/* The column and sort models are the view's (AGL-2617). */ const grid = useCrmViewGrid(views, columns);
|
|
626
638
|
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
@@ -785,7 +797,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
785
797
|
roster: roster,
|
|
786
798
|
csv: csvOptions,
|
|
787
799
|
orgId: orgId,
|
|
788
|
-
hostId: hostId
|
|
800
|
+
hostId: hostId,
|
|
801
|
+
org: org
|
|
789
802
|
}),
|
|
790
803
|
/*#__PURE__*/ _jsx(CrmColumnOrderProvider, {
|
|
791
804
|
value: grid.columnOrder,
|
|
@@ -836,6 +849,7 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
|
|
|
836
849
|
busy: createBusy,
|
|
837
850
|
error: createError,
|
|
838
851
|
roster: roster,
|
|
852
|
+
orgId: orgId,
|
|
839
853
|
onSubmit: (values)=>void handleCreate(values)
|
|
840
854
|
}),
|
|
841
855
|
/*#__PURE__*/ _jsx(AssignOwnerDialog, {
|