@aglyn/plugins-crm 1.0.0-beta.164 → 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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { campaignMembershipUnchanged, campaignMembershipValue, pluginDocsHelp, readCampaignIds } from "@aglyn/aglyn";
|
|
19
|
+
import CampaignPicker from "@aglyn/shared-ui-email-campaigns/components/campaign-picker.component";
|
|
20
|
+
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
21
|
+
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
22
|
+
import { useFirestore, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
|
|
23
|
+
import { Button, Stack } from "@mui/material";
|
|
24
|
+
import { doc, serverTimestamp, updateDoc } from "firebase/firestore";
|
|
25
|
+
import { useEffect, useState } from "react";
|
|
26
|
+
/** The helper line under the picker — the contact card's, word for word. */ export const LEAD_CAMPAIGNS_HELPER_TEXT = 'Your own filing. It never adds anyone to a send — a campaign mails its lists.';
|
|
27
|
+
/**
|
|
28
|
+
* The campaigns a lead is filed under, as NAMES for the record header.
|
|
29
|
+
*
|
|
30
|
+
* Resolved from the site's containers; an id with no live container — a
|
|
31
|
+
* campaign the console deleted, or one still loading — is left out rather
|
|
32
|
+
* than shown, because a document id on a chip tells a reader nothing and
|
|
33
|
+
* looks like a fault. The picker below keeps such an id visible as itself,
|
|
34
|
+
* so the filing is never silently lost; the header only ever names.
|
|
35
|
+
*/ export function leadCampaignNames(lead, options) {
|
|
36
|
+
const labels = new Map(options.map((option)=>[
|
|
37
|
+
option.value,
|
|
38
|
+
option.label
|
|
39
|
+
]));
|
|
40
|
+
return readCampaignIds(lead).map((id)=>labels.get(id)).filter((name)=>Boolean(name));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).
|
|
44
|
+
*
|
|
45
|
+
* The contact's Relationship card has carried this picker since AGL-2596;
|
|
46
|
+
* the lead's page had none, so a lead filed under a campaign by the New
|
|
47
|
+
* lead drawer, the bulk bar or a sequence's enroll showed nothing of it —
|
|
48
|
+
* the one field on the record the reader had no way to see or change.
|
|
49
|
+
*
|
|
50
|
+
* The same control, the same value helper, the same helper line. The save
|
|
51
|
+
* is the lead's own client door — a one-field `update` on
|
|
52
|
+
* `hosts/{hostId}/leads`, as the properties card writes status and notes —
|
|
53
|
+
* guarded by the seed like every draft edited over a cached read, and
|
|
54
|
+
* `campaignMembershipUnchanged` keeps Save quiet while the picker only
|
|
55
|
+
* reorders what is stored.
|
|
56
|
+
*
|
|
57
|
+
* It is FILING, not audience: the helper says so, in the sentence the
|
|
58
|
+
* contact card uses, because a reader who finds a campaign picker on a
|
|
59
|
+
* person's page assumes the opposite.
|
|
60
|
+
*/ export function LeadCampaignsCard(props) {
|
|
61
|
+
const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props;
|
|
62
|
+
const firestore = useFirestore();
|
|
63
|
+
const { enqueueSnackbar } = useSnackbar();
|
|
64
|
+
const stored = readCampaignIds(lead);
|
|
65
|
+
const [selected, setSelected] = useState(stored);
|
|
66
|
+
const [dirty, setDirty] = useState(false);
|
|
67
|
+
const [saving, setSaving] = useState(false);
|
|
68
|
+
// A newer membership from the server replaces an UNEDITED selection — a
|
|
69
|
+
// sequence's enroll or a bulk add lands while the page is open — and an
|
|
70
|
+
// edited one is the reader's, decided by the guard on save.
|
|
71
|
+
const storedKey = stored.join(',');
|
|
72
|
+
useEffect(()=>{
|
|
73
|
+
if (!dirty) setSelected(stored);
|
|
74
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
75
|
+
}, [
|
|
76
|
+
storedKey,
|
|
77
|
+
dirty
|
|
78
|
+
]);
|
|
79
|
+
const unchanged = campaignMembershipUnchanged(stored, selected);
|
|
80
|
+
const empty = optionsReady && !options.length;
|
|
81
|
+
const save = async ()=>{
|
|
82
|
+
if (unchanged) return;
|
|
83
|
+
setSaving(true);
|
|
84
|
+
const next = campaignMembershipValue(selected);
|
|
85
|
+
const verdict = await writeGuardedBySeed({
|
|
86
|
+
subject: 'lead',
|
|
87
|
+
fromCache,
|
|
88
|
+
unreadable: leadStatus === 'error'
|
|
89
|
+
}, async ()=>{
|
|
90
|
+
await updateDoc(doc(firestore, 'hosts', hostId, 'leads', leadId), {
|
|
91
|
+
campaignIds: next,
|
|
92
|
+
updatedAt: serverTimestamp()
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
setSaving(false);
|
|
96
|
+
if (!verdict.ok) {
|
|
97
|
+
var _verdict_message;
|
|
98
|
+
enqueueSnackbar((_verdict_message = verdict.message) != null ? _verdict_message : 'The filing could not be saved.', {
|
|
99
|
+
variant: 'warning'
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
setDirty(false);
|
|
104
|
+
enqueueSnackbar('Filing saved', {
|
|
105
|
+
variant: 'success',
|
|
106
|
+
persist: false
|
|
107
|
+
});
|
|
108
|
+
onFiled == null ? void 0 : onFiled({
|
|
109
|
+
added: next.filter((id)=>!stored.includes(id)),
|
|
110
|
+
removed: stored.filter((id)=>!next.includes(id))
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
return /*#__PURE__*/ _jsx(CardDisplay, {
|
|
114
|
+
header: 'Campaigns',
|
|
115
|
+
help: pluginDocsHelp('crmLeads', {
|
|
116
|
+
anchor: '#a-leads-page'
|
|
117
|
+
}),
|
|
118
|
+
contentGutterX: true,
|
|
119
|
+
contentGutterY: true,
|
|
120
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
121
|
+
spacing: 2,
|
|
122
|
+
children: [
|
|
123
|
+
/*#__PURE__*/ _jsx(CampaignPicker, {
|
|
124
|
+
options: options,
|
|
125
|
+
value: selected,
|
|
126
|
+
onChange: (next)=>{
|
|
127
|
+
setSelected(next);
|
|
128
|
+
setDirty(true);
|
|
129
|
+
},
|
|
130
|
+
label: "Filed under campaigns",
|
|
131
|
+
helperText: LEAD_CAMPAIGNS_HELPER_TEXT,
|
|
132
|
+
disabled: saving,
|
|
133
|
+
empty: empty,
|
|
134
|
+
emptyText: "This site has no campaigns yet. Create one from Marketing to file leads under it."
|
|
135
|
+
}),
|
|
136
|
+
empty ? null : /*#__PURE__*/ _jsx(Stack, {
|
|
137
|
+
direction: "row",
|
|
138
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
139
|
+
size: "small",
|
|
140
|
+
variant: "outlined",
|
|
141
|
+
disabled: saving || unchanged,
|
|
142
|
+
onClick: ()=>void save().catch((error)=>{
|
|
143
|
+
console.error(error);
|
|
144
|
+
enqueueSnackbar(error instanceof Error && error.message ? error.message : 'An error has occurred', {
|
|
145
|
+
variant: 'error',
|
|
146
|
+
allowDuplicate: true
|
|
147
|
+
});
|
|
148
|
+
setSaving(false);
|
|
149
|
+
}),
|
|
150
|
+
children: saving ? 'Saving…' : 'Save filing'
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
]
|
|
154
|
+
})
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
LeadCampaignsCard.displayName = 'LeadCampaignsCard';
|
|
158
|
+
export default LeadCampaignsCard;
|
|
159
|
+
|
|
160
|
+
//# sourceMappingURL=lead-campaigns-card.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-campaigns-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n campaignMembershipUnchanged,\n campaignMembershipValue,\n pluginDocsHelp,\n readCampaignIds,\n} from '@aglyn/aglyn'\nimport CampaignPicker, {\n type CampaignPickerOption,\n} from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack } from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\n\n/** The helper line under the picker — the contact card's, word for word. */\nexport const LEAD_CAMPAIGNS_HELPER_TEXT =\n 'Your own filing. It never adds anyone to a send — a campaign mails its lists.'\n\n/**\n * The campaigns a lead is filed under, as NAMES for the record header.\n *\n * Resolved from the site's containers; an id with no live container — a\n * campaign the console deleted, or one still loading — is left out rather\n * than shown, because a document id on a chip tells a reader nothing and\n * looks like a fault. The picker below keeps such an id visible as itself,\n * so the filing is never silently lost; the header only ever names.\n */\nexport function leadCampaignNames(\n lead: Record<string, unknown> | null | undefined,\n options: readonly CampaignPickerOption[],\n): string[] {\n const labels = new Map(options.map((option) => [option.value, option.label]))\n return readCampaignIds(lead)\n .map((id) => labels.get(id))\n .filter((name): name is string => Boolean(name))\n}\n\nexport interface LeadCampaignsCardProps {\n hostId: string\n leadId: string\n lead: Record<string, unknown>\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n /** The site's campaigns, read once by the page for this card and the header. */\n options: readonly CampaignPickerOption[]\n /** The campaign read has answered — false while it settles. */\n optionsReady: boolean\n /**\n * What a saved filing changed, once it has landed: the ids added to and\n * removed from the lead's membership. The page files the activity\n * entries (AGL-3274); the card only knows the document.\n */\n onFiled?: (change: { added: string[]; removed: string[] }) => void\n}\n\n/**\n * WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).\n *\n * The contact's Relationship card has carried this picker since AGL-2596;\n * the lead's page had none, so a lead filed under a campaign by the New\n * lead drawer, the bulk bar or a sequence's enroll showed nothing of it —\n * the one field on the record the reader had no way to see or change.\n *\n * The same control, the same value helper, the same helper line. The save\n * is the lead's own client door — a one-field `update` on\n * `hosts/{hostId}/leads`, as the properties card writes status and notes —\n * guarded by the seed like every draft edited over a cached read, and\n * `campaignMembershipUnchanged` keeps Save quiet while the picker only\n * reorders what is stored.\n *\n * It is FILING, not audience: the helper says so, in the sentence the\n * contact card uses, because a reader who finds a campaign picker on a\n * person's page assumes the opposite.\n */\nexport function LeadCampaignsCard(props: LeadCampaignsCardProps) {\n const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const stored = readCampaignIds(lead)\n const [selected, setSelected] = useState<string[]>(stored)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n // A newer membership from the server replaces an UNEDITED selection — a\n // sequence's enroll or a bulk add lands while the page is open — and an\n // edited one is the reader's, decided by the guard on save.\n const storedKey = stored.join(',')\n useEffect(() => {\n if (!dirty) setSelected(stored)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storedKey, dirty])\n\n const unchanged = campaignMembershipUnchanged(stored, selected)\n const empty = optionsReady && !options.length\n\n const save = async () => {\n if (unchanged) return\n setSaving(true)\n const next = campaignMembershipValue(selected)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n await updateDoc(doc(firestore, 'hosts', hostId, 'leads', leadId), {\n campaignIds: next,\n updatedAt: serverTimestamp(),\n })\n },\n )\n setSaving(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The filing could not be saved.', { variant: 'warning' })\n return\n }\n setDirty(false)\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n onFiled?.({\n added: next.filter((id) => !stored.includes(id)),\n removed: stored.filter((id) => !next.includes(id)),\n })\n }\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <CampaignPicker\n options={options}\n value={selected}\n onChange={(next) => {\n setSelected(next)\n setDirty(true)\n }}\n label=\"Filed under campaigns\"\n helperText={LEAD_CAMPAIGNS_HELPER_TEXT}\n disabled={saving}\n empty={empty}\n emptyText=\"This site has no campaigns yet. Create one from Marketing to file leads under it.\"\n />\n {empty ? null : (\n <Stack direction=\"row\">\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || unchanged}\n onClick={() => void save().catch((error: unknown) => {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n setSaving(false)\n })}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n </Stack>\n )}\n </Stack>\n </CardDisplay>\n )\n}\nLeadCampaignsCard.displayName = 'LeadCampaignsCard'\n\nexport default LeadCampaignsCard\n"],"names":["campaignMembershipUnchanged","campaignMembershipValue","pluginDocsHelp","readCampaignIds","CampaignPicker","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Stack","doc","serverTimestamp","updateDoc","useEffect","useState","LEAD_CAMPAIGNS_HELPER_TEXT","leadCampaignNames","lead","options","labels","Map","map","option","value","label","id","get","filter","name","Boolean","LeadCampaignsCard","props","hostId","leadId","leadStatus","fromCache","optionsReady","onFiled","firestore","enqueueSnackbar","stored","selected","setSelected","dirty","setDirty","saving","setSaving","storedKey","join","unchanged","empty","length","save","next","verdict","subject","unreadable","campaignIds","updatedAt","ok","message","variant","persist","added","includes","removed","header","help","anchor","contentGutterX","contentGutterY","spacing","onChange","helperText","disabled","emptyText","direction","size","onClick","catch","error","console","Error","allowDuplicate","displayName"],"mappings":"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"}
|
|
@@ -16,15 +16,17 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
18
|
import { pluginDocsHelp, normalizeContactEmail, readErasureRequestedAtMs } from "@aglyn/aglyn";
|
|
19
|
-
import { useFirestore, useFirestoreDoc, useOrgDataScope } from "@aglyn/tenant-feature-instance";
|
|
19
|
+
import { useFirestore, useFirestoreDoc, useHostCampaigns, useOrgDataScope } from "@aglyn/tenant-feature-instance";
|
|
20
20
|
import { Stack, Typography } from "@mui/material";
|
|
21
21
|
import { doc } from "firebase/firestore";
|
|
22
22
|
import { useState } from "react";
|
|
23
|
+
import { useCampaignFilingLog } from "../hooks/use-campaign-filing-log.js";
|
|
23
24
|
import { useOrgMemberOptions } from "../hooks/use-org-member-options.js";
|
|
24
25
|
import { crmRoutes } from "../model/crm-routes.js";
|
|
25
|
-
import { CrmRecordHeader } from "./crm-record-header.js";
|
|
26
|
+
import { CrmRecordChip, CrmRecordHeader } from "./crm-record-header.js";
|
|
26
27
|
import { CrmRecordInsightsZone } from "./crm-record-insights-zone.js";
|
|
27
28
|
import { useErasePersonAction } from "./erase-person-action.js";
|
|
29
|
+
import { LeadCampaignsCard, leadCampaignNames } from "./lead-campaigns-card.js";
|
|
28
30
|
import { LeadConvertDialog } from "./lead-convert-dialog.js";
|
|
29
31
|
import { LeadHistoryCard } from "./lead-history-card.js";
|
|
30
32
|
import { LeadPropertiesCard } from "./lead-properties-card.js";
|
|
@@ -52,6 +54,21 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
52
54
|
hostId
|
|
53
55
|
});
|
|
54
56
|
const roster = useOrgMemberOptions(orgId);
|
|
57
|
+
/*
|
|
58
|
+
* The site's campaigns, read once for the page (AGL-3274): the header
|
|
59
|
+
* names the ones the lead is filed under and the Campaigns card offers
|
|
60
|
+
* them. One listener, not one per surface — a lead belongs to one site,
|
|
61
|
+
* so the containers are that site's.
|
|
62
|
+
*/ const campaigns = useHostCampaigns(hostId, {
|
|
63
|
+
enabled: true
|
|
64
|
+
});
|
|
65
|
+
// A saved filing is written on the lead's Activity too (AGL-3274), one
|
|
66
|
+
// entry per campaign added or removed, by the member who saved it.
|
|
67
|
+
const logFiling = useCampaignFilingLog({
|
|
68
|
+
orgId,
|
|
69
|
+
hostId,
|
|
70
|
+
org: org
|
|
71
|
+
});
|
|
55
72
|
const [converting, setConverting] = useState(false);
|
|
56
73
|
const [unqualifying, setUnqualifying] = useState(false);
|
|
57
74
|
// The privacy erasure (AGL-2623), offered from the lead as from the
|
|
@@ -103,6 +120,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
103
120
|
children: [
|
|
104
121
|
/*#__PURE__*/ _jsx(LeadPropertiesCard, {
|
|
105
122
|
hostId: hostId,
|
|
123
|
+
orgId: orgId,
|
|
106
124
|
leadId: id,
|
|
107
125
|
lead: lead,
|
|
108
126
|
leadStatus: status,
|
|
@@ -114,7 +132,14 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
114
132
|
extraMenuItems: erase.menuItems,
|
|
115
133
|
banner: erase.banner,
|
|
116
134
|
erasurePending: erase.pendingSinceMs !== null,
|
|
117
|
-
org: org
|
|
135
|
+
org: org,
|
|
136
|
+
// The campaigns the lead is filed under (AGL-3274), by name
|
|
137
|
+
// beside the status. An id no container answers for draws no
|
|
138
|
+
// chip: the card below keeps it, the header only names.
|
|
139
|
+
extraChips: leadCampaignNames(lead, campaigns.options).map((name)=>/*#__PURE__*/ _jsx(CrmRecordChip, {
|
|
140
|
+
label: "Campaign",
|
|
141
|
+
value: name
|
|
142
|
+
}, name))
|
|
118
143
|
}),
|
|
119
144
|
/*#__PURE__*/ _jsx(CrmRecordInsightsZone, {
|
|
120
145
|
hostId: hostId,
|
|
@@ -123,6 +148,31 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
123
148
|
recordId: id,
|
|
124
149
|
name: label != null ? label : ''
|
|
125
150
|
}),
|
|
151
|
+
/*#__PURE__*/ _jsx(LeadCampaignsCard, {
|
|
152
|
+
hostId: hostId,
|
|
153
|
+
leadId: id,
|
|
154
|
+
lead: lead,
|
|
155
|
+
leadStatus: status,
|
|
156
|
+
fromCache: fromCache,
|
|
157
|
+
options: campaigns.options,
|
|
158
|
+
optionsReady: campaigns.ready,
|
|
159
|
+
onFiled: ({ added, removed })=>{
|
|
160
|
+
const named = (ids)=>ids.map((campaignId)=>{
|
|
161
|
+
var _ref;
|
|
162
|
+
var _campaigns_options_find;
|
|
163
|
+
return {
|
|
164
|
+
id: campaignId,
|
|
165
|
+
name: (_ref = (_campaigns_options_find = campaigns.options.find((option)=>option.value === campaignId)) == null ? void 0 : _campaigns_options_find.label) != null ? _ref : ''
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
void logFiling({
|
|
169
|
+
leadId: id
|
|
170
|
+
}, {
|
|
171
|
+
filed: named(added),
|
|
172
|
+
removed: named(removed)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}),
|
|
126
176
|
/*#__PURE__*/ _jsx(LeadHistoryCard, {
|
|
127
177
|
hostId: hostId,
|
|
128
178
|
leadId: id,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-detail-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n pluginDocsHelp,\n type CrmLeadFields,\n normalizeContactEmail,\n readErasureRequestedAtMs,\n} from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreDoc,\n useOrgDataScope,\n} from '@aglyn/tenant-feature-instance'\nimport { Stack, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useState } from 'react'\nimport { useOrgMemberOptions } from '../hooks/use-org-member-options'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport { CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { useErasePersonAction } from './erase-person-action'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport { LeadHistoryCard } from './lead-history-card'\nimport { LeadPropertiesCard } from './lead-properties-card'\nimport { LeadUnqualifyDialog } from './lead-unqualify-dialog'\nimport { RecordActivityCard } from './record-activity-card'\n\ntype LeadDocument = Record<string, unknown> & CrmLeadFields\n\n/**\n * `/crm/leads/{leadId}` — one lead (AGL-2608).\n *\n * The record behind a row of the Leads section: what the person did on the\n * site, who is working them, and the conversion that turns them into a\n * contact, a company and a deal. One document listen — `hosts/{hostId}/leads`\n * is host-scoped by path, so there is no `visibleTo` to filter — and one\n * roster request for the owner picker; the convert dialog's reads open only\n * when it does.\n */\nexport function LeadDetailPage(props: CrmDetailPageProps) {\n const { id, hostId, org, basePath } = props\n const firestore = useFirestore()\n const routes = crmRoutes(basePath)\n const {\n data: lead,\n status,\n fromCache,\n } = useFirestoreDoc<LeadDocument>(\n () => doc(firestore, 'hosts', hostId, 'leads', id),\n [firestore, hostId, id],\n )\n const { orgId } = useOrgDataScope({ hostId })\n const roster = useOrgMemberOptions(orgId)\n const [converting, setConverting] = useState(false)\n const [unqualifying, setUnqualifying] = useState(false)\n // The privacy erasure (AGL-2623), offered from the lead as from the\n // contact: the same request, filed by the lead's address.\n const leadEmail = lead ? normalizeContactEmail(lead['email']) : null\n const erase = useErasePersonAction({\n hostId,\n orgId,\n subject: lead && leadEmail ? { kind: 'lead', id, email: leadEmail } : null,\n requestedAtMs: readErasureRequestedAtMs(lead),\n })\n\n const label = lead ? String(lead['name'] || lead['email'] || id) : undefined\n\n if (status === 'error' || (status === 'success' && !lead)) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {status === 'error'\n ? 'This lead could not be read.'\n : 'This lead no longer exists — it may have been removed from the Inbox.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n if (!lead) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n loading\n />\n )\n }\n\n return (\n <>\n {/* The properties card is the record's lead card: it publishes the\n page heading and the trail, so the history card under it says what\n it holds rather than repeating the name. */}\n <Stack spacing={3}>\n <LeadPropertiesCard\n hostId={hostId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n basePath={basePath}\n roster={roster}\n onConvert={() => setConverting(true)}\n onUnqualify={() => setUnqualifying(true)}\n extraMenuItems={erase.menuItems}\n banner={erase.banner}\n erasurePending={erase.pendingSinceMs !== null}\n org={org}\n />\n {/* What an assistant says about where the lead stands (AGL-2917): read on its own site. */}\n <CrmRecordInsightsZone\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"lead\"\n recordId={id}\n name={label ?? ''}\n />\n <LeadHistoryCard hostId={hostId} leadId={id} lead={lead} />\n <RecordActivityCard hostId={hostId} org={org} leadId={id} />\n </Stack>\n <LeadConvertDialog\n open={converting}\n onClose={() => setConverting(false)}\n hostId={hostId}\n orgId={orgId}\n org={org as Record<string, unknown> | undefined}\n leadId={id}\n lead={lead}\n basePath={basePath}\n roster={roster}\n />\n <LeadUnqualifyDialog\n open={unqualifying}\n onClose={() => setUnqualifying(false)}\n hostId={hostId}\n leadId={id}\n leadLabel={label ?? id}\n />\n {erase.dialog}\n </>\n )\n}\nLeadDetailPage.displayName = 'LeadDetailPage'\n\nexport default LeadDetailPage\n"],"names":["pluginDocsHelp","normalizeContactEmail","readErasureRequestedAtMs","useFirestore","useFirestoreDoc","useOrgDataScope","Stack","Typography","doc","useState","useOrgMemberOptions","crmRoutes","CrmRecordHeader","CrmRecordInsightsZone","useErasePersonAction","LeadConvertDialog","LeadHistoryCard","LeadPropertiesCard","LeadUnqualifyDialog","RecordActivityCard","LeadDetailPage","props","id","hostId","org","basePath","firestore","routes","data","lead","status","fromCache","orgId","roster","converting","setConverting","unqualifying","setUnqualifying","leadEmail","erase","subject","kind","email","requestedAtMs","label","String","undefined","title","help","anchor","backHref","section","backLabel","variant","color","loading","spacing","leadId","leadStatus","onConvert","onUnqualify","extraMenuItems","menuItems","banner","erasurePending","pendingSinceMs","recordId","name","open","onClose","leadLabel","dialog","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-detail-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n pluginDocsHelp,\n type CrmLeadFields,\n normalizeContactEmail,\n readErasureRequestedAtMs,\n} from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreDoc,\n useHostCampaigns,\n useOrgDataScope,\n} from '@aglyn/tenant-feature-instance'\nimport { Stack, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useState } from 'react'\nimport { useCampaignFilingLog } from '../hooks/use-campaign-filing-log'\nimport { useOrgMemberOptions } from '../hooks/use-org-member-options'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { useErasePersonAction } from './erase-person-action'\nimport { LeadCampaignsCard, leadCampaignNames } from './lead-campaigns-card'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport { LeadHistoryCard } from './lead-history-card'\nimport { LeadPropertiesCard } from './lead-properties-card'\nimport { LeadUnqualifyDialog } from './lead-unqualify-dialog'\nimport { RecordActivityCard } from './record-activity-card'\n\ntype LeadDocument = Record<string, unknown> & CrmLeadFields\n\n/**\n * `/crm/leads/{leadId}` — one lead (AGL-2608).\n *\n * The record behind a row of the Leads section: what the person did on the\n * site, who is working them, and the conversion that turns them into a\n * contact, a company and a deal. One document listen — `hosts/{hostId}/leads`\n * is host-scoped by path, so there is no `visibleTo` to filter — and one\n * roster request for the owner picker; the convert dialog's reads open only\n * when it does.\n */\nexport function LeadDetailPage(props: CrmDetailPageProps) {\n const { id, hostId, org, basePath } = props\n const firestore = useFirestore()\n const routes = crmRoutes(basePath)\n const {\n data: lead,\n status,\n fromCache,\n } = useFirestoreDoc<LeadDocument>(\n () => doc(firestore, 'hosts', hostId, 'leads', id),\n [firestore, hostId, id],\n )\n const { orgId } = useOrgDataScope({ hostId })\n const roster = useOrgMemberOptions(orgId)\n /*\n * The site's campaigns, read once for the page (AGL-3274): the header\n * names the ones the lead is filed under and the Campaigns card offers\n * them. One listener, not one per surface — a lead belongs to one site,\n * so the containers are that site's.\n */\n const campaigns = useHostCampaigns(hostId, { enabled: true })\n // A saved filing is written on the lead's Activity too (AGL-3274), one\n // entry per campaign added or removed, by the member who saved it.\n const logFiling = useCampaignFilingLog({\n orgId,\n hostId,\n org: org as Record<string, unknown> | undefined,\n })\n const [converting, setConverting] = useState(false)\n const [unqualifying, setUnqualifying] = useState(false)\n // The privacy erasure (AGL-2623), offered from the lead as from the\n // contact: the same request, filed by the lead's address.\n const leadEmail = lead ? normalizeContactEmail(lead['email']) : null\n const erase = useErasePersonAction({\n hostId,\n orgId,\n subject: lead && leadEmail ? { kind: 'lead', id, email: leadEmail } : null,\n requestedAtMs: readErasureRequestedAtMs(lead),\n })\n\n const label = lead ? String(lead['name'] || lead['email'] || id) : undefined\n\n if (status === 'error' || (status === 'success' && !lead)) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {status === 'error'\n ? 'This lead could not be read.'\n : 'This lead no longer exists — it may have been removed from the Inbox.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n if (!lead) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n loading\n />\n )\n }\n\n return (\n <>\n {/* The properties card is the record's lead card: it publishes the\n page heading and the trail, so the history card under it says what\n it holds rather than repeating the name. */}\n <Stack spacing={3}>\n <LeadPropertiesCard\n hostId={hostId}\n orgId={orgId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n basePath={basePath}\n roster={roster}\n onConvert={() => setConverting(true)}\n onUnqualify={() => setUnqualifying(true)}\n extraMenuItems={erase.menuItems}\n banner={erase.banner}\n erasurePending={erase.pendingSinceMs !== null}\n org={org}\n // The campaigns the lead is filed under (AGL-3274), by name\n // beside the status. An id no container answers for draws no\n // chip: the card below keeps it, the header only names.\n extraChips={leadCampaignNames(lead, campaigns.options).map((name) => (\n <CrmRecordChip key={name} label=\"Campaign\" value={name} />\n ))}\n />\n {/* What an assistant says about where the lead stands (AGL-2917): read on its own site. */}\n <CrmRecordInsightsZone\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"lead\"\n recordId={id}\n name={label ?? ''}\n />\n <LeadCampaignsCard\n hostId={hostId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n options={campaigns.options}\n optionsReady={campaigns.ready}\n onFiled={({ added, removed }) => {\n const named = (ids: string[]) =>\n ids.map((campaignId) => ({\n id: campaignId,\n name: campaigns.options.find((option) => option.value === campaignId)?.label ?? '',\n }))\n void logFiling({ leadId: id }, { filed: named(added), removed: named(removed) })\n }}\n />\n <LeadHistoryCard hostId={hostId} leadId={id} lead={lead} />\n <RecordActivityCard hostId={hostId} org={org} leadId={id} />\n </Stack>\n <LeadConvertDialog\n open={converting}\n onClose={() => setConverting(false)}\n hostId={hostId}\n orgId={orgId}\n org={org as Record<string, unknown> | undefined}\n leadId={id}\n lead={lead}\n basePath={basePath}\n roster={roster}\n />\n <LeadUnqualifyDialog\n open={unqualifying}\n onClose={() => setUnqualifying(false)}\n hostId={hostId}\n leadId={id}\n leadLabel={label ?? id}\n />\n {erase.dialog}\n </>\n )\n}\nLeadDetailPage.displayName = 'LeadDetailPage'\n\nexport default LeadDetailPage\n"],"names":["pluginDocsHelp","normalizeContactEmail","readErasureRequestedAtMs","useFirestore","useFirestoreDoc","useHostCampaigns","useOrgDataScope","Stack","Typography","doc","useState","useCampaignFilingLog","useOrgMemberOptions","crmRoutes","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","useErasePersonAction","LeadCampaignsCard","leadCampaignNames","LeadConvertDialog","LeadHistoryCard","LeadPropertiesCard","LeadUnqualifyDialog","RecordActivityCard","LeadDetailPage","props","id","hostId","org","basePath","firestore","routes","data","lead","status","fromCache","orgId","roster","campaigns","enabled","logFiling","converting","setConverting","unqualifying","setUnqualifying","leadEmail","erase","subject","kind","email","requestedAtMs","label","String","undefined","title","help","anchor","backHref","section","backLabel","variant","color","loading","spacing","leadId","leadStatus","onConvert","onUnqualify","extraMenuItems","menuItems","banner","erasurePending","pendingSinceMs","extraChips","options","map","name","value","recordId","optionsReady","ready","onFiled","added","removed","named","ids","campaignId","find","option","filed","open","onClose","leadLabel","dialog","displayName"],"mappings":"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"}
|
|
@@ -155,6 +155,11 @@ function Fact(props) {
|
|
|
155
155
|
hostId: hostId,
|
|
156
156
|
recordKind: "lead",
|
|
157
157
|
recordId: leadId
|
|
158
|
+
}),
|
|
159
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
160
|
+
variant: "caption",
|
|
161
|
+
color: "text.secondary",
|
|
162
|
+
children: 'Filing is separate — see Filed under campaigns.'
|
|
158
163
|
})
|
|
159
164
|
]
|
|
160
165
|
}) : null
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-history-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { CAPTURED_BY_HOST_FIELD, CONTACT_SOURCE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\n// The component path and NOT the marketing barrel, for the reason the Inbox\n// and the contacts list give: the barrel is the tenant loader's entry point\n// for the plugin's SITE half, and a console card named there ships to every\n// published page.\nimport {\n CrmRecordAttributionZone,\n useHasCrmRecordAttribution,\n} from './crm-attribution-zone'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Chip, Stack, Typography } from '@mui/material'\n\n/**\n * The surfaces `addHostLead` names — `signup`, `booking`, `form:{formId}`,\n * `import` — as they read on screen. A form's id is not a name, so it is\n * shown as the kind with the id beside it rather than as an opaque token.\n * The bare kind `form` is the lifecycle backfill's spelling for a person\n * whose timeline kept no form id (AGL-2631), and reads as the kind, the\n * way the contact's own source chip does.\n */\nexport function leadSourceLabel(source: string): string {\n if (source === 'signup') return 'Sign-up'\n if (source === 'booking') return 'Booking'\n if (source === 'import') return CONTACT_SOURCE_LABELS.import\n if (source === 'form') return CONTACT_SOURCE_LABELS.form\n // A lead entered by hand or over the REST API (AGL-3231).\n if (source === 'manual') return 'Added by hand'\n if (source === 'api') return 'API'\n if (source.startsWith('form:')) return `Form ${source.slice('form:'.length)}`\n return source\n}\n\n/** Every surface that produced a capture — the array, or the older single field. */\nexport function leadSources(lead: Record<string, unknown>): string[] {\n const sources = lead['sources']\n if (Array.isArray(sources) && sources.length) {\n return sources.map((source) => String(source))\n }\n return typeof lead['source'] === 'string' && lead['source'] ? [lead['source']] : []\n}\n\n/** Epoch millis or a Firestore timestamp, as a local date-time, or a dash. */\nexport function leadTimeLabel(value: unknown): string {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return new Date(value).toLocaleString()\n }\n const asDate = (value as { toDate?: () => Date } | null | undefined)?.toDate?.()\n return asDate ? asDate.toLocaleString() : '—'\n}\n\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\nexport interface LeadHistoryCardProps {\n hostId: string\n leadId: string\n lead: Record<string, unknown>\n}\n\n/**\n * What the capture door recorded about this person (AGL-2608): every\n * surface that produced a capture, how many times, and when the first and\n * the latest happened — the fields `addHostLead` keeps on the one document\n * per person — plus the campaign the first capture is credited to.\n *\n * Read-only by construction. Nothing here is the team's to edit: it is what\n * the visitor did, and the CRM's working state lives on the card above it.\n */\nexport function LeadHistoryCard(props: LeadHistoryCardProps) {\n const { hostId, leadId, lead } = props\n // The caption introduces what another plugin draws; with none loaded it\n // would introduce nothing.\n const hasAttribution = useHasCrmRecordAttribution()\n const sources = leadSources(lead)\n const capturedBy = lead[CAPTURED_BY_HOST_FIELD]\n const count = Number(lead['submissionCount'] ?? 0) || (sources.length ? 1 : 0)\n return (\n <CardDisplay\n header={'Captured history'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={3}>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={3}>\n <Fact label=\"First seen\">\n {leadTimeLabel(lead['firstSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Last seen\">\n {leadTimeLabel(lead['lastSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Captures\">{count ? String(count) : '—'}</Fact>\n <Fact label=\"Captured on\">\n {Array.isArray(capturedBy) && capturedBy.length\n ? capturedBy.map(String).join(', ')\n : hostId}\n </Fact>\n </Stack>\n <Fact label=\"Sources\">\n {sources.length ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip key={source} size=\"small\" variant=\"outlined\" label={leadSourceLabel(source)} />\n ))}\n </Stack>\n ) : (\n '—'\n )}\n </Fact>\n {hasAttribution ? (\n <Stack spacing={1}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Where this lead came from'}\n </Typography>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"lead\"\n recordId={leadId}\n />\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadHistoryCard.displayName = 'LeadHistoryCard'\n\nexport default LeadHistoryCard\n"],"names":["CAPTURED_BY_HOST_FIELD","CONTACT_SOURCE_LABELS","pluginDocsHelp","CrmRecordAttributionZone","useHasCrmRecordAttribution","CardDisplay","Chip","Stack","Typography","leadSourceLabel","source","import","form","startsWith","slice","length","leadSources","lead","sources","Array","isArray","map","String","leadTimeLabel","value","Number","isFinite","Date","toLocaleString","asDate","toDate","Fact","props","spacing","variant","color","label","component","children","LeadHistoryCard","hostId","leadId","hasAttribution","capturedBy","count","header","help","anchor","contentGutterX","contentGutterY","direction","xs","md","join","sx","flexWrap","rowGap","size","recordKind","recordId","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-history-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { CAPTURED_BY_HOST_FIELD, CONTACT_SOURCE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\n// The component path and NOT the marketing barrel, for the reason the Inbox\n// and the contacts list give: the barrel is the tenant loader's entry point\n// for the plugin's SITE half, and a console card named there ships to every\n// published page.\nimport {\n CrmRecordAttributionZone,\n useHasCrmRecordAttribution,\n} from './crm-attribution-zone'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Chip, Stack, Typography } from '@mui/material'\n\n/**\n * The surfaces `addHostLead` names — `signup`, `booking`, `form:{formId}`,\n * `import` — as they read on screen. A form's id is not a name, so it is\n * shown as the kind with the id beside it rather than as an opaque token.\n * The bare kind `form` is the lifecycle backfill's spelling for a person\n * whose timeline kept no form id (AGL-2631), and reads as the kind, the\n * way the contact's own source chip does.\n */\nexport function leadSourceLabel(source: string): string {\n if (source === 'signup') return 'Sign-up'\n if (source === 'booking') return 'Booking'\n if (source === 'import') return CONTACT_SOURCE_LABELS.import\n if (source === 'form') return CONTACT_SOURCE_LABELS.form\n // A lead entered by hand or over the REST API (AGL-3231).\n if (source === 'manual') return 'Added by hand'\n if (source === 'api') return 'API'\n if (source.startsWith('form:')) return `Form ${source.slice('form:'.length)}`\n return source\n}\n\n/** Every surface that produced a capture — the array, or the older single field. */\nexport function leadSources(lead: Record<string, unknown>): string[] {\n const sources = lead['sources']\n if (Array.isArray(sources) && sources.length) {\n return sources.map((source) => String(source))\n }\n return typeof lead['source'] === 'string' && lead['source'] ? [lead['source']] : []\n}\n\n/** Epoch millis or a Firestore timestamp, as a local date-time, or a dash. */\nexport function leadTimeLabel(value: unknown): string {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return new Date(value).toLocaleString()\n }\n const asDate = (value as { toDate?: () => Date } | null | undefined)?.toDate?.()\n return asDate ? asDate.toLocaleString() : '—'\n}\n\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\nexport interface LeadHistoryCardProps {\n hostId: string\n leadId: string\n lead: Record<string, unknown>\n}\n\n/**\n * What the capture door recorded about this person (AGL-2608): every\n * surface that produced a capture, how many times, and when the first and\n * the latest happened — the fields `addHostLead` keeps on the one document\n * per person — plus the campaign the first capture is credited to.\n *\n * Read-only by construction. Nothing here is the team's to edit: it is what\n * the visitor did, and the CRM's working state lives on the card above it.\n */\nexport function LeadHistoryCard(props: LeadHistoryCardProps) {\n const { hostId, leadId, lead } = props\n // The caption introduces what another plugin draws; with none loaded it\n // would introduce nothing.\n const hasAttribution = useHasCrmRecordAttribution()\n const sources = leadSources(lead)\n const capturedBy = lead[CAPTURED_BY_HOST_FIELD]\n const count = Number(lead['submissionCount'] ?? 0) || (sources.length ? 1 : 0)\n return (\n <CardDisplay\n header={'Captured history'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={3}>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={3}>\n <Fact label=\"First seen\">\n {leadTimeLabel(lead['firstSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Last seen\">\n {leadTimeLabel(lead['lastSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Captures\">{count ? String(count) : '—'}</Fact>\n <Fact label=\"Captured on\">\n {Array.isArray(capturedBy) && capturedBy.length\n ? capturedBy.map(String).join(', ')\n : hostId}\n </Fact>\n </Stack>\n <Fact label=\"Sources\">\n {sources.length ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip key={source} size=\"small\" variant=\"outlined\" label={leadSourceLabel(source)} />\n ))}\n </Stack>\n ) : (\n '—'\n )}\n </Fact>\n {hasAttribution ? (\n <Stack spacing={1}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Where this lead came from'}\n </Typography>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"lead\"\n recordId={leadId}\n />\n {/* Attribution is which campaign brought the person; the\n filing on the Campaigns card is the team's own (AGL-3274),\n and a reader who sees one campaign here and another there\n is owed the difference. */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Filing is separate — see Filed under campaigns.'}\n </Typography>\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadHistoryCard.displayName = 'LeadHistoryCard'\n\nexport default LeadHistoryCard\n"],"names":["CAPTURED_BY_HOST_FIELD","CONTACT_SOURCE_LABELS","pluginDocsHelp","CrmRecordAttributionZone","useHasCrmRecordAttribution","CardDisplay","Chip","Stack","Typography","leadSourceLabel","source","import","form","startsWith","slice","length","leadSources","lead","sources","Array","isArray","map","String","leadTimeLabel","value","Number","isFinite","Date","toLocaleString","asDate","toDate","Fact","props","spacing","variant","color","label","component","children","LeadHistoryCard","hostId","leadId","hasAttribution","capturedBy","count","header","help","anchor","contentGutterX","contentGutterY","direction","xs","md","join","sx","flexWrap","rowGap","size","recordKind","recordId","displayName"],"mappings":"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"}
|
|
@@ -9,6 +9,12 @@ import type { OrgMemberOptions } from '../hooks/use-org-member-options';
|
|
|
9
9
|
export declare const CONVERT_PENDING_ERASURE_REASON = "An erasure is pending for this person";
|
|
10
10
|
export interface LeadPropertiesCardProps {
|
|
11
11
|
hostId: string;
|
|
12
|
+
/**
|
|
13
|
+
* The org the site belongs to, as the page already resolved it — the
|
|
14
|
+
* custom lead fields are ORG-wide (AGL-3272), and a second lookup here
|
|
15
|
+
* would be one more read per record page for an answer already in hand.
|
|
16
|
+
*/
|
|
17
|
+
orgId: string | null;
|
|
12
18
|
leadId: string;
|
|
13
19
|
lead: Record<string, unknown> & CrmLeadFields;
|
|
14
20
|
leadStatus: FirestoreDocStatus;
|
|
@@ -26,6 +32,12 @@ export interface LeadPropertiesCardProps {
|
|
|
26
32
|
extraMenuItems?: RowActionsMenuItem[];
|
|
27
33
|
/** What the page shows above the facts — the erasure-pending state. */
|
|
28
34
|
banner?: React.ReactNode;
|
|
35
|
+
/**
|
|
36
|
+
* Chips the page adds after the owner — the campaigns the lead is filed
|
|
37
|
+
* under (AGL-3274), named from the containers the page reads once for
|
|
38
|
+
* this header and the Campaigns card below it.
|
|
39
|
+
*/
|
|
40
|
+
extraChips?: React.ReactNode;
|
|
29
41
|
/**
|
|
30
42
|
* An erasure request is waiting on this person (AGL-2623). Convert stays
|
|
31
43
|
* on the page but is refused with the reason, the way the overflow's items
|
|
@@ -23,7 +23,10 @@ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
|
23
23
|
import { useFirestore, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
|
|
24
24
|
import { Alert, Button, FormControl, InputLabel, MenuItem, Select, Stack, TextField, Tooltip, Typography } from "@mui/material";
|
|
25
25
|
import { deleteField, doc, serverTimestamp, updateDoc } from "firebase/firestore";
|
|
26
|
-
import { useEffect, useId, useState } from "react";
|
|
26
|
+
import { useEffect, useId, useMemo, useState } from "react";
|
|
27
|
+
import { useContactFieldDefinitions } from "../hooks/use-contact-field-definitions.js";
|
|
28
|
+
import { crmCustomDraftChanges, crmCustomDraftMissingRequired, crmCustomDraftValue, crmCustomDraftWrites } from "../model/crm-custom-draft.js";
|
|
29
|
+
import { CrmCustomFieldControl } from "./crm-custom-field-control.js";
|
|
27
30
|
import { crmRoutes } from "../model/crm-routes.js";
|
|
28
31
|
import { addressDraftFrom, ContactAddressFields } from "./contact-address-fields.js";
|
|
29
32
|
import { CrmCallButton, CrmPhoneLink } from "./crm-call-actions.js";
|
|
@@ -90,7 +93,7 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
90
93
|
* the actions become links to what the conversion made.
|
|
91
94
|
*/ export function LeadPropertiesCard(props) {
|
|
92
95
|
var _lead_phone, _lead_notes, _lead_email, _lead_email1, _lead_name;
|
|
93
|
-
const { hostId, leadId, lead, leadStatus, fromCache, basePath, roster, onConvert, onUnqualify, extraMenuItems = [], banner, erasurePending = false, org } = props;
|
|
96
|
+
const { hostId, orgId, leadId, lead, leadStatus, fromCache, basePath, roster, onConvert, onUnqualify, extraMenuItems = [], banner, extraChips, erasurePending = false, org } = props;
|
|
94
97
|
const firestore = useFirestore();
|
|
95
98
|
const { enqueueSnackbar } = useSnackbar();
|
|
96
99
|
const routes = crmRoutes(basePath);
|
|
@@ -131,6 +134,23 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
131
134
|
lead.tags,
|
|
132
135
|
lead.address
|
|
133
136
|
]);
|
|
137
|
+
/*
|
|
138
|
+
* THE ORG'S OWN LEAD FIELDS (AGL-3272), edited under the same Save as
|
|
139
|
+
* the profile: one button over one card, so a person filling a lead in
|
|
140
|
+
* does not have to find two.
|
|
141
|
+
*
|
|
142
|
+
* The draft holds only the keys the reader touched, and Save writes the
|
|
143
|
+
* difference as dotted paths — a `custom` map written whole would take
|
|
144
|
+
* out every key this card did not show, which is what a retired field's
|
|
145
|
+
* values and an integration's writes sit under.
|
|
146
|
+
*/ const fields = useContactFieldDefinitions(orgId, 'lead');
|
|
147
|
+
const storedCustom = useMemo(()=>{
|
|
148
|
+
var _lead_custom;
|
|
149
|
+
return (_lead_custom = lead.custom) != null ? _lead_custom : {};
|
|
150
|
+
}, [
|
|
151
|
+
lead.custom
|
|
152
|
+
]);
|
|
153
|
+
const [custom, setCustom] = useState({});
|
|
134
154
|
const editProfile = (key, value)=>{
|
|
135
155
|
setProfile((current)=>_extends({}, current, {
|
|
136
156
|
[key]: value
|
|
@@ -180,6 +200,12 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
180
200
|
}
|
|
181
201
|
setNotesDirty(false);
|
|
182
202
|
};
|
|
203
|
+
/*
|
|
204
|
+
* What Save is offered for: a profile field edited, or a custom value
|
|
205
|
+
* that differs from the stored map. Touched-and-put-back does not count
|
|
206
|
+
* — the draft keeps the key either way, and a Save that wrote nothing
|
|
207
|
+
* would still bump `updatedAt`.
|
|
208
|
+
*/ const dirty = profileDirty || crmCustomDraftChanges(storedCustom, custom).length > 0;
|
|
183
209
|
const saveProfile = async ()=>{
|
|
184
210
|
const { patch, errors } = Aglyn.normalizeCrmLeadProfile({
|
|
185
211
|
company: profile.company,
|
|
@@ -192,12 +218,23 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
192
218
|
});
|
|
193
219
|
setProfileErrors(errors);
|
|
194
220
|
if (Object.keys(errors).length) return;
|
|
221
|
+
/*
|
|
222
|
+
* A required field the reader CLEARED is refused; one the lead has
|
|
223
|
+
* always lacked is not this save's to demand, or a field added after
|
|
224
|
+
* the lead was captured would block every later edit to its company.
|
|
225
|
+
*/ const missing = crmCustomDraftMissingRequired(fields.active, storedCustom, custom, 'edit');
|
|
226
|
+
if (missing.length) {
|
|
227
|
+
enqueueSnackbar(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`, {
|
|
228
|
+
variant: 'warning'
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
195
232
|
setSavingProfile(true);
|
|
196
233
|
const verdict = await writeGuardedBySeed({
|
|
197
234
|
subject: 'lead',
|
|
198
235
|
fromCache,
|
|
199
236
|
unreadable: leadStatus === 'error'
|
|
200
|
-
}, ()=>write(profileWrite(patch), 'Lead saved'));
|
|
237
|
+
}, ()=>write(_extends({}, profileWrite(patch), crmCustomDraftWrites(storedCustom, custom)), 'Lead saved'));
|
|
201
238
|
setSavingProfile(false);
|
|
202
239
|
if (!verdict.ok) {
|
|
203
240
|
var _verdict_message;
|
|
@@ -207,6 +244,9 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
207
244
|
return;
|
|
208
245
|
}
|
|
209
246
|
setProfileDirty(false);
|
|
247
|
+
// The draft is spent: what it held is stored, and the controls read
|
|
248
|
+
// the document again.
|
|
249
|
+
setCustom({});
|
|
210
250
|
};
|
|
211
251
|
const consent = Aglyn.readMarketingBasis(lead, Aglyn.soloConsentGroup(hostId));
|
|
212
252
|
const consentLine = consent.basis === 'granted' ? `Opted in to marketing${consent.basisAtMs ? ` on ${new Date(consent.basisAtMs).toLocaleDateString()}` : ''}` : consent.basis === 'declined' ? 'Declined marketing' : 'No marketing consent recorded — this lead cannot be emailed marketing';
|
|
@@ -291,7 +331,8 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
291
331
|
/*#__PURE__*/ _jsx(CrmRecordChip, {
|
|
292
332
|
label: "Owner",
|
|
293
333
|
value: lead.ownerUid ? roster.labelFor(lead.ownerUid) : undefined
|
|
294
|
-
})
|
|
334
|
+
}),
|
|
335
|
+
extraChips
|
|
295
336
|
]
|
|
296
337
|
}),
|
|
297
338
|
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
@@ -548,6 +589,23 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
548
589
|
onChange: (next)=>editProfile('address', next),
|
|
549
590
|
disabled: converted
|
|
550
591
|
}),
|
|
592
|
+
fields.active.length ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
593
|
+
children: [
|
|
594
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
595
|
+
variant: "caption",
|
|
596
|
+
color: "text.secondary",
|
|
597
|
+
children: 'Custom fields'
|
|
598
|
+
}),
|
|
599
|
+
fields.active.map((definition)=>/*#__PURE__*/ _jsx(CrmCustomFieldControl, {
|
|
600
|
+
definition: definition,
|
|
601
|
+
value: crmCustomDraftValue(storedCustom, custom, definition.key),
|
|
602
|
+
onChange: (value)=>setCustom((current)=>_extends({}, current, {
|
|
603
|
+
[definition.key]: value
|
|
604
|
+
})),
|
|
605
|
+
disabled: converted || savingProfile
|
|
606
|
+
}, definition.$id))
|
|
607
|
+
]
|
|
608
|
+
}) : null,
|
|
551
609
|
converted ? null : /*#__PURE__*/ _jsx(Stack, {
|
|
552
610
|
direction: "row",
|
|
553
611
|
spacing: 1,
|
|
@@ -558,7 +616,7 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
558
616
|
size: "small",
|
|
559
617
|
variant: "contained",
|
|
560
618
|
onClick: ()=>void saveProfile(),
|
|
561
|
-
disabled: !
|
|
619
|
+
disabled: !dirty || savingProfile,
|
|
562
620
|
children: 'Save'
|
|
563
621
|
})
|
|
564
622
|
})
|