@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.
@@ -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, Fragment as _Fragment } from "react/jsx-runtime";
18
- import { pluginDocsHelp, normalizeContactEmail, readErasureRequestedAtMs } from "@aglyn/aglyn";
19
- import { useFirestore, useFirestoreDoc, useHostCampaigns, useOrgDataScope } from "@aglyn/tenant-feature-instance";
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 — `hosts/{hostId}/leads`
41
- * is host-scoped by path, so there is no `visibleTo` to filter — and one
42
- * roster request for the owner picker; the convert dialog's reads open only
43
- * when it does.
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 `orgId` is null
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 } = useOrgDataScope({
55
- hostId
70
+ */ const { scope, orgId, ready, consentGroup: viewingGroup } = useCrmScope({
71
+ hostId,
72
+ org
56
73
  });
57
- const { data: lead, status, fromCache } = useFirestoreDoc(()=>orgId ? doc(firestore, 'orgs', orgId, 'leads', id) : null, [
74
+ const { data: lead, status, fromCache } = useFirestoreDoc(()=>scope ? doc(firestore, scope[0], scope[1], 'leads', id) : null, [
58
75
  firestore,
59
- orgId,
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 — a lead belongs to one site,
67
- * so the containers are that site's.
68
- */ const campaigns = useHostCampaigns(hostId, {
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
- if (status === 'error' || status === 'success' && !lead) {
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 /*#__PURE__*/ _jsxs(_Fragment, {
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: 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: 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: 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: hostId,
223
+ hostId: siteHostId,
184
224
  leadId: id,
185
225
  lead: lead
186
226
  }),
187
227
  /*#__PURE__*/ _jsx(RecordActivityCard, {
188
- hostId: 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: 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: 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
- hostId: string;
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(String).join(', ') : hostId
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":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,sBAAsB,EAAEC,qBAAqB,EAAEC,cAAc,QAAQ,eAAc;AAC5F,4EAA4E;AAC5E,4EAA4E;AAC5E,4EAA4E;AAC5E,kBAAkB;AAClB,SACEC,wBAAwB,EACxBC,0BAA0B,QACrB,4BAAwB;AAC/B,SAASC,WAAW,QAAQ,uBAAsB;AAClD,SAASC,IAAI,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAEvD;;;;;;;CAOC,GACD,OAAO,SAASC,gBAAgBC,MAAc;IAC5C,IAAIA,WAAW,UAAU,OAAO;IAChC,IAAIA,WAAW,WAAW,OAAO;IACjC,IAAIA,WAAW,UAAU,OAAOT,sBAAsBU,MAAM;IAC5D,IAAID,WAAW,QAAQ,OAAOT,sBAAsBW,IAAI;IACxD,0DAA0D;IAC1D,IAAIF,WAAW,UAAU,OAAO;IAChC,IAAIA,WAAW,OAAO,OAAO;IAC7B,IAAIA,OAAOG,UAAU,CAAC,UAAU,OAAO,CAAC,KAAK,EAAEH,OAAOI,KAAK,CAAC,QAAQC,MAAM,GAAG;IAC7E,OAAOL;AACT;AAEA,kFAAkF,GAClF,OAAO,SAASM,YAAYC,IAA6B;IACvD,MAAMC,UAAUD,IAAI,CAAC,UAAU;IAC/B,IAAIE,MAAMC,OAAO,CAACF,YAAYA,QAAQH,MAAM,EAAE;QAC5C,OAAOG,QAAQG,GAAG,CAAC,CAACX,SAAWY,OAAOZ;IACxC;IACA,OAAO,OAAOO,IAAI,CAAC,SAAS,KAAK,YAAYA,IAAI,CAAC,SAAS,GAAG;QAACA,IAAI,CAAC,SAAS;KAAC,GAAG,EAAE;AACrF;AAEA,4EAA4E,GAC5E,OAAO,SAASM,cAAcC,KAAc;QAI3B;IAHf,IAAI,OAAOA,UAAU,YAAYC,OAAOC,QAAQ,CAACF,QAAQ;QACvD,OAAO,IAAIG,KAAKH,OAAOI,cAAc;IACvC;IACA,MAAMC,SAAUL,0BAAD,gBAAA,AAACA,MAAsDM,MAAM,qBAA7D,mBAACN;IAChB,OAAOK,SAASA,OAAOD,cAAc,KAAK;AAC5C;AAEA,SAASG,KAAKC,KAAmD;IAC/D,qBACE,MAACzB;QAAM0B,SAAS;;0BACd,KAACzB;gBAAW0B,SAAQ;gBAAUC,OAAM;0BACjCH,MAAMI,KAAK;;0BAEd,KAAC5B;gBAAW0B,SAAQ;gBAAQG,WAAU;0BACnCL,MAAMM,QAAQ;;;;AAIvB;AAQA;;;;;;;;CAQC,GACD,OAAO,SAASC,gBAAgBP,KAA2B;QAOpCf,uBAWIA,qBAGAA;IApBzB,MAAM,EAAEuB,MAAM,EAAEC,MAAM,EAAExB,IAAI,EAAE,GAAGe;IACjC,wEAAwE;IACxE,2BAA2B;IAC3B,MAAMU,iBAAiBtC;IACvB,MAAMc,UAAUF,YAAYC;IAC5B,MAAM0B,aAAa1B,IAAI,CAACjB,uBAAuB;IAC/C,MAAM4C,QAAQnB,QAAOR,wBAAAA,IAAI,CAAC,kBAAkB,YAAvBA,wBAA2B,MAAOC,CAAAA,QAAQH,MAAM,GAAG,IAAI,CAAA;IAC5E,qBACE,KAACV;QACCwC,QAAQ;QACRC,MAAM5C,eAAe,YAAY;YAAE6C,QAAQ;QAAgB;QAC3DC,cAAc;QACdC,cAAc;kBAEd,cAAA,MAAC1C;YAAM0B,SAAS;;8BACd,MAAC1B;oBAAM2C,WAAW;wBAAEC,IAAI;wBAAUC,IAAI;oBAAM;oBAAGnB,SAAS;;sCACtD,KAACF;4BAAKK,OAAM;sCACTb,eAAcN,sBAAAA,IAAI,CAAC,gBAAgB,YAArBA,sBAAyBA,IAAI,CAAC,YAAY;;sCAE3D,KAACc;4BAAKK,OAAM;sCACTb,eAAcN,qBAAAA,IAAI,CAAC,eAAe,YAApBA,qBAAwBA,IAAI,CAAC,YAAY;;sCAE1D,KAACc;4BAAKK,OAAM;sCAAYQ,QAAQtB,OAAOsB,SAAS;;sCAChD,KAACb;4BAAKK,OAAM;sCACTjB,MAAMC,OAAO,CAACuB,eAAeA,WAAW5B,MAAM,GAC3C4B,WAAWtB,GAAG,CAACC,QAAQ+B,IAAI,CAAC,QAC5Bb;;;;8BAGR,KAACT;oBAAKK,OAAM;8BACTlB,QAAQH,MAAM,iBACb,KAACR;wBAAM2C,WAAU;wBAAMjB,SAAS;wBAAKqB,IAAI;4BAAEC,UAAU;4BAAQC,QAAQ;wBAAI;kCACtEtC,QAAQG,GAAG,CAAC,CAACX,uBACZ,KAACJ;gCAAkBmD,MAAK;gCAAQvB,SAAQ;gCAAWE,OAAO3B,gBAAgBC;+BAA/DA;yBAIf;;gBAGHgC,+BACC,MAACnC;oBAAM0B,SAAS;;sCACd,KAACzB;4BAAW0B,SAAQ;4BAAUC,OAAM;sCACjC;;sCAEH,KAAChC;4BACCqC,QAAQA;4BACRkB,YAAW;4BACXC,UAAUlB;;sCAMZ,KAACjC;4BAAW0B,SAAQ;4BAAUC,OAAM;sCACjC;;;qBAGH;;;;AAIZ;AACAI,gBAAgBqB,WAAW,GAAG;AAE9B,eAAerB,gBAAe"}
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":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,sBAAsB,EAAEC,qBAAqB,EAAEC,cAAc,QAAQ,eAAc;AAC5F,4EAA4E;AAC5E,4EAA4E;AAC5E,4EAA4E;AAC5E,kBAAkB;AAClB,SACEC,wBAAwB,EACxBC,0BAA0B,QACrB,4BAAwB;AAC/B,SAASC,WAAW,QAAQ,uBAAsB;AAClD,SAASC,IAAI,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACvD,SAASC,cAAc,QAAQ,gCAA4B;AAE3D;;;;;;;CAOC,GACD,OAAO,SAASC,gBAAgBC,MAAc;IAC5C,IAAIA,WAAW,UAAU,OAAO;IAChC,IAAIA,WAAW,WAAW,OAAO;IACjC,IAAIA,WAAW,UAAU,OAAOV,sBAAsBW,MAAM;IAC5D,IAAID,WAAW,QAAQ,OAAOV,sBAAsBY,IAAI;IACxD,0DAA0D;IAC1D,IAAIF,WAAW,UAAU,OAAO;IAChC,IAAIA,WAAW,OAAO,OAAO;IAC7B,IAAIA,OAAOG,UAAU,CAAC,UAAU,OAAO,CAAC,KAAK,EAAEH,OAAOI,KAAK,CAAC,QAAQC,MAAM,GAAG;IAC7E,OAAOL;AACT;AAEA,kFAAkF,GAClF,OAAO,SAASM,YAAYC,IAA6B;IACvD,MAAMC,UAAUD,IAAI,CAAC,UAAU;IAC/B,IAAIE,MAAMC,OAAO,CAACF,YAAYA,QAAQH,MAAM,EAAE;QAC5C,OAAOG,QAAQG,GAAG,CAAC,CAACX,SAAWY,OAAOZ;IACxC;IACA,OAAO,OAAOO,IAAI,CAAC,SAAS,KAAK,YAAYA,IAAI,CAAC,SAAS,GAAG;QAACA,IAAI,CAAC,SAAS;KAAC,GAAG,EAAE;AACrF;AAEA,4EAA4E,GAC5E,OAAO,SAASM,cAAcC,KAAc;QAI3B;IAHf,IAAI,OAAOA,UAAU,YAAYC,OAAOC,QAAQ,CAACF,QAAQ;QACvD,OAAO,IAAIG,KAAKH,OAAOI,cAAc;IACvC;IACA,MAAMC,SAAUL,0BAAD,gBAAA,AAACA,MAAsDM,MAAM,qBAA7D,mBAACN;IAChB,OAAOK,SAASA,OAAOD,cAAc,KAAK;AAC5C;AAEA,SAASG,KAAKC,KAAmD;IAC/D,qBACE,MAAC1B;QAAM2B,SAAS;;0BACd,KAAC1B;gBAAW2B,SAAQ;gBAAUC,OAAM;0BACjCH,MAAMI,KAAK;;0BAEd,KAAC7B;gBAAW2B,SAAQ;gBAAQG,WAAU;0BACnCL,MAAMM,QAAQ;;;;AAIvB;AAeA;;;;;;;;CAQC,GACD,OAAO,SAASC,gBAAgBP,KAA2B;QAWpCf,uBAWIA,qBAGAA;IAxBzB,MAAM,EAAEuB,MAAM,EAAEC,MAAM,EAAExB,IAAI,EAAE,GAAGe;IACjC,wEAAwE;IACxE,2BAA2B;IAC3B,MAAMU,iBAAiBvC;IACvB,wEAAwE;IACxE,wEAAwE;IACxE,yBAAyB;IACzB,MAAMwC,QAAQnC;IACd,MAAMU,UAAUF,YAAYC;IAC5B,MAAM2B,aAAa3B,IAAI,CAAClB,uBAAuB;IAC/C,MAAM8C,QAAQpB,QAAOR,wBAAAA,IAAI,CAAC,kBAAkB,YAAvBA,wBAA2B,MAAOC,CAAAA,QAAQH,MAAM,GAAG,IAAI,CAAA;IAC5E,qBACE,KAACX;QACC0C,QAAQ;QACRC,MAAM9C,eAAe,YAAY;YAAE+C,QAAQ;QAAgB;QAC3DC,cAAc;QACdC,cAAc;kBAEd,cAAA,MAAC5C;YAAM2B,SAAS;;8BACd,MAAC3B;oBAAM6C,WAAW;wBAAEC,IAAI;wBAAUC,IAAI;oBAAM;oBAAGpB,SAAS;;sCACtD,KAACF;4BAAKK,OAAM;sCACTb,eAAcN,sBAAAA,IAAI,CAAC,gBAAgB,YAArBA,sBAAyBA,IAAI,CAAC,YAAY;;sCAE3D,KAACc;4BAAKK,OAAM;sCACTb,eAAcN,qBAAAA,IAAI,CAAC,eAAe,YAApBA,qBAAwBA,IAAI,CAAC,YAAY;;sCAE1D,KAACc;4BAAKK,OAAM;sCAAYS,QAAQvB,OAAOuB,SAAS;;sCAChD,KAACd;4BAAKK,OAAM;sCACTjB,MAAMC,OAAO,CAACwB,eAAeA,WAAW7B,MAAM,GAC3C6B,WACGvB,GAAG,CAAC,CAACiC;;+CAAOX,yBAAAA,MAAOY,QAAQ,CAACjC,OAAOgC,uBAAQhC,OAAOgC;+BAClDE,IAAI,CAAC,QACPhB,iBAAAA,SAAU;;;;8BAGnB,KAACT;oBAAKK,OAAM;8BACTlB,QAAQH,MAAM,iBACb,KAACT;wBAAM6C,WAAU;wBAAMlB,SAAS;wBAAKwB,IAAI;4BAAEC,UAAU;4BAAQC,QAAQ;wBAAI;kCACtEzC,QAAQG,GAAG,CAAC,CAACX,uBACZ,KAACL;gCAAkBuD,MAAK;gCAAQ1B,SAAQ;gCAAWE,OAAO3B,gBAAgBC;+BAA/DA;yBAIf;;gBAGHgC,kBAAkBF,uBACjB,MAAClC;oBAAM2B,SAAS;;sCACd,KAAC1B;4BAAW2B,SAAQ;4BAAUC,OAAM;sCACjC;;sCAEH,KAACjC;4BACCsC,QAAQA;4BACRqB,YAAW;4BACXC,UAAUrB;;sCAMZ,KAAClC;4BAAW2B,SAAQ;4BAAUC,OAAM;sCACjC;;;qBAGH;;;;AAIZ;AACAI,gBAAgBwB,WAAW,GAAG;AAE9B,eAAexB,gBAAe"}
@@ -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
- hostId: string;
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, Aglyn.soloConsentGroup(hostId));
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",