@aglyn/plugins-crm 1.0.0-beta.168 → 1.0.0-beta.170
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/crm-console-page.js +5 -4
- package/src/lib/components/crm-console-page.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.d.ts +7 -1
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-convert-dialog.d.ts +8 -1
- package/src/lib/components/lead-convert-dialog.js +4 -2
- package/src/lib/components/lead-convert-dialog.js.map +1 -1
- package/src/lib/components/lead-detail-page.d.ts +18 -4
- package/src/lib/components/lead-detail-page.js +69 -29
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-history-card.d.ts +8 -1
- package/src/lib/components/lead-history-card.js +10 -2
- package/src/lib/components/lead-history-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.d.ts +16 -2
- package/src/lib/components/lead-properties-card.js +2 -2
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.d.ts +6 -1
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-section.js +3 -3
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/server/erase-person.js +3 -11
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/record-email-state.js +4 -14
- package/src/lib/server/record-email-state.js.map +1 -1
|
@@ -14,13 +14,15 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
18
|
-
import { pluginDocsHelp, normalizeContactEmail, readErasureRequestedAtMs } from "@aglyn/aglyn";
|
|
19
|
-
import { useFirestore, useFirestoreDoc, useHostCampaigns
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { pluginDocsHelp, leadPrimaryGroup, normalizeContactEmail, readErasureRequestedAtMs } from "@aglyn/aglyn";
|
|
19
|
+
import { useFirestore, useFirestoreDoc, useHostCampaigns } from "@aglyn/tenant-feature-instance";
|
|
20
20
|
import { Stack, Typography } from "@mui/material";
|
|
21
21
|
import { doc } from "firebase/firestore";
|
|
22
|
-
import { useState } from "react";
|
|
22
|
+
import { useMemo, useState } from "react";
|
|
23
23
|
import { useCampaignFilingLog } from "../hooks/use-campaign-filing-log.js";
|
|
24
|
+
import { CrmCreateSiteDefault } from "../hooks/use-crm-org-mount.js";
|
|
25
|
+
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
24
26
|
import { useOrgMemberOptions } from "../hooks/use-org-member-options.js";
|
|
25
27
|
import { crmRoutes } from "../model/crm-routes.js";
|
|
26
28
|
import { CrmRecordChip, CrmRecordHeader } from "./crm-record-header.js";
|
|
@@ -37,42 +39,70 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
37
39
|
*
|
|
38
40
|
* The record behind a row of the Leads section: what the person did on the
|
|
39
41
|
* site, who is working them, and the conversion that turns them into a
|
|
40
|
-
* contact, a company and a deal. One document listen
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* contact, a company and a deal. One document listen and one roster request
|
|
43
|
+
* for the owner picker; the convert dialog's reads open only when it does.
|
|
44
|
+
*
|
|
45
|
+
* ## The two levels, and the site a lead is acted on as (AGL-3278)
|
|
46
|
+
*
|
|
47
|
+
* The org root comes from `useCrmScope`, as it does on every other CRM
|
|
48
|
+
* record page: under a site it is looked up from that site, and at the
|
|
49
|
+
* organization level it is the mount's, known at once. It used to come
|
|
50
|
+
* from `useOrgDataScope({ hostId })` — correct only under a site, because
|
|
51
|
+
* an absent host settles at `orgId: null`, and a null org builds no
|
|
52
|
+
* reference, opens no listener and leaves the page loading forever.
|
|
53
|
+
*
|
|
54
|
+
* Several surfaces still need ONE site: the campaigns to offer, the feed
|
|
55
|
+
* an activity is filed in, the booking door, the consent basis, the
|
|
56
|
+
* conversion. Under a site that is the mounted one. At the organization
|
|
57
|
+
* level it is the lead's own first capturing site — `leadPrimaryGroup` —
|
|
58
|
+
* the same answer `contactPrimaryGroup` gives the contact page, and the
|
|
59
|
+
* same one the Leads list already converts a row as.
|
|
44
60
|
*/ export function LeadDetailPage(props) {
|
|
45
61
|
const { id, hostId, org, basePath } = props;
|
|
46
62
|
const firestore = useFirestore();
|
|
47
63
|
const routes = crmRoutes(basePath);
|
|
48
64
|
/*
|
|
49
65
|
* Resolved BEFORE the read that needs it (AGL-3275). A lead is an org row
|
|
50
|
-
* now, so the org has to be known to address one — and `
|
|
66
|
+
* now, so the org has to be known to address one — and `scope` is null
|
|
51
67
|
* while the host-index lookup settles. Handing that null to `doc()` is a
|
|
52
68
|
* thrown `TypeError`, which the page renders as a 500 rather than as a
|
|
53
69
|
* record still loading.
|
|
54
|
-
*/ const { orgId } =
|
|
55
|
-
hostId
|
|
70
|
+
*/ const { scope, orgId, ready, consentGroup: viewingGroup } = useCrmScope({
|
|
71
|
+
hostId,
|
|
72
|
+
org
|
|
56
73
|
});
|
|
57
|
-
const { data: lead, status, fromCache } = useFirestoreDoc(()=>
|
|
74
|
+
const { data: lead, status, fromCache } = useFirestoreDoc(()=>scope ? doc(firestore, scope[0], scope[1], 'leads', id) : null, [
|
|
58
75
|
firestore,
|
|
59
|
-
|
|
76
|
+
scope,
|
|
60
77
|
id
|
|
61
78
|
]);
|
|
79
|
+
/*
|
|
80
|
+
* The group this lead is READ through: the mounted site's under a site,
|
|
81
|
+
* and the lead's own first capturing site at the organization level
|
|
82
|
+
* (AGL-3278). The consent basis is read against it — a refusal recorded
|
|
83
|
+
* for any sibling in a declared group stands against every one of them,
|
|
84
|
+
* so a group of one would report a person as opted in who had
|
|
85
|
+
* unsubscribed from the brand beside this one.
|
|
86
|
+
*/ const leadGroup = useMemo(()=>viewingGroup != null ? viewingGroup : leadPrimaryGroup(lead, org), [
|
|
87
|
+
viewingGroup,
|
|
88
|
+
lead,
|
|
89
|
+
org
|
|
90
|
+
]);
|
|
91
|
+
/** The one site this page acts as, or null for a lead no site captured. */ const siteHostId = hostId != null ? hostId : leadGroup.hostId || null;
|
|
62
92
|
const roster = useOrgMemberOptions(orgId);
|
|
63
93
|
/*
|
|
64
94
|
* The site's campaigns, read once for the page (AGL-3274): the header
|
|
65
95
|
* names the ones the lead is filed under and the Campaigns card offers
|
|
66
|
-
* them. One listener, not one per surface —
|
|
67
|
-
*
|
|
68
|
-
*/ const campaigns = useHostCampaigns(
|
|
96
|
+
* them. One listener, not one per surface — the containers are the
|
|
97
|
+
* capturing site's.
|
|
98
|
+
*/ const campaigns = useHostCampaigns(siteHostId != null ? siteHostId : undefined, {
|
|
69
99
|
enabled: true
|
|
70
100
|
});
|
|
71
101
|
// A saved filing is written on the lead's Activity too (AGL-3274), one
|
|
72
102
|
// entry per campaign added or removed, by the member who saved it.
|
|
73
103
|
const logFiling = useCampaignFilingLog({
|
|
74
104
|
orgId,
|
|
75
|
-
hostId,
|
|
105
|
+
hostId: siteHostId,
|
|
76
106
|
org: org
|
|
77
107
|
});
|
|
78
108
|
const [converting, setConverting] = useState(false);
|
|
@@ -81,7 +111,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
81
111
|
// contact: the same request, filed by the lead's address.
|
|
82
112
|
const leadEmail = lead ? normalizeContactEmail(lead['email']) : null;
|
|
83
113
|
const erase = useErasePersonAction({
|
|
84
|
-
hostId,
|
|
114
|
+
hostId: siteHostId,
|
|
85
115
|
orgId,
|
|
86
116
|
subject: lead && leadEmail ? {
|
|
87
117
|
kind: 'lead',
|
|
@@ -91,7 +121,13 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
91
121
|
requestedAtMs: readErasureRequestedAtMs(lead)
|
|
92
122
|
});
|
|
93
123
|
const label = lead ? String(lead['name'] || lead['email'] || id) : undefined;
|
|
94
|
-
|
|
124
|
+
/*
|
|
125
|
+
* A SETTLED lookup with no org is an answer, not a wait (AGL-3278). The
|
|
126
|
+
* spinner below is for a read in flight; standing on it once `ready` has
|
|
127
|
+
* said there is no org to read from is the endless "Loading…" this page
|
|
128
|
+
* shipped with.
|
|
129
|
+
*/ const noOrg = ready && !scope;
|
|
130
|
+
if (noOrg || status === 'error' || status === 'success' && !lead) {
|
|
95
131
|
return /*#__PURE__*/ _jsx(CrmRecordHeader, {
|
|
96
132
|
kind: "Lead",
|
|
97
133
|
title: undefined,
|
|
@@ -103,7 +139,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
103
139
|
children: /*#__PURE__*/ _jsx(Typography, {
|
|
104
140
|
variant: "body2",
|
|
105
141
|
color: "text.secondary",
|
|
106
|
-
children: status === 'error' ? 'This lead could not be read.' : 'This lead no longer exists — it may have been removed from the Inbox.'
|
|
142
|
+
children: noOrg || status === 'error' ? 'This lead could not be read.' : 'This lead no longer exists — it may have been removed from the Inbox.'
|
|
107
143
|
})
|
|
108
144
|
});
|
|
109
145
|
}
|
|
@@ -119,13 +155,17 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
119
155
|
loading: true
|
|
120
156
|
});
|
|
121
157
|
}
|
|
122
|
-
return
|
|
158
|
+
return(/* A task or an activity created from this page defaults to the lead's
|
|
159
|
+
own site, not to the site the reader last picked in a list's drawer
|
|
160
|
+
(AGL-2630) — as on every other record page. */ /*#__PURE__*/ _jsxs(CrmCreateSiteDefault, {
|
|
161
|
+
hostId: siteHostId,
|
|
123
162
|
children: [
|
|
124
163
|
/*#__PURE__*/ _jsxs(Stack, {
|
|
125
164
|
spacing: 3,
|
|
126
165
|
children: [
|
|
127
166
|
/*#__PURE__*/ _jsx(LeadPropertiesCard, {
|
|
128
|
-
hostId:
|
|
167
|
+
hostId: siteHostId,
|
|
168
|
+
consentGroup: leadGroup,
|
|
129
169
|
orgId: orgId,
|
|
130
170
|
leadId: id,
|
|
131
171
|
lead: lead,
|
|
@@ -148,14 +188,14 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
148
188
|
}, name))
|
|
149
189
|
}),
|
|
150
190
|
/*#__PURE__*/ _jsx(CrmRecordInsightsZone, {
|
|
151
|
-
hostId:
|
|
191
|
+
hostId: siteHostId,
|
|
152
192
|
org: org,
|
|
153
193
|
kind: "lead",
|
|
154
194
|
recordId: id,
|
|
155
195
|
name: label != null ? label : ''
|
|
156
196
|
}),
|
|
157
197
|
/*#__PURE__*/ _jsx(LeadCampaignsCard, {
|
|
158
|
-
hostId:
|
|
198
|
+
hostId: siteHostId,
|
|
159
199
|
leadId: id,
|
|
160
200
|
lead: lead,
|
|
161
201
|
leadStatus: status,
|
|
@@ -180,12 +220,12 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
180
220
|
}
|
|
181
221
|
}),
|
|
182
222
|
/*#__PURE__*/ _jsx(LeadHistoryCard, {
|
|
183
|
-
hostId:
|
|
223
|
+
hostId: siteHostId,
|
|
184
224
|
leadId: id,
|
|
185
225
|
lead: lead
|
|
186
226
|
}),
|
|
187
227
|
/*#__PURE__*/ _jsx(RecordActivityCard, {
|
|
188
|
-
hostId:
|
|
228
|
+
hostId: siteHostId,
|
|
189
229
|
org: org,
|
|
190
230
|
leadId: id
|
|
191
231
|
})
|
|
@@ -194,7 +234,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
194
234
|
/*#__PURE__*/ _jsx(LeadConvertDialog, {
|
|
195
235
|
open: converting,
|
|
196
236
|
onClose: ()=>setConverting(false),
|
|
197
|
-
hostId:
|
|
237
|
+
hostId: siteHostId,
|
|
198
238
|
orgId: orgId,
|
|
199
239
|
org: org,
|
|
200
240
|
leadId: id,
|
|
@@ -205,13 +245,13 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
205
245
|
/*#__PURE__*/ _jsx(LeadUnqualifyDialog, {
|
|
206
246
|
open: unqualifying,
|
|
207
247
|
onClose: ()=>setUnqualifying(false),
|
|
208
|
-
hostId:
|
|
248
|
+
hostId: siteHostId,
|
|
209
249
|
leadId: id,
|
|
210
250
|
leadLabel: label != null ? label : id
|
|
211
251
|
}),
|
|
212
252
|
erase.dialog
|
|
213
253
|
]
|
|
214
|
-
});
|
|
254
|
+
}));
|
|
215
255
|
}
|
|
216
256
|
LeadDetailPage.displayName = 'LeadDetailPage';
|
|
217
257
|
export default LeadDetailPage;
|
|
@@ -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 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 /*\n * Resolved BEFORE the read that needs it (AGL-3275). A lead is an org row\n * now, so the org has to be known to address one — and `orgId` is null\n * while the host-index lookup settles. Handing that null to `doc()` is a\n * thrown `TypeError`, which the page renders as a 500 rather than as a\n * record still loading.\n */\n const { orgId } = useOrgDataScope({ hostId })\n const {\n data: lead,\n status,\n fromCache,\n } = useFirestoreDoc<LeadDocument>(\n () => (orgId ? doc(firestore, 'orgs', orgId, 'leads', id) : null),\n [firestore, orgId, id],\n )\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","orgId","data","lead","status","fromCache","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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"}
|
|
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 leadPrimaryGroup,\n normalizeContactEmail,\n readErasureRequestedAtMs,\n} from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreDoc,\n useHostCampaigns,\n} from '@aglyn/tenant-feature-instance'\nimport { Stack, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useMemo, useState } from 'react'\nimport { useCampaignFilingLog } from '../hooks/use-campaign-filing-log'\nimport { CrmCreateSiteDefault } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\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 and one roster request\n * for the owner picker; the convert dialog's reads open only when it does.\n *\n * ## The two levels, and the site a lead is acted on as (AGL-3278)\n *\n * The org root comes from `useCrmScope`, as it does on every other CRM\n * record page: under a site it is looked up from that site, and at the\n * organization level it is the mount's, known at once. It used to come\n * from `useOrgDataScope({ hostId })` — correct only under a site, because\n * an absent host settles at `orgId: null`, and a null org builds no\n * reference, opens no listener and leaves the page loading forever.\n *\n * Several surfaces still need ONE site: the campaigns to offer, the feed\n * an activity is filed in, the booking door, the consent basis, the\n * conversion. Under a site that is the mounted one. At the organization\n * level it is the lead's own first capturing site — `leadPrimaryGroup` —\n * the same answer `contactPrimaryGroup` gives the contact page, and the\n * same one the Leads list already converts a row as.\n */\nexport function LeadDetailPage(props: CrmDetailPageProps) {\n const { id, hostId, org, basePath } = props\n const firestore = useFirestore()\n const routes = crmRoutes(basePath)\n /*\n * Resolved BEFORE the read that needs it (AGL-3275). A lead is an org row\n * now, so the org has to be known to address one — and `scope` is null\n * while the host-index lookup settles. Handing that null to `doc()` is a\n * thrown `TypeError`, which the page renders as a 500 rather than as a\n * record still loading.\n */\n const { scope, orgId, ready, consentGroup: viewingGroup } = useCrmScope({\n hostId,\n org,\n })\n const {\n data: lead,\n status,\n fromCache,\n } = useFirestoreDoc<LeadDocument>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'leads', id) : null),\n [firestore, scope, id],\n )\n /*\n * The group this lead is READ through: the mounted site's under a site,\n * and the lead's own first capturing site at the organization level\n * (AGL-3278). The consent basis is read against it — a refusal recorded\n * for any sibling in a declared group stands against every one of them,\n * so a group of one would report a person as opted in who had\n * unsubscribed from the brand beside this one.\n */\n const leadGroup = useMemo(\n () => viewingGroup ?? leadPrimaryGroup(lead, org as Record<string, unknown>),\n [viewingGroup, lead, org],\n )\n /** The one site this page acts as, or null for a lead no site captured. */\n const siteHostId = hostId ?? (leadGroup.hostId || null)\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 — the containers are the\n * capturing site's.\n */\n const campaigns = useHostCampaigns(siteHostId ?? undefined, { 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: siteHostId,\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: siteHostId,\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 /*\n * A SETTLED lookup with no org is an answer, not a wait (AGL-3278). The\n * spinner below is for a read in flight; standing on it once `ready` has\n * said there is no org to read from is the endless \"Loading…\" this page\n * shipped with.\n */\n const noOrg = ready && !scope\n if (noOrg || 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 {noOrg || 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 /* A task or an activity created from this page defaults to the lead's\n own site, not to the site the reader last picked in a list's drawer\n (AGL-2630) — as on every other record page. */\n <CrmCreateSiteDefault hostId={siteHostId}>\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={siteHostId}\n consentGroup={leadGroup}\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={siteHostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"lead\"\n recordId={id}\n name={label ?? ''}\n />\n <LeadCampaignsCard\n hostId={siteHostId}\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={siteHostId} leadId={id} lead={lead} />\n <RecordActivityCard hostId={siteHostId} org={org} leadId={id} />\n </Stack>\n <LeadConvertDialog\n open={converting}\n onClose={() => setConverting(false)}\n hostId={siteHostId}\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={siteHostId}\n leadId={id}\n leadLabel={label ?? id}\n />\n {erase.dialog}\n </CrmCreateSiteDefault>\n )\n}\nLeadDetailPage.displayName = 'LeadDetailPage'\n\nexport default LeadDetailPage\n"],"names":["pluginDocsHelp","leadPrimaryGroup","normalizeContactEmail","readErasureRequestedAtMs","useFirestore","useFirestoreDoc","useHostCampaigns","Stack","Typography","doc","useMemo","useState","useCampaignFilingLog","CrmCreateSiteDefault","useCrmScope","useOrgMemberOptions","crmRoutes","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","useErasePersonAction","LeadCampaignsCard","leadCampaignNames","LeadConvertDialog","LeadHistoryCard","LeadPropertiesCard","LeadUnqualifyDialog","RecordActivityCard","LeadDetailPage","props","id","hostId","org","basePath","firestore","routes","scope","orgId","ready","consentGroup","viewingGroup","data","lead","status","fromCache","leadGroup","siteHostId","roster","campaigns","undefined","enabled","logFiling","converting","setConverting","unqualifying","setUnqualifying","leadEmail","erase","subject","kind","email","requestedAtMs","label","String","noOrg","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","onFiled","added","removed","named","ids","campaignId","find","option","filed","open","onClose","leadLabel","dialog","displayName"],"mappings":"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"}
|
|
@@ -12,7 +12,14 @@ export declare function leadSources(lead: Record<string, unknown>): string[];
|
|
|
12
12
|
/** Epoch millis or a Firestore timestamp, as a local date-time, or a dash. */
|
|
13
13
|
export declare function leadTimeLabel(value: unknown): string;
|
|
14
14
|
export interface LeadHistoryCardProps {
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The lead's own site, or `null` at the organization level for a lead no
|
|
17
|
+
* site captured (AGL-3278). Named on "Captured on" when the document
|
|
18
|
+
* carries no capture list of its own, and the one site the attribution
|
|
19
|
+
* zone is asked about — with none the zone is not drawn, as on the
|
|
20
|
+
* contact's Associations card.
|
|
21
|
+
*/
|
|
22
|
+
hostId: string | null;
|
|
16
23
|
leadId: string;
|
|
17
24
|
lead: Record<string, unknown>;
|
|
18
25
|
}
|
|
@@ -23,6 +23,7 @@ import { CAPTURED_BY_HOST_FIELD, CONTACT_SOURCE_LABELS, pluginDocsHelp } from "@
|
|
|
23
23
|
import { CrmRecordAttributionZone, useHasCrmRecordAttribution } from "./crm-attribution-zone.js";
|
|
24
24
|
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
25
25
|
import { Chip, Stack, Typography } from "@mui/material";
|
|
26
|
+
import { useCrmOrgMount } from "../hooks/use-crm-org-mount.js";
|
|
26
27
|
/**
|
|
27
28
|
* The surfaces `addHostLead` names — `signup`, `booking`, `form:{formId}`,
|
|
28
29
|
* `import` — as they read on screen. A form's id is not a name, so it is
|
|
@@ -89,6 +90,10 @@ function Fact(props) {
|
|
|
89
90
|
// The caption introduces what another plugin draws; with none loaded it
|
|
90
91
|
// would introduce nothing.
|
|
91
92
|
const hasAttribution = useHasCrmRecordAttribution();
|
|
93
|
+
// Sites read by NAME at the organization level, as they do in the Leads
|
|
94
|
+
// list's "Known by" column; under a site there is no mount and an id is
|
|
95
|
+
// all there is to print.
|
|
96
|
+
const mount = useCrmOrgMount();
|
|
92
97
|
const sources = leadSources(lead);
|
|
93
98
|
const capturedBy = lead[CAPTURED_BY_HOST_FIELD];
|
|
94
99
|
const count = Number((_lead_submissionCount = lead['submissionCount']) != null ? _lead_submissionCount : 0) || (sources.length ? 1 : 0);
|
|
@@ -123,7 +128,10 @@ function Fact(props) {
|
|
|
123
128
|
}),
|
|
124
129
|
/*#__PURE__*/ _jsx(Fact, {
|
|
125
130
|
label: "Captured on",
|
|
126
|
-
children: Array.isArray(capturedBy) && capturedBy.length ? capturedBy.map(
|
|
131
|
+
children: Array.isArray(capturedBy) && capturedBy.length ? capturedBy.map((id)=>{
|
|
132
|
+
var _ref;
|
|
133
|
+
return (_ref = mount == null ? void 0 : mount.siteName(String(id))) != null ? _ref : String(id);
|
|
134
|
+
}).join(', ') : hostId != null ? hostId : '—'
|
|
127
135
|
})
|
|
128
136
|
]
|
|
129
137
|
}),
|
|
@@ -143,7 +151,7 @@ function Fact(props) {
|
|
|
143
151
|
}, source))
|
|
144
152
|
}) : '—'
|
|
145
153
|
}),
|
|
146
|
-
hasAttribution ? /*#__PURE__*/ _jsxs(Stack, {
|
|
154
|
+
hasAttribution && hostId ? /*#__PURE__*/ _jsxs(Stack, {
|
|
147
155
|
spacing: 1,
|
|
148
156
|
children: [
|
|
149
157
|
/*#__PURE__*/ _jsx(Typography, {
|
|
@@ -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 {/* 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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"}
|
|
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'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\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 /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Named on \"Captured on\" when the document\n * carries no capture list of its own, and the one site the attribution\n * zone is asked about — with none the zone is not drawn, as on the\n * contact's Associations card.\n */\n hostId: string | null\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 // Sites read by NAME at the organization level, as they do in the Leads\n // list's \"Known by\" column; under a site there is no mount and an id is\n // all there is to print.\n const mount = useCrmOrgMount()\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\n .map((id) => mount?.siteName(String(id)) ?? String(id))\n .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 && hostId ? (\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","useCrmOrgMount","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","mount","capturedBy","count","header","help","anchor","contentGutterX","contentGutterY","direction","xs","md","id","siteName","join","sx","flexWrap","rowGap","size","recordKind","recordId","displayName"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AglynOrgBilling, CrmLeadFields } from '@aglyn/aglyn';
|
|
1
|
+
import type { AglynOrgBilling, ConsentGroup, CrmLeadFields } from '@aglyn/aglyn';
|
|
2
2
|
import type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component';
|
|
3
3
|
import { type FirestoreDocStatus } from '@aglyn/tenant-feature-instance';
|
|
4
4
|
import type { OrgMemberOptions } from '../hooks/use-org-member-options';
|
|
@@ -8,7 +8,21 @@ import type { OrgMemberOptions } from '../hooks/use-org-member-options';
|
|
|
8
8
|
*/
|
|
9
9
|
export declare const CONVERT_PENDING_ERASURE_REASON = "An erasure is pending for this person";
|
|
10
10
|
export interface LeadPropertiesCardProps {
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The site this lead is acted on as — the mounted one, or at the
|
|
13
|
+
* organization level the lead's own first capturing site (AGL-3278).
|
|
14
|
+
* `null` for a lead no site has captured: the booking door, the call and
|
|
15
|
+
* the email close, because each of them is one site's to offer.
|
|
16
|
+
*/
|
|
17
|
+
hostId: string | null;
|
|
18
|
+
/**
|
|
19
|
+
* The group the consent basis is read against (AGL-3278): the site's own
|
|
20
|
+
* declared group, resolved by the page. Not `soloConsentGroup(hostId)`,
|
|
21
|
+
* which is what this card used to spell and which cannot see a refusal
|
|
22
|
+
* recorded against a sibling brand — three sites presenting as one sender
|
|
23
|
+
* are one sender to the person unsubscribing from them.
|
|
24
|
+
*/
|
|
25
|
+
consentGroup: ConsentGroup;
|
|
12
26
|
/**
|
|
13
27
|
* The org the site belongs to, as the page already resolved it — the
|
|
14
28
|
* custom lead fields are ORG-wide (AGL-3272), and a second lookup here
|
|
@@ -93,7 +93,7 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
93
93
|
* the actions become links to what the conversion made.
|
|
94
94
|
*/ export function LeadPropertiesCard(props) {
|
|
95
95
|
var _lead_phone, _lead_notes, _lead_email, _lead_email1, _lead_name;
|
|
96
|
-
const { hostId, orgId, leadId, lead, leadStatus, fromCache, basePath, roster, onConvert, onUnqualify, extraMenuItems = [], banner, extraChips, erasurePending = false, org } = props;
|
|
96
|
+
const { hostId, consentGroup, orgId, leadId, lead, leadStatus, fromCache, basePath, roster, onConvert, onUnqualify, extraMenuItems = [], banner, extraChips, erasurePending = false, org } = props;
|
|
97
97
|
const firestore = useFirestore();
|
|
98
98
|
const { enqueueSnackbar } = useSnackbar();
|
|
99
99
|
const routes = crmRoutes(basePath);
|
|
@@ -261,7 +261,7 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
261
261
|
// the document again.
|
|
262
262
|
setCustom({});
|
|
263
263
|
};
|
|
264
|
-
const consent = Aglyn.readMarketingBasis(lead,
|
|
264
|
+
const consent = Aglyn.readMarketingBasis(lead, consentGroup);
|
|
265
265
|
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';
|
|
266
266
|
return /*#__PURE__*/ _jsx(CrmRecordHeader, {
|
|
267
267
|
kind: "Lead",
|