@aglyn/plugins-crm 1.0.0-beta.273 → 1.0.0-beta.275
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/README.md +1 -1
- package/package.json +14 -14
- package/src/lib/components/contact-detail-page.js +9 -0
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/crm-email-actions-zone-token.d.ts +42 -0
- package/src/lib/components/crm-email-actions-zone-token.js +19 -0
- package/src/lib/components/crm-email-actions-zone-token.js.map +1 -0
- package/src/lib/components/crm-email-actions-zone.d.ts +27 -0
- package/src/lib/components/crm-email-actions-zone.js +56 -0
- package/src/lib/components/crm-email-actions-zone.js.map +1 -0
- package/src/lib/components/lead-properties-card.js +14 -5
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/plugin.js +11 -0
- package/src/lib/plugin.js.map +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ The CRM is console-only. Its records live in Firestore and it has no canvas comp
|
|
|
32
32
|
- A **CRM** nav item and hub at `/crm`, with the sections Contacts, Leads, Companies, Deals, Tasks, Reports, Fields and Settings as routes beneath it. The old `/contacts` address redirects to the hub. The hub is code-split, requires the `data.manage` permission, and sits behind the `crm` feature flag.
|
|
33
33
|
- Dashboard widgets "Tasks due" and "CRM at a glance" in both the `hostDashboard` (one site) and `orgDashboard` (whole organization) slots.
|
|
34
34
|
- A "Saves to contact fields" widget in the `formContactFields` slot, a zone the forms plugin hosts on a form's page.
|
|
35
|
-
-
|
|
35
|
+
- Three zones the CRM hosts for other plugins to draw in: one for booking a meeting from a record, one that says which campaign or link a record came from, and one before a contact's or a lead's Send email for another way to write to the person, such as answering them in a sequence's thread.
|
|
36
36
|
- The CRM's record routes, so other surfaces can link to a record.
|
|
37
37
|
- Record count sources for `lead` (the leads a site captured) and `contact` (the organization's, a count that crosses sites) (`plugin-record-counts`), which a campaign's conversion report divides by.
|
|
38
38
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-crm",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.275",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.275",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.275",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.275",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.275",
|
|
32
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.275",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.275",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.275",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.275",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.275",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.275",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.275",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.275",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.275",
|
|
41
41
|
"@dnd-kit/core": "^6.3.1",
|
|
42
42
|
"@dnd-kit/utilities": "^3.2.2",
|
|
43
43
|
"@swc/helpers": "0.5.23",
|
|
@@ -42,6 +42,7 @@ import ContactPropertiesCard from "./contact-properties-card.js";
|
|
|
42
42
|
import ContactTimelineCard from "./contact-timeline-card.js";
|
|
43
43
|
import { AddToListButton } from "./add-to-list-button.js";
|
|
44
44
|
import { useBookingDoor } from "./crm-booking-zone.js";
|
|
45
|
+
import { CrmRecordEmailActionsZone } from "./crm-email-actions-zone.js";
|
|
45
46
|
import { CrmSendEmailButton } from "./crm-send-email-button.js";
|
|
46
47
|
import { ContactDealsCard } from "./contact-deals-card.js";
|
|
47
48
|
import { CrmCallButton } from "./crm-call-actions.js";
|
|
@@ -350,6 +351,14 @@ const contactDocsHelp = pluginDocsHelp('contactRecord', {
|
|
|
350
351
|
doNotCall: record.doNotCall,
|
|
351
352
|
suiteLocked: !suiteIncluded
|
|
352
353
|
}),
|
|
354
|
+
/*#__PURE__*/ _jsx(CrmRecordEmailActionsZone, {
|
|
355
|
+
hostId: siteHostId,
|
|
356
|
+
org: org,
|
|
357
|
+
kind: "contact",
|
|
358
|
+
recordId: id,
|
|
359
|
+
name: record.name,
|
|
360
|
+
email: record.email || null
|
|
361
|
+
}),
|
|
353
362
|
/*#__PURE__*/ _jsx(CrmSendEmailButton, {
|
|
354
363
|
hostId: siteHostId,
|
|
355
364
|
org: org,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-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 * as Aglyn from '@aglyn/aglyn'\nimport { CONTACT_LIFECYCLE_STAGE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n pluginRecordByEmailHref,\n pluginRecordHref,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { mdiDeleteOutline, mdiMerge } from '@aglyn/shared-data-mdi'\nimport { AppLink, MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport { Stack, Tooltip, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useParams, useRouter } from 'next/navigation'\nimport { useCallback, useMemo } from 'react'\nimport { useContactRemove } from '../hooks/use-contact-remove'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { CrmCreateSiteDefault, useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { crmShareChipFor } from '../model/crm-sharing'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { contactPrimaryGroup, contactRecordFromDoc } from '../model/contact-record'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport ContactAssociationsCard from './contact-associations-card'\nimport ContactDuplicatesCard from './contact-duplicates-card'\nimport ContactKnownByCard from './contact-known-by-card'\nimport ContactMergeDialog, { useContactMergeDialog } from './contact-merge-dialog'\nimport ContactPropertiesCard from './contact-properties-card'\nimport ContactTimelineCard from './contact-timeline-card'\nimport { AddToListButton } from './add-to-list-button'\nimport { useBookingDoor } from './crm-booking-zone'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport { ContactDealsCard } from './contact-deals-card'\nimport { CrmCallButton } from './crm-call-actions'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmAccountLockChip, CrmEmailGatewayChip } from './crm-email-check'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { CrmSuiteNotice, crmSuiteIncluded, crmSuiteLockedReason } from './crm-suite-lock'\nimport { useErasePersonAction } from './erase-person-action'\nimport RecordFilesCard from './record-files-card'\nimport { CrmShareChipView, RecordSharingCard } from './record-sharing-card'\nimport { RecordTasksCard } from './record-tasks-card'\nimport { useSiteRouteContext } from './use-site-route-context'\nimport { useOrgMembers } from './use-org-members'\n\nconst contactDocsHelp = pluginDocsHelp('contactRecord', { anchor: '#the-record-page' })\n\n/**\n * `/crm/contacts/{contactId}` — one person (AGL-2596).\n *\n * A ROUTE rather than the v1 drawer, because a person is the thing every\n * other CRM record points at: a deal, a task and an activity all name a\n * contact, and a link that opens a drawer on top of a list is not a link\n * anybody can paste.\n *\n * ## What this page reads\n *\n * ONE document, by id, through the scoped rules — a single-document get is\n * judged on that document's own `visibleTo`, so no `array-contains-any`\n * filter is needed here the way it is on the list. The row is then flattened\n * through the viewing group's facet ONCE (`contactRecordFromDoc`) and the\n * cards below read the flat record: none of them reaches into\n * `facets.{groupId}` for itself, which is what keeps a card from ever\n * showing another holder's notes.\n *\n * Beyond the document: the team roster (one request, for the owner picker)\n * and the site's campaigns (one listen, for the filing picker) — both paid\n * here because opening a record IS the ask for them, and neither is paid by\n * the list.\n *\n * ## The cards\n *\n * Each in its own file, so a card can grow without this page growing:\n * properties (the editable profile, with the org's custom fields under it),\n * the relationship (sources, attribution, consent, campaign filing) and the\n * timeline. Other CRM sections add a card here by inserting one import and\n * one line.\n *\n * ## Delete is a detach\n *\n * The row is shared by every site that has captured this person, so the\n * action in the overflow menu drops THIS group's facet, consent, attribution\n * and scope tokens, and deletes the document only when nobody else is left\n * holding it. `crm/contact-remove` decides which against the stored\n * document and keeps any marketing refusal the person gave (AGL-3338); the\n * rules refuse a client the write. Not the erasure path: a privacy erasure\n * removes the person regardless of who else holds them.\n */\nexport function ContactDetailPage(props: CrmDetailPageProps) {\n const { hostId, org, id, basePath } = props\n const routes = crmRoutes(basePath)\n const router = useRouter()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The org root, the viewing group and what this viewer may list — the\n // merge search's and the duplicates query's one clause — from the one\n // scope hook (AGL-2614); at the organization level the group is `null`\n // and resolved per person below, and the listeners carry no clause\n // (AGL-2630).\n const {\n scope,\n orgId,\n consentGroup: viewingGroup,\n visibleTo,\n } = useCrmScope({ hostId, org })\n const mount = useCrmOrgMount()\n const merge = useContactMergeDialog()\n /*\n * THE CRM (AGL-2788). This page is the CRM's, and the shell mounts no CRM\n * page for a plan without it (AGL-2851). What locks below — owner, stage\n * and company; tasks, deals, files and custom fields; a logged call or\n * activity; one-to-one email; a merge — draws only for an org whose plan\n * lacks the CRM. The shell mounted this page once the org had settled, so\n * this is the plan, not a paint of the org still loading.\n */\n const suiteIncluded = crmSuiteIncluded(org)\n\n const {\n data: row,\n status,\n fromCache,\n } = useFirestoreDoc<Record<string, any>>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'contacts', id) : null),\n [firestore, scope, id],\n { idField: '$id' },\n )\n /*\n * The controller this page is showing. Under a site: the sites declared\n * to be one sender, or this site alone — resolved from the org document\n * the shell already passed, so it costs no read. At the ORGANIZATION\n * level: the person's own PRIMARY holder, the first capturing site's\n * group, so an org-wide member reads the profile that site keeps and an\n * edit goes back to the same facet; the \"Known by\" card says which other\n * sites hold one.\n */\n const consentGroup = useMemo(\n () => viewingGroup ?? contactPrimaryGroup(row, org as Record<string, unknown>),\n [viewingGroup, row, org],\n )\n /**\n * The SITE the page acts as, for what needs one — the lead lookup, the\n * campaign filing, the site's activity feed: the mounted site, or the\n * primary holder's, or nothing for a row no site has captured.\n */\n const siteHostId = hostId ?? (consentGroup.hostId || null)\n /*\n * The feed the act is logged in, decided by the level it was performed\n * at (AGL-2738) and so given the MOUNTED site rather than `siteHostId`:\n * at the org hub a client-direct append to the holder's own site would\n * face a gate the record write never had to pass.\n */\n const logActivity = useCrmActivityLogger(hostId)\n const record = useMemo(\n () => (row ? contactRecordFromDoc(row, consentGroup) : null),\n [row, consentGroup],\n )\n const notFound = Boolean(scope) && status !== 'loading' && !row\n /** How the site the delete detaches from is named — see `handleRemove`. */\n const siteLabel = hostId\n ? 'this site'\n : siteHostId\n ? (mount?.siteName(siteHostId) ?? siteHostId)\n : 'its site'\n\n // The roster, for the owner picker and the owner's name — read because a\n // record page is the one place both are shown.\n const members = useOrgMembers(orgId, { enabled: Boolean(record) })\n /*\n * Where this person's ORDERS are read (AGL-2622): the site's orders list,\n * narrowed to their address, at the address the plugin that keeps orders\n * publishes. The count on the header is the number; the list is the rows.\n * Built from the route params already in the URL, so no document is read\n * to draw a link, and absent off a site — the org-level mount has no\n * orders list to point at — and where no plugin keeps orders.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const ordersHref =\n params?.orgSlug && params?.host && record?.email\n ? pluginRecordByEmailHref(\n 'order',\n { orgSlug: String(params.orgSlug), host: String(params.host) },\n record.email,\n )\n : null\n /*\n * Where this person's BOOKINGS are read (AGL-2660): the site's Bookings\n * page narrowed to their address, under the same match rule the booking\n * row's own \"View in CRM\" uses. Offered only where the site has a booking\n * door — a site that never enabled Bookings should not carry the row. At\n * the organization level the capturing site's page, addressed through\n * the subdomain the mount knows it by.\n */\n const bookingDoor = useBookingDoor(siteHostId, org)\n const siteSlug = params?.host\n ? String(params.host)\n : siteHostId && mount\n ? mount.siteSubdomain(siteHostId)\n : null\n const bookingsHref =\n bookingDoor.open && params?.orgSlug && siteSlug && record?.email\n ? pluginRecordByEmailHref(\n 'booking',\n { orgSlug: String(params.orgSlug), host: siteSlug },\n record.email,\n )\n : null\n\n /*\n * Where a campaign entry on the timeline links to: the email's own report\n * (AGL-2616), at the address the plugin that keeps messages publishes as\n * `emailMessage` (AGL-3080). Only a campaign THIS site sent can be\n * addressed — a sibling site in the same consent group has pages of its\n * own under a subdomain this page does not know — so the builder answers\n * `null` for those, and where nothing publishes the kind, and the entry\n * draws unlinked.\n */\n const siteRoute = useSiteRouteContext()\n const campaignHref = useCallback(\n (email: Aglyn.ContactCampaignEmail) =>\n siteRoute && email.hostId === hostId\n ? pluginRecordHref('emailMessage', siteRoute, email.campaignId)\n : null,\n [siteRoute, hostId],\n )\n\n const contactRemove = useContactRemove(hostId)\n const handleRemove = useCallback(async () => {\n if (!row || !scope) return\n const confirmed = await confirm({\n title: 'Delete this contact?',\n description:\n `\"${record?.email ?? id}\" is removed from ${siteLabel}'s ` +\n 'Contacts, along with its notes, tags and timeline. Other sites ' +\n 'that captured the same person keep their own records. Their form ' +\n 'submissions, orders, bookings, and membership records are ' +\n 'separate — delete those from their own pages if the request ' +\n 'covers them.',\n confirmationText: 'Delete contact',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n // The route decides against the stored document, keeps any refusal,\n // and restamps what the Contacts list filters by (AGL-3338).\n const removed = await contactRemove.removeOne(id)\n // `siteLabel` and not \"this site\": at the organization level the line\n // lands in a feed that spans every site, so it has to name the one.\n logActivity(`Removed contact from ${siteLabel}`, {\n type: 'contact',\n id,\n name: record?.name || record?.email,\n })\n enqueueSnackbar(\n removed === 'deleted'\n ? 'Contact deleted'\n : `Contact removed from ${siteLabel}`,\n { variant: 'success', persist: false },\n )\n router.push(routes.section('contacts'))\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [\n confirm,\n contactRemove,\n enqueueSnackbar,\n id,\n logActivity,\n record,\n router,\n routes,\n row,\n scope,\n siteLabel,\n ])\n\n const overflowItems: RowActionsMenuItem[] = [\n /*\n * Two records for one person become one (AGL-2625). Opened empty, to be\n * searched, with the OTHER record surviving: a reader on the record they\n * want gone says \"merge this into…\". The dialog lets them switch.\n */\n {\n key: 'merge',\n label: 'Merge into…',\n icon: <MdiIcon path={mdiMerge.path} size={0.8} />,\n disabled: !row || !suiteIncluded,\n disabledReason: suiteIncluded ? 'The contact has not loaded' : crmSuiteLockedReason(),\n onClick: () => merge.open(),\n },\n {\n key: 'remove',\n label: 'Delete contact',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !row,\n disabledReason: 'The contact has not loaded',\n onClick: () => void handleRemove(),\n },\n ]\n // The privacy erasure (AGL-2623): its item joins the overflow, its banner\n // sits under the header while a request waits, its dialog renders once.\n const erase = useErasePersonAction({\n // Filed as the site the page acts as — at the organization level the\n // person's primary holder (AGL-2630).\n hostId: siteHostId,\n orgId,\n subject: record ? { kind: 'contact', id, email: record.email } : null,\n requestedAtMs: Aglyn.readErasureRequestedAtMs(row),\n })\n\n if (notFound) {\n return (\n <CrmRecordHeader\n kind=\"Contact\"\n title={undefined}\n help={contactDocsHelp}\n backHref={routes.section('contacts')}\n backLabel=\"Back to contacts\"\n >\n {/*\n * Not \"no data\". A contact that cannot be read is a different\n * situation from one with an empty profile — it may have been\n * removed from this site, or be held only by a site this reader\n * cannot see.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This contact could not be loaded. It may have been removed from this site.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n\n return (\n // A create opened from this page — a task, an activity, a deal — files\n // under the site that captured this person unless the reader says\n // otherwise (AGL-2630).\n <CrmCreateSiteDefault hostId={siteHostId}>\n <Stack spacing={3}>\n <CrmRecordHeader\n kind=\"Contact\"\n title={record ? record.name || record.email : undefined}\n // The identity, and the other addresses a merge folded into it.\n subtitle={\n record\n ? [record.email, ...record.alternateEmails.map((email) => `also ${email}`)].join(' · ')\n : undefined\n }\n help={contactDocsHelp}\n backHref={routes.section('contacts')}\n backLabel=\"Back to contacts\"\n actions={\n record ? (\n <>\n <AddToListButton hostId={hostId} org={org} contactId={id} email={record.email} />\n {/* Dial the number, and log the call it was (AGL-2661). */}\n <CrmCallButton\n hostId={siteHostId}\n org={org}\n link={{ contactId: id }}\n // The main number, or the mobile when that is all there is (AGL-3515).\n phone={record.phone || record.mobilePhone}\n doNotCall={record.doNotCall}\n suiteLocked={!suiteIncluded}\n />\n <CrmSendEmailButton\n hostId={siteHostId}\n org={org}\n contactId={id}\n email={record.email}\n name={record.name}\n emailState={record.emailState}\n suiteLocked={!suiteIncluded}\n />\n </>\n ) : null\n }\n menuItems={[...overflowItems, ...erase.menuItems]}\n // The booking door (AGL-2660): this site's services, or at the\n // organization level the capturing site's.\n booking={record ? { hostId: siteHostId, org, kind: 'contact', recordId: id } : undefined}\n loading={!record}\n chips={\n record ? (\n <>\n {/* The verdict on the address (AGL-3245), first: a bounce or a\n do-not-contact mark is what a person deciding to write must see. */}\n <CrmEmailStateChip state={record.emailState} />\n {/* Seen here only through a share (AGL-3336): who shared it. */}\n <CrmShareChipView chip={crmShareChipFor(row, viewingGroup?.hostIds ?? [])} org={org} />\n {/* The mail gateway in front of the address (AGL-3328). */}\n <CrmEmailGatewayChip\n hostId={siteHostId}\n email={record.email}\n emailState={record.emailState}\n enabled={suiteIncluded}\n />\n <CrmAccountLockChip\n hostId={siteHostId}\n email={record.email}\n enabled={suiteIncluded}\n />\n <CrmRecordChip\n label=\"Stage\"\n value={\n record.lifecycleStage\n ? CONTACT_LIFECYCLE_STAGE_LABELS[record.lifecycleStage]\n : undefined\n }\n />\n <CrmRecordChip\n label=\"Owner\"\n value={record.ownerUid ? members.memberName(record.ownerUid) : undefined}\n />\n {/*\n The last time they opened or clicked one of this site's\n campaigns (AGL-2616) — the relationship's pulse, beside the\n owner who keeps it.\n */}\n <CrmRecordChip\n label=\"Last engaged\"\n value={\n record.lastEmailEngagementAtMs ? (\n <Tooltip title={new Date(record.lastEmailEngagementAtMs).toLocaleString()}>\n <span>\n {Aglyn.activityTimeLabel(record.lastEmailEngagementAtMs, Date.now())}\n </span>\n </Tooltip>\n ) : undefined\n }\n />\n <CrmRecordChip\n label=\"Orders\"\n value={\n record.ordersCount > 0 ? (\n ordersHref ? (\n <AppLink href={ordersHref} color=\"inherit\" underline=\"hover\">\n {`${record.ordersCount.toLocaleString()} · $${(record.ltvCents / 100).toFixed(2)} lifetime`}\n </AppLink>\n ) : (\n `${record.ordersCount.toLocaleString()} · $${(record.ltvCents / 100).toFixed(2)} lifetime`\n )\n ) : undefined\n }\n />\n </>\n ) : null\n }\n >\n {erase.banner}\n </CrmRecordHeader>\n {record && row && scope ? (\n <>\n {suiteIncluded ? null : (\n <CrmSuiteNotice>\n {'Working this contact — the owner, stage and company, tasks, ' +\n 'deals, files and custom fields, logging a call or an activity, ' +\n 'one-to-one email and merging duplicates — is part of the CRM.'}\n </CrmSuiteNotice>\n )}\n {/*\n The cross-site fact, at the organization level only (AGL-2630):\n which sites know this person, and their consent for each. Under\n a site the page IS one site's view and the card would say so on\n every record.\n */}\n {/* What an assistant proposes about the person (AGL-2917), where the plan works a contact. */}\n {suiteIncluded ? (\n <CrmRecordInsightsZone\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"contact\"\n recordId={id}\n name={record.name || record.email}\n taskLink={{ contactId: id }}\n />\n ) : null}\n {mount ? <ContactKnownByCard row={row} contactId={id} org={org} /> : null}\n {/* Where the person is visible and why, and a manager's share (AGL-3336). */}\n <RecordSharingCard\n object=\"contacts\"\n id={id}\n record={row}\n hostId={hostId ?? null}\n orgId={orgId}\n org={org}\n viewingHostIds={viewingGroup?.hostIds}\n />\n <ContactPropertiesCard\n hostId={hostId}\n org={org}\n record={record}\n orgId={orgId}\n consentGroup={consentGroup}\n seed={{ status, fromCache }}\n members={members}\n suiteLocked={!suiteIncluded}\n />\n <ContactAssociationsCard\n hostId={siteHostId}\n record={record}\n row={row}\n consentGroup={consentGroup}\n seed={{ status, fromCache }}\n basePath={basePath}\n bookingsHref={bookingsHref}\n />\n <ContactTimelineCard\n hostId={hostId}\n org={org}\n contactId={id}\n contact={row}\n campaignHref={campaignHref}\n suiteLocked={!suiteIncluded}\n />\n {/*\n On a plan without the CRM, the cards that only work a record —\n deals, tasks, files — are named in the notice above rather than\n drawn as upsells in a row.\n */}\n {suiteIncluded ? (\n <>\n <ContactDealsCard\n hostId={hostId}\n org={org}\n basePath={basePath}\n contactId={id}\n contactName={record.name || record.email}\n />\n <RecordTasksCard hostId={hostId} org={org} basePath={basePath} contactId={id} />\n {/*\n The files are the VIEWING holder's, like everything else on this\n page: the write is a facet path, so a contract one brand filed\n never appears on a sibling brand's copy of the person.\n */}\n <RecordFilesCard\n scope={scope}\n collection=\"contacts\"\n recordId={id}\n hostId={hostId}\n facetGroupId={consentGroup.groupId}\n mediaIds={record.mediaIds}\n topic=\"contactRecord\"\n />\n </>\n ) : null}\n <ContactDuplicatesCard\n current={{ id, doc: row }}\n scope={scope}\n consentGroup={consentGroup}\n visibleTo={visibleTo}\n basePath={basePath}\n onMerge={(candidate) => merge.open({ other: candidate, keep: 'current' })}\n suiteLocked={!suiteIncluded}\n />\n <ContactMergeDialog\n {...merge.state}\n onClose={merge.close}\n hostId={siteHostId}\n current={{ id, doc: row }}\n scope={scope}\n consentGroup={consentGroup}\n visibleTo={visibleTo}\n memberName={members.memberName}\n // The page's own record was folded away: its address now opens\n // the survivor, which is where the reader is taken.\n onMerged={(survivorId) => {\n if (survivorId !== id) router.replace(routes.contact(survivorId))\n }}\n />\n </>\n ) : null}\n {erase.dialog}\n </Stack>\n </CrmCreateSiteDefault>\n )\n}\nContactDetailPage.displayName = 'ContactDetailPage'\n\nexport default ContactDetailPage\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","pluginDocsHelp","pluginRecordByEmailHref","pluginRecordHref","mdiDeleteOutline","mdiMerge","AppLink","MdiIcon","useConfirmationContext","useSnackbar","useFirestore","useFirestoreDoc","Stack","Tooltip","Typography","doc","useParams","useRouter","useCallback","useMemo","useContactRemove","useCrmActivityLogger","CrmCreateSiteDefault","useCrmOrgMount","crmShareChipFor","useCrmScope","contactPrimaryGroup","contactRecordFromDoc","crmRoutes","ContactAssociationsCard","ContactDuplicatesCard","ContactKnownByCard","ContactMergeDialog","useContactMergeDialog","ContactPropertiesCard","ContactTimelineCard","AddToListButton","useBookingDoor","CrmSendEmailButton","ContactDealsCard","CrmCallButton","CrmEmailStateChip","CrmAccountLockChip","CrmEmailGatewayChip","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","CrmSuiteNotice","crmSuiteIncluded","crmSuiteLockedReason","useErasePersonAction","RecordFilesCard","CrmShareChipView","RecordSharingCard","RecordTasksCard","useSiteRouteContext","useOrgMembers","contactDocsHelp","anchor","ContactDetailPage","props","hostId","org","id","basePath","routes","router","firestore","enqueueSnackbar","confirm","scope","orgId","consentGroup","viewingGroup","visibleTo","mount","merge","suiteIncluded","data","row","status","fromCache","idField","siteHostId","logActivity","record","notFound","Boolean","siteLabel","siteName","members","enabled","params","ordersHref","orgSlug","host","email","String","bookingDoor","siteSlug","siteSubdomain","bookingsHref","open","siteRoute","campaignHref","campaignId","contactRemove","handleRemove","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","removed","removeOne","type","name","variant","persist","push","section","error","console","allowDuplicate","overflowItems","key","label","icon","path","size","disabled","disabledReason","onClick","destructive","erase","subject","kind","requestedAtMs","readErasureRequestedAtMs","undefined","help","backHref","backLabel","spacing","subtitle","alternateEmails","map","join","actions","contactId","link","phone","mobilePhone","doNotCall","suiteLocked","emailState","menuItems","booking","recordId","loading","chips","state","chip","hostIds","value","lifecycleStage","ownerUid","memberName","lastEmailEngagementAtMs","Date","toLocaleString","span","activityTimeLabel","now","ordersCount","href","underline","ltvCents","toFixed","banner","taskLink","object","viewingHostIds","seed","contact","contactName","collection","facetGroupId","groupId","mediaIds","topic","current","onMerge","candidate","other","keep","onClose","close","onMerged","survivorId","replace","dialog","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-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 * as Aglyn from '@aglyn/aglyn'\nimport { CONTACT_LIFECYCLE_STAGE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\nimport {\n pluginRecordByEmailHref,\n pluginRecordHref,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { mdiDeleteOutline, mdiMerge } from '@aglyn/shared-data-mdi'\nimport { AppLink, MdiIcon, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport { Stack, Tooltip, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useParams, useRouter } from 'next/navigation'\nimport { useCallback, useMemo } from 'react'\nimport { useContactRemove } from '../hooks/use-contact-remove'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { CrmCreateSiteDefault, useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { crmShareChipFor } from '../model/crm-sharing'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { contactPrimaryGroup, contactRecordFromDoc } from '../model/contact-record'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport ContactAssociationsCard from './contact-associations-card'\nimport ContactDuplicatesCard from './contact-duplicates-card'\nimport ContactKnownByCard from './contact-known-by-card'\nimport ContactMergeDialog, { useContactMergeDialog } from './contact-merge-dialog'\nimport ContactPropertiesCard from './contact-properties-card'\nimport ContactTimelineCard from './contact-timeline-card'\nimport { AddToListButton } from './add-to-list-button'\nimport { useBookingDoor } from './crm-booking-zone'\nimport { CrmRecordEmailActionsZone } from './crm-email-actions-zone'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport { ContactDealsCard } from './contact-deals-card'\nimport { CrmCallButton } from './crm-call-actions'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmAccountLockChip, CrmEmailGatewayChip } from './crm-email-check'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { CrmSuiteNotice, crmSuiteIncluded, crmSuiteLockedReason } from './crm-suite-lock'\nimport { useErasePersonAction } from './erase-person-action'\nimport RecordFilesCard from './record-files-card'\nimport { CrmShareChipView, RecordSharingCard } from './record-sharing-card'\nimport { RecordTasksCard } from './record-tasks-card'\nimport { useSiteRouteContext } from './use-site-route-context'\nimport { useOrgMembers } from './use-org-members'\n\nconst contactDocsHelp = pluginDocsHelp('contactRecord', { anchor: '#the-record-page' })\n\n/**\n * `/crm/contacts/{contactId}` — one person (AGL-2596).\n *\n * A ROUTE rather than the v1 drawer, because a person is the thing every\n * other CRM record points at: a deal, a task and an activity all name a\n * contact, and a link that opens a drawer on top of a list is not a link\n * anybody can paste.\n *\n * ## What this page reads\n *\n * ONE document, by id, through the scoped rules — a single-document get is\n * judged on that document's own `visibleTo`, so no `array-contains-any`\n * filter is needed here the way it is on the list. The row is then flattened\n * through the viewing group's facet ONCE (`contactRecordFromDoc`) and the\n * cards below read the flat record: none of them reaches into\n * `facets.{groupId}` for itself, which is what keeps a card from ever\n * showing another holder's notes.\n *\n * Beyond the document: the team roster (one request, for the owner picker)\n * and the site's campaigns (one listen, for the filing picker) — both paid\n * here because opening a record IS the ask for them, and neither is paid by\n * the list.\n *\n * ## The cards\n *\n * Each in its own file, so a card can grow without this page growing:\n * properties (the editable profile, with the org's custom fields under it),\n * the relationship (sources, attribution, consent, campaign filing) and the\n * timeline. Other CRM sections add a card here by inserting one import and\n * one line.\n *\n * ## Delete is a detach\n *\n * The row is shared by every site that has captured this person, so the\n * action in the overflow menu drops THIS group's facet, consent, attribution\n * and scope tokens, and deletes the document only when nobody else is left\n * holding it. `crm/contact-remove` decides which against the stored\n * document and keeps any marketing refusal the person gave (AGL-3338); the\n * rules refuse a client the write. Not the erasure path: a privacy erasure\n * removes the person regardless of who else holds them.\n */\nexport function ContactDetailPage(props: CrmDetailPageProps) {\n const { hostId, org, id, basePath } = props\n const routes = crmRoutes(basePath)\n const router = useRouter()\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The org root, the viewing group and what this viewer may list — the\n // merge search's and the duplicates query's one clause — from the one\n // scope hook (AGL-2614); at the organization level the group is `null`\n // and resolved per person below, and the listeners carry no clause\n // (AGL-2630).\n const {\n scope,\n orgId,\n consentGroup: viewingGroup,\n visibleTo,\n } = useCrmScope({ hostId, org })\n const mount = useCrmOrgMount()\n const merge = useContactMergeDialog()\n /*\n * THE CRM (AGL-2788). This page is the CRM's, and the shell mounts no CRM\n * page for a plan without it (AGL-2851). What locks below — owner, stage\n * and company; tasks, deals, files and custom fields; a logged call or\n * activity; one-to-one email; a merge — draws only for an org whose plan\n * lacks the CRM. The shell mounted this page once the org had settled, so\n * this is the plan, not a paint of the org still loading.\n */\n const suiteIncluded = crmSuiteIncluded(org)\n\n const {\n data: row,\n status,\n fromCache,\n } = useFirestoreDoc<Record<string, any>>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'contacts', id) : null),\n [firestore, scope, id],\n { idField: '$id' },\n )\n /*\n * The controller this page is showing. Under a site: the sites declared\n * to be one sender, or this site alone — resolved from the org document\n * the shell already passed, so it costs no read. At the ORGANIZATION\n * level: the person's own PRIMARY holder, the first capturing site's\n * group, so an org-wide member reads the profile that site keeps and an\n * edit goes back to the same facet; the \"Known by\" card says which other\n * sites hold one.\n */\n const consentGroup = useMemo(\n () => viewingGroup ?? contactPrimaryGroup(row, org as Record<string, unknown>),\n [viewingGroup, row, org],\n )\n /**\n * The SITE the page acts as, for what needs one — the lead lookup, the\n * campaign filing, the site's activity feed: the mounted site, or the\n * primary holder's, or nothing for a row no site has captured.\n */\n const siteHostId = hostId ?? (consentGroup.hostId || null)\n /*\n * The feed the act is logged in, decided by the level it was performed\n * at (AGL-2738) and so given the MOUNTED site rather than `siteHostId`:\n * at the org hub a client-direct append to the holder's own site would\n * face a gate the record write never had to pass.\n */\n const logActivity = useCrmActivityLogger(hostId)\n const record = useMemo(\n () => (row ? contactRecordFromDoc(row, consentGroup) : null),\n [row, consentGroup],\n )\n const notFound = Boolean(scope) && status !== 'loading' && !row\n /** How the site the delete detaches from is named — see `handleRemove`. */\n const siteLabel = hostId\n ? 'this site'\n : siteHostId\n ? (mount?.siteName(siteHostId) ?? siteHostId)\n : 'its site'\n\n // The roster, for the owner picker and the owner's name — read because a\n // record page is the one place both are shown.\n const members = useOrgMembers(orgId, { enabled: Boolean(record) })\n /*\n * Where this person's ORDERS are read (AGL-2622): the site's orders list,\n * narrowed to their address, at the address the plugin that keeps orders\n * publishes. The count on the header is the number; the list is the rows.\n * Built from the route params already in the URL, so no document is read\n * to draw a link, and absent off a site — the org-level mount has no\n * orders list to point at — and where no plugin keeps orders.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const ordersHref =\n params?.orgSlug && params?.host && record?.email\n ? pluginRecordByEmailHref(\n 'order',\n { orgSlug: String(params.orgSlug), host: String(params.host) },\n record.email,\n )\n : null\n /*\n * Where this person's BOOKINGS are read (AGL-2660): the site's Bookings\n * page narrowed to their address, under the same match rule the booking\n * row's own \"View in CRM\" uses. Offered only where the site has a booking\n * door — a site that never enabled Bookings should not carry the row. At\n * the organization level the capturing site's page, addressed through\n * the subdomain the mount knows it by.\n */\n const bookingDoor = useBookingDoor(siteHostId, org)\n const siteSlug = params?.host\n ? String(params.host)\n : siteHostId && mount\n ? mount.siteSubdomain(siteHostId)\n : null\n const bookingsHref =\n bookingDoor.open && params?.orgSlug && siteSlug && record?.email\n ? pluginRecordByEmailHref(\n 'booking',\n { orgSlug: String(params.orgSlug), host: siteSlug },\n record.email,\n )\n : null\n\n /*\n * Where a campaign entry on the timeline links to: the email's own report\n * (AGL-2616), at the address the plugin that keeps messages publishes as\n * `emailMessage` (AGL-3080). Only a campaign THIS site sent can be\n * addressed — a sibling site in the same consent group has pages of its\n * own under a subdomain this page does not know — so the builder answers\n * `null` for those, and where nothing publishes the kind, and the entry\n * draws unlinked.\n */\n const siteRoute = useSiteRouteContext()\n const campaignHref = useCallback(\n (email: Aglyn.ContactCampaignEmail) =>\n siteRoute && email.hostId === hostId\n ? pluginRecordHref('emailMessage', siteRoute, email.campaignId)\n : null,\n [siteRoute, hostId],\n )\n\n const contactRemove = useContactRemove(hostId)\n const handleRemove = useCallback(async () => {\n if (!row || !scope) return\n const confirmed = await confirm({\n title: 'Delete this contact?',\n description:\n `\"${record?.email ?? id}\" is removed from ${siteLabel}'s ` +\n 'Contacts, along with its notes, tags and timeline. Other sites ' +\n 'that captured the same person keep their own records. Their form ' +\n 'submissions, orders, bookings, and membership records are ' +\n 'separate — delete those from their own pages if the request ' +\n 'covers them.',\n confirmationText: 'Delete contact',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n // The route decides against the stored document, keeps any refusal,\n // and restamps what the Contacts list filters by (AGL-3338).\n const removed = await contactRemove.removeOne(id)\n // `siteLabel` and not \"this site\": at the organization level the line\n // lands in a feed that spans every site, so it has to name the one.\n logActivity(`Removed contact from ${siteLabel}`, {\n type: 'contact',\n id,\n name: record?.name || record?.email,\n })\n enqueueSnackbar(\n removed === 'deleted'\n ? 'Contact deleted'\n : `Contact removed from ${siteLabel}`,\n { variant: 'success', persist: false },\n )\n router.push(routes.section('contacts'))\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [\n confirm,\n contactRemove,\n enqueueSnackbar,\n id,\n logActivity,\n record,\n router,\n routes,\n row,\n scope,\n siteLabel,\n ])\n\n const overflowItems: RowActionsMenuItem[] = [\n /*\n * Two records for one person become one (AGL-2625). Opened empty, to be\n * searched, with the OTHER record surviving: a reader on the record they\n * want gone says \"merge this into…\". The dialog lets them switch.\n */\n {\n key: 'merge',\n label: 'Merge into…',\n icon: <MdiIcon path={mdiMerge.path} size={0.8} />,\n disabled: !row || !suiteIncluded,\n disabledReason: suiteIncluded ? 'The contact has not loaded' : crmSuiteLockedReason(),\n onClick: () => merge.open(),\n },\n {\n key: 'remove',\n label: 'Delete contact',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !row,\n disabledReason: 'The contact has not loaded',\n onClick: () => void handleRemove(),\n },\n ]\n // The privacy erasure (AGL-2623): its item joins the overflow, its banner\n // sits under the header while a request waits, its dialog renders once.\n const erase = useErasePersonAction({\n // Filed as the site the page acts as — at the organization level the\n // person's primary holder (AGL-2630).\n hostId: siteHostId,\n orgId,\n subject: record ? { kind: 'contact', id, email: record.email } : null,\n requestedAtMs: Aglyn.readErasureRequestedAtMs(row),\n })\n\n if (notFound) {\n return (\n <CrmRecordHeader\n kind=\"Contact\"\n title={undefined}\n help={contactDocsHelp}\n backHref={routes.section('contacts')}\n backLabel=\"Back to contacts\"\n >\n {/*\n * Not \"no data\". A contact that cannot be read is a different\n * situation from one with an empty profile — it may have been\n * removed from this site, or be held only by a site this reader\n * cannot see.\n */}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This contact could not be loaded. It may have been removed from this site.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n\n return (\n // A create opened from this page — a task, an activity, a deal — files\n // under the site that captured this person unless the reader says\n // otherwise (AGL-2630).\n <CrmCreateSiteDefault hostId={siteHostId}>\n <Stack spacing={3}>\n <CrmRecordHeader\n kind=\"Contact\"\n title={record ? record.name || record.email : undefined}\n // The identity, and the other addresses a merge folded into it.\n subtitle={\n record\n ? [record.email, ...record.alternateEmails.map((email) => `also ${email}`)].join(' · ')\n : undefined\n }\n help={contactDocsHelp}\n backHref={routes.section('contacts')}\n backLabel=\"Back to contacts\"\n actions={\n record ? (\n <>\n <AddToListButton hostId={hostId} org={org} contactId={id} email={record.email} />\n {/* Dial the number, and log the call it was (AGL-2661). */}\n <CrmCallButton\n hostId={siteHostId}\n org={org}\n link={{ contactId: id }}\n // The main number, or the mobile when that is all there is (AGL-3515).\n phone={record.phone || record.mobilePhone}\n doNotCall={record.doNotCall}\n suiteLocked={!suiteIncluded}\n />\n {/* An answer in a thread another plugin keeps with them (AGL-4020), ahead of a new message. */}\n <CrmRecordEmailActionsZone\n hostId={siteHostId}\n org={org}\n kind=\"contact\"\n recordId={id}\n name={record.name}\n email={record.email || null}\n />\n <CrmSendEmailButton\n hostId={siteHostId}\n org={org}\n contactId={id}\n email={record.email}\n name={record.name}\n emailState={record.emailState}\n suiteLocked={!suiteIncluded}\n />\n </>\n ) : null\n }\n menuItems={[...overflowItems, ...erase.menuItems]}\n // The booking door (AGL-2660): this site's services, or at the\n // organization level the capturing site's.\n booking={record ? { hostId: siteHostId, org, kind: 'contact', recordId: id } : undefined}\n loading={!record}\n chips={\n record ? (\n <>\n {/* The verdict on the address (AGL-3245), first: a bounce or a\n do-not-contact mark is what a person deciding to write must see. */}\n <CrmEmailStateChip state={record.emailState} />\n {/* Seen here only through a share (AGL-3336): who shared it. */}\n <CrmShareChipView chip={crmShareChipFor(row, viewingGroup?.hostIds ?? [])} org={org} />\n {/* The mail gateway in front of the address (AGL-3328). */}\n <CrmEmailGatewayChip\n hostId={siteHostId}\n email={record.email}\n emailState={record.emailState}\n enabled={suiteIncluded}\n />\n <CrmAccountLockChip\n hostId={siteHostId}\n email={record.email}\n enabled={suiteIncluded}\n />\n <CrmRecordChip\n label=\"Stage\"\n value={\n record.lifecycleStage\n ? CONTACT_LIFECYCLE_STAGE_LABELS[record.lifecycleStage]\n : undefined\n }\n />\n <CrmRecordChip\n label=\"Owner\"\n value={record.ownerUid ? members.memberName(record.ownerUid) : undefined}\n />\n {/*\n The last time they opened or clicked one of this site's\n campaigns (AGL-2616) — the relationship's pulse, beside the\n owner who keeps it.\n */}\n <CrmRecordChip\n label=\"Last engaged\"\n value={\n record.lastEmailEngagementAtMs ? (\n <Tooltip title={new Date(record.lastEmailEngagementAtMs).toLocaleString()}>\n <span>\n {Aglyn.activityTimeLabel(record.lastEmailEngagementAtMs, Date.now())}\n </span>\n </Tooltip>\n ) : undefined\n }\n />\n <CrmRecordChip\n label=\"Orders\"\n value={\n record.ordersCount > 0 ? (\n ordersHref ? (\n <AppLink href={ordersHref} color=\"inherit\" underline=\"hover\">\n {`${record.ordersCount.toLocaleString()} · $${(record.ltvCents / 100).toFixed(2)} lifetime`}\n </AppLink>\n ) : (\n `${record.ordersCount.toLocaleString()} · $${(record.ltvCents / 100).toFixed(2)} lifetime`\n )\n ) : undefined\n }\n />\n </>\n ) : null\n }\n >\n {erase.banner}\n </CrmRecordHeader>\n {record && row && scope ? (\n <>\n {suiteIncluded ? null : (\n <CrmSuiteNotice>\n {'Working this contact — the owner, stage and company, tasks, ' +\n 'deals, files and custom fields, logging a call or an activity, ' +\n 'one-to-one email and merging duplicates — is part of the CRM.'}\n </CrmSuiteNotice>\n )}\n {/*\n The cross-site fact, at the organization level only (AGL-2630):\n which sites know this person, and their consent for each. Under\n a site the page IS one site's view and the card would say so on\n every record.\n */}\n {/* What an assistant proposes about the person (AGL-2917), where the plan works a contact. */}\n {suiteIncluded ? (\n <CrmRecordInsightsZone\n hostId={hostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"contact\"\n recordId={id}\n name={record.name || record.email}\n taskLink={{ contactId: id }}\n />\n ) : null}\n {mount ? <ContactKnownByCard row={row} contactId={id} org={org} /> : null}\n {/* Where the person is visible and why, and a manager's share (AGL-3336). */}\n <RecordSharingCard\n object=\"contacts\"\n id={id}\n record={row}\n hostId={hostId ?? null}\n orgId={orgId}\n org={org}\n viewingHostIds={viewingGroup?.hostIds}\n />\n <ContactPropertiesCard\n hostId={hostId}\n org={org}\n record={record}\n orgId={orgId}\n consentGroup={consentGroup}\n seed={{ status, fromCache }}\n members={members}\n suiteLocked={!suiteIncluded}\n />\n <ContactAssociationsCard\n hostId={siteHostId}\n record={record}\n row={row}\n consentGroup={consentGroup}\n seed={{ status, fromCache }}\n basePath={basePath}\n bookingsHref={bookingsHref}\n />\n <ContactTimelineCard\n hostId={hostId}\n org={org}\n contactId={id}\n contact={row}\n campaignHref={campaignHref}\n suiteLocked={!suiteIncluded}\n />\n {/*\n On a plan without the CRM, the cards that only work a record —\n deals, tasks, files — are named in the notice above rather than\n drawn as upsells in a row.\n */}\n {suiteIncluded ? (\n <>\n <ContactDealsCard\n hostId={hostId}\n org={org}\n basePath={basePath}\n contactId={id}\n contactName={record.name || record.email}\n />\n <RecordTasksCard hostId={hostId} org={org} basePath={basePath} contactId={id} />\n {/*\n The files are the VIEWING holder's, like everything else on this\n page: the write is a facet path, so a contract one brand filed\n never appears on a sibling brand's copy of the person.\n */}\n <RecordFilesCard\n scope={scope}\n collection=\"contacts\"\n recordId={id}\n hostId={hostId}\n facetGroupId={consentGroup.groupId}\n mediaIds={record.mediaIds}\n topic=\"contactRecord\"\n />\n </>\n ) : null}\n <ContactDuplicatesCard\n current={{ id, doc: row }}\n scope={scope}\n consentGroup={consentGroup}\n visibleTo={visibleTo}\n basePath={basePath}\n onMerge={(candidate) => merge.open({ other: candidate, keep: 'current' })}\n suiteLocked={!suiteIncluded}\n />\n <ContactMergeDialog\n {...merge.state}\n onClose={merge.close}\n hostId={siteHostId}\n current={{ id, doc: row }}\n scope={scope}\n consentGroup={consentGroup}\n visibleTo={visibleTo}\n memberName={members.memberName}\n // The page's own record was folded away: its address now opens\n // the survivor, which is where the reader is taken.\n onMerged={(survivorId) => {\n if (survivorId !== id) router.replace(routes.contact(survivorId))\n }}\n />\n </>\n ) : null}\n {erase.dialog}\n </Stack>\n </CrmCreateSiteDefault>\n )\n}\nContactDetailPage.displayName = 'ContactDetailPage'\n\nexport default ContactDetailPage\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","pluginDocsHelp","pluginRecordByEmailHref","pluginRecordHref","mdiDeleteOutline","mdiMerge","AppLink","MdiIcon","useConfirmationContext","useSnackbar","useFirestore","useFirestoreDoc","Stack","Tooltip","Typography","doc","useParams","useRouter","useCallback","useMemo","useContactRemove","useCrmActivityLogger","CrmCreateSiteDefault","useCrmOrgMount","crmShareChipFor","useCrmScope","contactPrimaryGroup","contactRecordFromDoc","crmRoutes","ContactAssociationsCard","ContactDuplicatesCard","ContactKnownByCard","ContactMergeDialog","useContactMergeDialog","ContactPropertiesCard","ContactTimelineCard","AddToListButton","useBookingDoor","CrmRecordEmailActionsZone","CrmSendEmailButton","ContactDealsCard","CrmCallButton","CrmEmailStateChip","CrmAccountLockChip","CrmEmailGatewayChip","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","CrmSuiteNotice","crmSuiteIncluded","crmSuiteLockedReason","useErasePersonAction","RecordFilesCard","CrmShareChipView","RecordSharingCard","RecordTasksCard","useSiteRouteContext","useOrgMembers","contactDocsHelp","anchor","ContactDetailPage","props","hostId","org","id","basePath","routes","router","firestore","enqueueSnackbar","confirm","scope","orgId","consentGroup","viewingGroup","visibleTo","mount","merge","suiteIncluded","data","row","status","fromCache","idField","siteHostId","logActivity","record","notFound","Boolean","siteLabel","siteName","members","enabled","params","ordersHref","orgSlug","host","email","String","bookingDoor","siteSlug","siteSubdomain","bookingsHref","open","siteRoute","campaignHref","campaignId","contactRemove","handleRemove","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","removed","removeOne","type","name","variant","persist","push","section","error","console","allowDuplicate","overflowItems","key","label","icon","path","size","disabled","disabledReason","onClick","destructive","erase","subject","kind","requestedAtMs","readErasureRequestedAtMs","undefined","help","backHref","backLabel","spacing","subtitle","alternateEmails","map","join","actions","contactId","link","phone","mobilePhone","doNotCall","suiteLocked","recordId","emailState","menuItems","booking","loading","chips","state","chip","hostIds","value","lifecycleStage","ownerUid","memberName","lastEmailEngagementAtMs","Date","toLocaleString","span","activityTimeLabel","now","ordersCount","href","underline","ltvCents","toFixed","banner","taskLink","object","viewingHostIds","seed","contact","contactName","collection","facetGroupId","groupId","mediaIds","topic","current","onMerge","candidate","other","keep","onClose","close","onMerged","survivorId","replace","dialog","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Another way to write to the person, drawn before **Send email** by
|
|
19
|
+
* whichever plugin keeps a conversation with them (AGL-4020).
|
|
20
|
+
*
|
|
21
|
+
* The CRM's one-to-one composer starts a NEW message from the site's sending
|
|
22
|
+
* address. A person who answered a sequence email is in a thread another
|
|
23
|
+
* plugin keeps, in a member's own mailbox, and the answer belongs in that
|
|
24
|
+
* thread from that mailbox — which only that plugin can send. So the record
|
|
25
|
+
* header hosts this zone ahead of its own button and hands it the person; a
|
|
26
|
+
* plugin with a thread to answer draws its control here, and where none has
|
|
27
|
+
* one the header shows Send email alone.
|
|
28
|
+
*/
|
|
29
|
+
export type CrmEmailActionsRecordKind = 'contact' | 'lead';
|
|
30
|
+
export interface CrmRecordEmailActionsZoneProps {
|
|
31
|
+
/** The site the page is under, or the record's capturing site; `null` at the organization level for a record no site captured. */
|
|
32
|
+
hostId: string | null;
|
|
33
|
+
/** The organization the record belongs to; `undefined` while it resolves. */
|
|
34
|
+
orgId: string | undefined;
|
|
35
|
+
kind: CrmEmailActionsRecordKind;
|
|
36
|
+
recordId: string;
|
|
37
|
+
/** The record's name as the page shows it. */
|
|
38
|
+
name: string;
|
|
39
|
+
/** The record's address, `null` for one that has none. */
|
|
40
|
+
email: string | null;
|
|
41
|
+
}
|
|
42
|
+
export declare const CRM_RECORD_EMAIL_ACTIONS_ZONE: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<CrmRecordEmailActionsZoneProps>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { definePluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
|
|
17
|
+
export const CRM_RECORD_EMAIL_ACTIONS_ZONE = definePluginZone('crmRecordEmailActions');
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=crm-email-actions-zone-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-email-actions-zone-token.ts"],"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\nimport { definePluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\n\n/**\n * Another way to write to the person, drawn before **Send email** by\n * whichever plugin keeps a conversation with them (AGL-4020).\n *\n * The CRM's one-to-one composer starts a NEW message from the site's sending\n * address. A person who answered a sequence email is in a thread another\n * plugin keeps, in a member's own mailbox, and the answer belongs in that\n * thread from that mailbox — which only that plugin can send. So the record\n * header hosts this zone ahead of its own button and hands it the person; a\n * plugin with a thread to answer draws its control here, and where none has\n * one the header shows Send email alone.\n */\nexport type CrmEmailActionsRecordKind = 'contact' | 'lead'\n\nexport interface CrmRecordEmailActionsZoneProps {\n /** The site the page is under, or the record's capturing site; `null` at the organization level for a record no site captured. */\n hostId: string | null\n /** The organization the record belongs to; `undefined` while it resolves. */\n orgId: string | undefined\n kind: CrmEmailActionsRecordKind\n recordId: string\n /** The record's name as the page shows it. */\n name: string\n /** The record's address, `null` for one that has none. */\n email: string | null\n}\n\nexport const CRM_RECORD_EMAIL_ACTIONS_ZONE =\n definePluginZone<CrmRecordEmailActionsZoneProps>('crmRecordEmailActions')\n"],"names":["definePluginZone","CRM_RECORD_EMAIL_ACTIONS_ZONE"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,2CAA0C;AA6B3E,OAAO,MAAMC,gCACXD,iBAAiD,yBAAwB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type CrmOrgDoc } from '../hooks/use-crm-scope';
|
|
18
|
+
import { type CrmRecordEmailActionsZoneProps } from './crm-email-actions-zone-token';
|
|
19
|
+
export { CRM_RECORD_EMAIL_ACTIONS_ZONE, type CrmEmailActionsRecordKind, type CrmRecordEmailActionsZoneProps, } from './crm-email-actions-zone-token';
|
|
20
|
+
type CrmRecordEmailActionsZoneInput = Omit<CrmRecordEmailActionsZoneProps, 'orgId'> & {
|
|
21
|
+
org?: CrmOrgDoc;
|
|
22
|
+
};
|
|
23
|
+
/** One control among the header's own: the zone is `bare`, so no wrapper. */
|
|
24
|
+
export declare function CrmRecordEmailActionsZone(props: CrmRecordEmailActionsZoneInput): import("react").JSX.Element;
|
|
25
|
+
export declare namespace CrmRecordEmailActionsZone {
|
|
26
|
+
var displayName: string;
|
|
27
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
19
|
+
import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
|
|
20
|
+
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
21
|
+
import { CRM_RECORD_EMAIL_ACTIONS_ZONE } from "./crm-email-actions-zone-token.js";
|
|
22
|
+
/*
|
|
23
|
+
* The `crmRecordEmailActions` zone a contact's and a lead's header host
|
|
24
|
+
* before Send email (AGL-4020). The token and its props live in
|
|
25
|
+
* `./crm-email-actions-zone-token`, which the plugin's register module
|
|
26
|
+
* declares without loading this component.
|
|
27
|
+
*/ export { CRM_RECORD_EMAIL_ACTIONS_ZONE } from "./crm-email-actions-zone-token.js";
|
|
28
|
+
/** One control among the header's own: the zone is `bare`, so no wrapper. */ export function CrmRecordEmailActionsZone(props) {
|
|
29
|
+
const Slot = useConsoleWidgetSlot();
|
|
30
|
+
// Outside the console shell, or for a record with no address, there is
|
|
31
|
+
// nothing to draw and nothing below is read.
|
|
32
|
+
return Slot && props.email ? /*#__PURE__*/ _jsx(HostedEmailActionsZone, _extends({}, props, {
|
|
33
|
+
Slot: Slot
|
|
34
|
+
})) : null;
|
|
35
|
+
}
|
|
36
|
+
CrmRecordEmailActionsZone.displayName = 'CrmRecordEmailActionsZone';
|
|
37
|
+
function HostedEmailActionsZone(props) {
|
|
38
|
+
const { Slot } = props;
|
|
39
|
+
const { orgId } = useCrmScope({
|
|
40
|
+
hostId: props.hostId,
|
|
41
|
+
org: props.org
|
|
42
|
+
});
|
|
43
|
+
const zone = {
|
|
44
|
+
hostId: props.hostId,
|
|
45
|
+
orgId: orgId != null ? orgId : undefined,
|
|
46
|
+
kind: props.kind,
|
|
47
|
+
recordId: props.recordId,
|
|
48
|
+
name: props.name,
|
|
49
|
+
email: props.email
|
|
50
|
+
};
|
|
51
|
+
return /*#__PURE__*/ _jsx(Slot, _extends({
|
|
52
|
+
slot: CRM_RECORD_EMAIL_ACTIONS_ZONE.id
|
|
53
|
+
}, zone));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=crm-email-actions-zone.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-email-actions-zone.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport {\n type ConsoleWidgetSlotRenderer,\n useConsoleWidgetSlot,\n} from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { useCrmScope, type CrmOrgDoc } from '../hooks/use-crm-scope'\nimport {\n CRM_RECORD_EMAIL_ACTIONS_ZONE,\n type CrmRecordEmailActionsZoneProps,\n} from './crm-email-actions-zone-token'\n\n/*\n * The `crmRecordEmailActions` zone a contact's and a lead's header host\n * before Send email (AGL-4020). The token and its props live in\n * `./crm-email-actions-zone-token`, which the plugin's register module\n * declares without loading this component.\n */\nexport {\n CRM_RECORD_EMAIL_ACTIONS_ZONE,\n type CrmEmailActionsRecordKind,\n type CrmRecordEmailActionsZoneProps,\n} from './crm-email-actions-zone-token'\n\ntype CrmRecordEmailActionsZoneInput = Omit<CrmRecordEmailActionsZoneProps, 'orgId'> & { org?: CrmOrgDoc }\n\n/** One control among the header's own: the zone is `bare`, so no wrapper. */\nexport function CrmRecordEmailActionsZone(props: CrmRecordEmailActionsZoneInput) {\n const Slot = useConsoleWidgetSlot()\n // Outside the console shell, or for a record with no address, there is\n // nothing to draw and nothing below is read.\n return Slot && props.email ? <HostedEmailActionsZone {...props} Slot={Slot} /> : null\n}\nCrmRecordEmailActionsZone.displayName = 'CrmRecordEmailActionsZone'\n\nfunction HostedEmailActionsZone(\n props: CrmRecordEmailActionsZoneInput & { Slot: ConsoleWidgetSlotRenderer },\n) {\n const { Slot } = props\n const { orgId } = useCrmScope({ hostId: props.hostId, org: props.org })\n const zone: CrmRecordEmailActionsZoneProps = {\n hostId: props.hostId,\n orgId: orgId ?? undefined,\n kind: props.kind,\n recordId: props.recordId,\n name: props.name,\n email: props.email,\n }\n return <Slot slot={CRM_RECORD_EMAIL_ACTIONS_ZONE.id} {...zone} />\n}\n"],"names":["useConsoleWidgetSlot","useCrmScope","CRM_RECORD_EMAIL_ACTIONS_ZONE","CrmRecordEmailActionsZone","props","Slot","email","HostedEmailActionsZone","displayName","orgId","hostId","org","zone","undefined","kind","recordId","name","slot","id"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;AAEA,SAEEA,oBAAoB,QACf,qDAAoD;AAC3D,SAASC,WAAW,QAAwB,4BAAwB;AACpE,SACEC,6BAA6B,QAExB,oCAAgC;AAEvC;;;;;CAKC,GACD,SACEA,6BAA6B,QAGxB,oCAAgC;AAIvC,2EAA2E,GAC3E,OAAO,SAASC,0BAA0BC,KAAqC;IAC7E,MAAMC,OAAOL;IACb,uEAAuE;IACvE,6CAA6C;IAC7C,OAAOK,QAAQD,MAAME,KAAK,iBAAG,KAACC,qCAA2BH;QAAOC,MAAMA;UAAW;AACnF;AACAF,0BAA0BK,WAAW,GAAG;AAExC,SAASD,uBACPH,KAA2E;IAE3E,MAAM,EAAEC,IAAI,EAAE,GAAGD;IACjB,MAAM,EAAEK,KAAK,EAAE,GAAGR,YAAY;QAAES,QAAQN,MAAMM,MAAM;QAAEC,KAAKP,MAAMO,GAAG;IAAC;IACrE,MAAMC,OAAuC;QAC3CF,QAAQN,MAAMM,MAAM;QACpBD,KAAK,EAAEA,gBAAAA,QAASI;QAChBC,MAAMV,MAAMU,IAAI;QAChBC,UAAUX,MAAMW,QAAQ;QACxBC,MAAMZ,MAAMY,IAAI;QAChBV,OAAOF,MAAME,KAAK;IACpB;IACA,qBAAO,KAACD;QAAKY,MAAMf,8BAA8BgB,EAAE;OAAMN;AAC3D"}
|
|
@@ -36,6 +36,7 @@ import { CrmCallButton, CrmPhoneLink } from "./crm-call-actions.js";
|
|
|
36
36
|
import { CrmEmailStateChip } from "./crm-email-state-chip.js";
|
|
37
37
|
import { CrmAccountLockChip, CrmEmailGatewayChip } from "./crm-email-check.js";
|
|
38
38
|
import { CrmRecordChip, CrmRecordHeader } from "./crm-record-header.js";
|
|
39
|
+
import { CrmRecordEmailActionsZone } from "./crm-email-actions-zone.js";
|
|
39
40
|
import { CrmSendEmailButton } from "./crm-send-email-button.js";
|
|
40
41
|
import { LeadOwnerSelect } from "./lead-owner-select.js";
|
|
41
42
|
import { LeadStatusChip } from "./lead-status-chip.js";
|
|
@@ -111,7 +112,7 @@ const NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX;
|
|
|
111
112
|
* conversion, and the header's actions become links to what the conversion
|
|
112
113
|
* made.
|
|
113
114
|
*/ export function LeadPropertiesCard(props) {
|
|
114
|
-
var _lead_phone, _lead_mobilePhone, _lead_notes, _lead_email,
|
|
115
|
+
var _lead_phone, _lead_mobilePhone, _lead_notes, _lead_email, _lead_name, _lead_email1, _lead_email2, _lead_name1, _lead_email3, _lead_email4;
|
|
115
116
|
const { hostId, consentGroup, orgId, leadId, lead, leadStatus, fromCache, basePath, roster, onConvert, onUnqualify, extraMenuItems = [], banner, extraChips, erasurePending = false, org } = props;
|
|
116
117
|
const firestore = useFirestore();
|
|
117
118
|
const { enqueueSnackbar } = useSnackbar();
|
|
@@ -395,11 +396,19 @@ const NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX;
|
|
|
395
396
|
phone: leadPhone || leadMobile,
|
|
396
397
|
doNotCall: doNotCall
|
|
397
398
|
}),
|
|
399
|
+
/*#__PURE__*/ _jsx(CrmRecordEmailActionsZone, {
|
|
400
|
+
hostId: hostId,
|
|
401
|
+
org: org,
|
|
402
|
+
kind: "lead",
|
|
403
|
+
recordId: leadId,
|
|
404
|
+
name: String((_lead_name = lead['name']) != null ? _lead_name : ''),
|
|
405
|
+
email: String((_lead_email1 = lead['email']) != null ? _lead_email1 : '') || null
|
|
406
|
+
}),
|
|
398
407
|
/*#__PURE__*/ _jsx(CrmSendEmailButton, {
|
|
399
408
|
hostId: hostId,
|
|
400
409
|
leadId: leadId,
|
|
401
|
-
email: String((
|
|
402
|
-
name: String((
|
|
410
|
+
email: String((_lead_email2 = lead['email']) != null ? _lead_email2 : ''),
|
|
411
|
+
name: String((_lead_name1 = lead['name']) != null ? _lead_name1 : ''),
|
|
403
412
|
emailState: emailState
|
|
404
413
|
})
|
|
405
414
|
]
|
|
@@ -430,12 +439,12 @@ const NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX;
|
|
|
430
439
|
}),
|
|
431
440
|
/*#__PURE__*/ _jsx(CrmEmailGatewayChip, {
|
|
432
441
|
hostId: hostId,
|
|
433
|
-
email: String((
|
|
442
|
+
email: String((_lead_email3 = lead['email']) != null ? _lead_email3 : ''),
|
|
434
443
|
emailState: emailState
|
|
435
444
|
}),
|
|
436
445
|
/*#__PURE__*/ _jsx(CrmAccountLockChip, {
|
|
437
446
|
hostId: hostId,
|
|
438
|
-
email: String((
|
|
447
|
+
email: String((_lead_email4 = lead['email']) != null ? _lead_email4 : '')
|
|
439
448
|
}),
|
|
440
449
|
/*#__PURE__*/ _jsx(CrmRecordChip, {
|
|
441
450
|
label: "Owner",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-properties-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type {\n AglynOrgBilling,\n ConsentGroup,\n CrmLeadFields,\n CrmLeadProfilePatch,\n} from '@aglyn/aglyn'\nimport { mdiAccountCancelOutline } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n FormControl,\n InputLabel,\n Select,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { deleteField, doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useId, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\nimport { useLeadStatusPicklist } from '../hooks/use-lead-status-picklist'\nimport { useLeadPicklists } from '../hooks/use-lead-picklists'\nimport {\n crmCustomDraftChanges,\n type CrmCustomDraft,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n LeadProfileFields,\n type LeadProfileDraft,\n leadProfileDraftBody,\n leadProfileDraftFrom,\n} from './lead-profile-fields'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmAccountLockChip, CrmEmailGatewayChip } from './crm-email-check'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { LeadStatusChip } from './lead-status-chip'\nimport { leadStatusChoices, leadStatusMenuItems } from './lead-status-options'\nimport {\n crmClientListFields,\n CRM_CLIENT_SEARCH_FIELDS,\n CRM_CLIENT_SORT_FIELDS,\n} from '../model/crm-list-query'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\n/** The Leads list's fields a profile save rewrites; the verdict key is the server's. */\nconst LEAD_CLIENT_LIST_FIELDS = [\n ...CRM_CLIENT_SEARCH_FIELDS,\n // The Lead, Company and Title headers' sort keys (AGL-3680).\n ...CRM_CLIENT_SORT_FIELDS,\n 'leadSourceKey',\n // The lead source's group, read off the org's list (AGL-3577).\n 'leadSourceDirection',\n // Industry and Rating (AGL-3513).\n 'industryKey',\n 'ratingKey',\n] as const\n\nconst NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX\n\n/** The patch as the document takes it: a cleared field is deleted, not blanked. */\nfunction profileWrite(patch: CrmLeadProfilePatch): Record<string, unknown> {\n const write: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n write[key] = value === null ? deleteField() : value\n }\n return write\n}\n\n/**\n * Why Convert is refused while an erasure waits on the person — the same\n * sentence shape the overflow's items carry, so the two read as one state.\n */\nexport const CONVERT_PENDING_ERASURE_REASON = 'An erasure is pending for this person'\n\n/** A label over a value — the record page's one row shape. */\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\n/**\n * The help on a lead's header card — one card, so the page's loading and\n * not-found branches carry the same link as the loaded one.\n */\nexport const LEAD_PAGE_HELP = Aglyn.pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })\n\nexport interface LeadPropertiesCardProps {\n /**\n * The site this lead is acted on as — the mounted one, or at the\n * organization level the lead's own first capturing site (AGL-3278).\n * `null` for a lead no site has captured: the booking door, the call and\n * the email close, because each of them is one site's to offer.\n */\n hostId: string | null\n /**\n * The group the consent basis is read against (AGL-3278): the site's own\n * declared group, resolved by the page. Not `soloConsentGroup(hostId)`,\n * which is what this card used to spell and which cannot see a refusal\n * recorded against a sibling brand — three sites presenting as one sender\n * are one sender to the person unsubscribing from them.\n */\n consentGroup: ConsentGroup\n /**\n * The org the site belongs to, as the page already resolved it — the\n * custom lead fields are ORG-wide (AGL-3272), and a second lookup here\n * would be one more read per record page for an answer already in hand.\n */\n orgId: string | null\n leadId: string\n lead: Record<string, unknown> & CrmLeadFields\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n basePath: string\n roster: OrgMemberOptions\n onConvert: () => void\n /** Opens the Unqualify dialog — with the Unqualified value picked, when the status select named one. */\n onUnqualify: (statusLabel?: string) => void\n /**\n * Items the page adds to the overflow beside Unqualify — the privacy\n * erasure (AGL-2623) lives on the page, because it needs the workspace\n * role and the API, and this card owns the header it must appear in.\n */\n extraMenuItems?: RowActionsMenuItem[]\n /** What the page shows above the facts — the erasure-pending state. */\n banner?: React.ReactNode\n /**\n * Chips the page adds after the owner — the campaigns the lead is filed\n * under (AGL-3274), named from the containers the page reads once for\n * this header and the Campaigns card below it.\n */\n extraChips?: React.ReactNode\n /**\n * An erasure request is waiting on this person (AGL-2623). Convert stays\n * on the page but is refused with the reason, the way the overflow's items\n * are: a conversion filed now would reach the capture door only to be\n * refused there, and the lead itself goes when the request runs.\n */\n erasurePending?: boolean\n /**\n * The org the shell passed: the booking door reads whether the lead's site\n * runs Bookings and whether the plan is entitled to it (AGL-2660), and a\n * logged call reads the activity scope it belongs in (AGL-2661).\n */\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * What the team knows and decides about a lead: status, owner, notes, and\n * the identity and consent the capture recorded (AGL-2608).\n *\n * Two cards. The record's header carries the identity, the status and owner,\n * the consent the capture recorded and the record's actions. **Details**\n * under it carries the profile, the org's own lead fields under **More\n * fields**, and the notes — one card with one Save in its header, the way\n * Salesforce keeps a record's standard and custom fields in one Details\n * section.\n *\n * Status and owner are single-field client writes — the rules let a site\n * admin, editor or author update `hosts/{hostId}/leads`, and a one-field\n * `update` cannot roll anything else back. The details are drafts seeded\n * from the document, so their save goes through `writeGuardedBySeed`: a\n * draft edited over a cached read would otherwise overwrite a newer note or\n * profile with an older one.\n *\n * Converted leads are read-only here, but for the notes. Their status is the\n * conversion, and the header's actions become links to what the conversion\n * made.\n */\nexport function LeadPropertiesCard(props: LeadPropertiesCardProps) {\n const {\n hostId,\n consentGroup,\n orgId,\n leadId,\n lead,\n leadStatus,\n fromCache,\n basePath,\n roster,\n onConvert,\n onUnqualify,\n extraMenuItems = [],\n banner,\n extraChips,\n erasurePending = false,\n org,\n } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const routes = crmRoutes(basePath)\n // A client-direct write owes the org's sharing rules a re-evaluation (AGL-3336).\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n /*\n * Built at USE, not at render (AGL-3275). A lead is an org row, so the org\n * has to be known to address one — and `orgId` is null while the lookup\n * settles. Composing a ref from that null throws during render, which the\n * page shows as a 500 rather than as a card still loading.\n */\n const refFor = () => (orgId ? doc(firestore, 'orgs', orgId, 'leads', leadId) : null)\n const status = Aglyn.crmLeadStatus(lead)\n const converted = Boolean(lead.convertedContactId)\n const open = Aglyn.isCrmLeadOpen(lead) && !converted\n // The last verdict on the address (AGL-3245), as the platform stamped it.\n const emailState = Aglyn.readEmailState(lead)\n /** What a capture that took a number left on the document (AGL-2661). */\n const leadPhone = String(lead['phone'] ?? '').trim()\n const leadMobile = String(lead.mobilePhone ?? '').trim()\n // The person asked not to be phoned (AGL-3513): a hint on every dial control.\n const doNotCall = lead.doNotCall === true\n\n const [notes, setNotes] = useState(String(lead.notes ?? ''))\n // The label's id, so the status combobox is named \"Status\" rather than\n // after the status it shows — see `LeadOwnerSelect`.\n const statusLabelId = useId()\n const [notesDirty, setNotesDirty] = useState(false)\n /*\n * THE LEAD'S OWN PROFILE (AGL-3231, AGL-3513) — the name's parts,\n * company, title, the phones, website, address, tags, lead source,\n * industry, rating, size and revenue — edited in the Details card with\n * the notes and the custom fields, under one Save, the way the contact's\n * Properties card saves. Seeded from the document and guarded on save: a\n * draft edited over a cached read must not overwrite a newer profile\n * with an older one.\n */\n const [profile, setProfile] = useState<LeadProfileDraft>(() => leadProfileDraftFrom(lead))\n const [profileDirty, setProfileDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n const [profileErrors, setProfileErrors] = useState<Record<string, string>>({})\n // The stored profile as one value, so a change to any of its fields reseeds.\n const profileSeed = JSON.stringify(Aglyn.CRM_LEAD_PROFILE_KEYS.map((key) => lead[key] ?? null))\n useEffect(() => {\n if (!profileDirty) setProfile(leadProfileDraftFrom(lead))\n // The draft follows the document until it is edited.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [profileDirty, profileSeed])\n /*\n * THE ORG'S OWN LEAD FIELDS (AGL-3272), edited under the same Save as\n * the profile: one button over one card, so a person filling a lead in\n * does not have to find two.\n *\n * The draft holds only the keys the reader touched, and Save writes the\n * difference as dotted paths — a `custom` map written whole would take\n * out every key this card did not show, which is what a retired field's\n * values and an integration's writes sit under.\n */\n const fields = useContactFieldDefinitions(orgId, 'lead')\n // The org's lead source values (AGL-3298), for the select below.\n const leadSources = useLeadSourcePicklist(orgId)\n // The org's lead status values (AGL-3512): the chip's words and the select's choices.\n const leadStatuses = useLeadStatusPicklist(orgId)\n // The org's Salutation, Industry and Rating lists (AGL-3513).\n const leadPicklists = useLeadPicklists(orgId)\n const statusChoices = useMemo(\n () => leadStatusChoices(leadStatuses.picklist, lead),\n [leadStatuses.picklist, lead],\n )\n const storedCustom = useMemo(() => lead.custom ?? {}, [lead.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n const editProfile = <K extends keyof LeadProfileDraft>(key: K, value: LeadProfileDraft[K]) => {\n setProfile((current) => ({ ...current, [key]: value }))\n setProfileDirty(true)\n }\n // A newer note from the server replaces an UNEDITED draft; an edited one is\n // the reader's, and the guard on save decides whether it may land.\n useEffect(() => {\n if (!notesDirty) setNotes(String(lead.notes ?? ''))\n }, [lead.notes, notesDirty])\n\n /** One update of the lead, and whether it landed. */\n const write = async (fields: Record<string, unknown>, done: string): Promise<boolean> => {\n try {\n const ref = refFor()\n if (!ref) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return false\n }\n await updateDoc(ref, { ...fields, updatedAt: serverTimestamp() })\n followUpSharing('leads', [leadId])\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n return false\n }\n }\n\n /*\n * What Save is offered for: a profile field edited, a custom value that\n * differs from the stored map, or the notes. Touched-and-put-back does\n * not count for a custom value — the draft keeps the key either way, and\n * a Save that wrote nothing would still bump `updatedAt`.\n */\n const customChanged = crmCustomDraftChanges(storedCustom, custom).length > 0\n const dirty = profileDirty || customChanged || notesDirty\n\n /** Every draft back to what the document holds. */\n const discard = () => {\n setProfile(leadProfileDraftFrom(lead))\n setProfileDirty(false)\n setProfileErrors({})\n setCustom({})\n setNotes(String(lead.notes ?? ''))\n setNotesDirty(false)\n }\n\n /*\n * The Details card's one Save: the profile and the custom values while\n * the lead is open, and the notes, in one update. A converted lead's\n * profile is the contact's now, so only its notes are written.\n */\n const saveDetails = async () => {\n const update: Record<string, unknown> = {}\n if (!converted && (profileDirty || customChanged)) {\n const { body, revenueError } = leadProfileDraftBody(profile)\n const { patch, errors } = Aglyn.normalizeCrmLeadProfile(body)\n const shown: Record<string, string> = { ...errors }\n if (revenueError) shown['annualRevenueCents'] = revenueError\n setProfileErrors(shown)\n if (Object.keys(shown).length) return\n /*\n * A required field the reader CLEARED is refused; one the lead has\n * always lacked is not this save's to demand, or a field added after\n * the lead was captured would block every later edit to its company.\n */\n const missing = crmCustomDraftMissingRequired(\n fields.active,\n storedCustom,\n custom,\n 'edit',\n )\n if (missing.length) {\n enqueueSnackbar(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`, {\n variant: 'warning',\n })\n return\n }\n const profileFields = profileWrite(patch)\n // While a first or last name is set, the name is theirs (AGL-3513).\n const composed = Aglyn.crmLeadComposedName(lead, patch)\n if (composed && composed !== lead['name']) profileFields['name'] = composed\n Object.assign(\n update,\n profileFields,\n crmCustomDraftWrites(storedCustom, custom),\n // The company, title, tags and lead source are what the Leads list\n // searches and filters by (AGL-3321).\n crmClientListFields(\n 'leads',\n lead,\n profileFields,\n LEAD_CLIENT_LIST_FIELDS,\n // A direction only from the org's list as the server answered it.\n leadSources.ready ? { leadSources: leadSources.picklist } : {},\n ),\n )\n }\n if (notesDirty) update['notes'] = notes.trim().slice(0, NOTES_MAX)\n if (!Object.keys(update).length) return\n setSaving(true)\n let landed = false\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n landed = await write(update, 'Lead saved')\n },\n )\n setSaving(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The lead could not be saved.', {\n variant: 'warning',\n })\n return\n }\n if (!landed) return\n setProfileDirty(false)\n setNotesDirty(false)\n // The draft is spent: what it held is stored, and the controls read\n // the document again.\n setCustom({})\n }\n\n const consent = Aglyn.readMarketingBasis(lead, consentGroup)\n const consentLine =\n consent.basis === 'granted'\n ? `Opted in to marketing${\n consent.basisAtMs ? ` on ${new Date(consent.basisAtMs).toLocaleDateString()}` : ''\n }`\n : consent.basis === 'declined'\n ? 'Declined marketing'\n : 'No marketing consent recorded — this lead cannot be emailed marketing'\n\n return (\n <>\n <CrmRecordHeader\n kind=\"Lead\"\n title={String(lead['name'] || lead['email'] || leadId)}\n // The name is the heading; the address is the one line under it,\n // unless the address IS the name, in which case there is no second fact.\n subtitle={lead['name'] ? String(lead['email'] ?? '') : undefined}\n help={LEAD_PAGE_HELP}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n // The booking door (AGL-2660), while the lead is still the record\n // being worked: once converted, the contact is where a meeting is\n // booked from, and the links below lead there.\n booking={converted ? undefined : { hostId, org, kind: 'lead', recordId: leadId }}\n actions={\n <>\n {/*\n Once converted, the lead's actions are the records the\n conversion made.\n */}\n {converted ? (\n <>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.contact(String(lead.convertedContactId))}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open contact'}\n </Button>\n {lead.companyId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.company(lead.companyId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open company'}\n </Button>\n ) : null}\n {lead.dealId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.deal(lead.dealId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open deal'}\n </Button>\n ) : null}\n </>\n ) : erasurePending ? (\n <Tooltip title={CONVERT_PENDING_ERASURE_REASON}>\n {/* A disabled button receives no pointer events, so the\n tooltip anchors on the span around it. */}\n <span>\n <Button size=\"small\" variant=\"contained\" disabled>\n {'Convert'}\n </Button>\n </span>\n </Tooltip>\n ) : (\n <Button size=\"small\" variant=\"contained\" onClick={onConvert}>\n {'Convert'}\n </Button>\n )}\n {/* Dial the number the capture carried, and log the call (AGL-2661). */}\n <CrmCallButton\n hostId={hostId}\n org={org}\n link={{ leadId }}\n phone={leadPhone || leadMobile}\n doNotCall={doNotCall}\n />\n <CrmSendEmailButton\n hostId={hostId}\n leadId={leadId}\n email={String(lead['email'] ?? '')}\n name={String(lead['name'] ?? '')}\n emailState={emailState}\n />\n </>\n }\n menuItems={[\n ...(open\n ? [\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => onUnqualify(),\n } satisfies RowActionsMenuItem,\n ]\n : []),\n ...extraMenuItems,\n ]}\n chips={\n <>\n <LeadStatusChip lead={lead} statuses={leadStatuses.picklist} />\n {/* The verdict on the address (AGL-3245), beside the status: a\n bounce does not move New or Working, but it is the first\n thing a person deciding whether to write must see. */}\n <CrmEmailStateChip state={emailState} />\n {/* The mail gateway in front of the address, and what it did\n with this site's mail this week (AGL-3328). */}\n <CrmEmailGatewayChip\n hostId={hostId}\n email={String(lead['email'] ?? '')}\n emailState={emailState}\n />\n <CrmAccountLockChip\n hostId={hostId}\n email={String(lead['email'] ?? '')}\n />\n <CrmRecordChip\n label=\"Owner\"\n value={lead.ownerUid ? roster.labelFor(lead.ownerUid) : undefined}\n />\n {extraChips}\n </>\n }\n >\n <Stack spacing={3}>\n {banner}\n {/*\n Only when the capture carried one (AGL-2661): the sign-up door\n writes no phone, and a booking only when its service asks for\n one (AGL-3493), so a row for every lead would often be a\n permanent blank. A form that captures one fills this.\n */}\n {leadPhone ? (\n <Fact label=\"Phone\">\n <CrmPhoneLink phone={leadPhone} doNotCall={doNotCall} />\n </Fact>\n ) : null}\n {leadMobile ? (\n <Fact label=\"Mobile phone\">\n <CrmPhoneLink phone={leadMobile} doNotCall={doNotCall} />\n </Fact>\n ) : null}\n <Fact label=\"Marketing consent\">{consentLine}</Fact>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n {converted ? (\n <Fact label=\"Status\">\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <LeadStatusChip lead={lead} statuses={leadStatuses.picklist} />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {lead.convertedAtMs\n ? `Converted ${new Date(lead.convertedAtMs).toLocaleString()}`\n : 'Converted'}\n </Typography>\n </Stack>\n </Fact>\n ) : (\n <FormControl size=\"small\" sx={{ minWidth: 200 }}>\n <InputLabel id={statusLabelId}>{'Status'}</InputLabel>\n <Select\n labelId={statusLabelId}\n label=\"Status\"\n value={Aglyn.crmLeadStatusLabel(lead, leadStatuses.picklist)}\n onChange={(event) => {\n const choice = statusChoices.find((entry) => entry.label === event.target.value)\n if (!choice) return\n if (choice.status === 'unqualified') {\n onUnqualify(choice.label)\n return\n }\n // The meaning and the org's label for it, together\n // (AGL-3512). Reopening drops the reason with the closed\n // state: a lead being worked again is not \"unqualified\n // because …\".\n void write(\n {\n status: choice.status,\n statusLabel: choice.label,\n ...(status === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n },\n 'Status updated',\n )\n }}\n >\n {leadStatusMenuItems(statusChoices)}\n </Select>\n </FormControl>\n )}\n <LeadOwnerSelect\n value={lead.ownerUid}\n roster={roster}\n fullWidth={false}\n onChange={(uid) =>\n void write({ ownerUid: uid || deleteField() }, uid ? 'Owner assigned' : 'Owner cleared')\n }\n />\n </Stack>\n {status === 'unqualified' && lead.unqualifiedReason ? (\n <Alert severity=\"info\">{`Unqualified: ${lead.unqualifiedReason}`}</Alert>\n ) : null}\n </Stack>\n </CrmRecordHeader>\n {/*\n The profile (AGL-3231), the org's own lead fields (AGL-3272) and the\n notes: what Salesforce keeps on a lead and hands to the contact and\n the account on convert. Read-only once converted — the contact is the\n record then, and the header's links lead there — but for the notes.\n */}\n <CardDisplay\n header={'Details'}\n help={Aglyn.pluginDocsHelp('crmLeads', { anchor: '#lead-details' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {dirty ? (\n <Button size=\"small\" disabled={saving} onClick={discard}>\n {'Discard changes'}\n </Button>\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void saveDetails()}\n disabled={!dirty || saving}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <LeadProfileFields\n draft={profile}\n onChange={editProfile}\n errors={profileErrors}\n disabled={converted}\n leadSources={leadSources.picklist}\n lists={leadPicklists.lists}\n stored={lead}\n />\n {/*\n The org's own lead fields (AGL-3272), after the built-in ones\n because they describe the same record and save with it. Absent,\n not an empty box, while the org defines none.\n */}\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'More fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={converted || saving}\n />\n ))}\n </>\n ) : null}\n <TextField\n size=\"small\"\n label=\"Notes\"\n value={notes}\n onChange={(event) => {\n setNotes(event.target.value)\n setNotesDirty(true)\n }}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: NOTES_MAX } }}\n />\n </Stack>\n </CardDisplay>\n </>\n )\n}\nLeadPropertiesCard.displayName = 'LeadPropertiesCard'\n\nexport default LeadPropertiesCard\n"],"names":["Aglyn","mdiAccountCancelOutline","AppLink","CardDisplay","MdiIcon","useSnackbar","useFirestore","writeGuardedBySeed","Alert","Button","FormControl","InputLabel","Select","Stack","TextField","Tooltip","Typography","deleteField","doc","serverTimestamp","updateDoc","useEffect","useId","useMemo","useState","useContactFieldDefinitions","useLeadSourcePicklist","useLeadStatusPicklist","useLeadPicklists","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","crmRoutes","LeadProfileFields","leadProfileDraftBody","leadProfileDraftFrom","CrmCallButton","CrmPhoneLink","CrmEmailStateChip","CrmAccountLockChip","CrmEmailGatewayChip","CrmRecordChip","CrmRecordHeader","CrmSendEmailButton","LeadOwnerSelect","LeadStatusChip","leadStatusChoices","leadStatusMenuItems","crmClientListFields","CRM_CLIENT_SEARCH_FIELDS","CRM_CLIENT_SORT_FIELDS","useCrmSharingFollowUp","LEAD_CLIENT_LIST_FIELDS","NOTES_MAX","CRM_LEAD_NOTES_MAX","profileWrite","patch","write","key","value","Object","entries","undefined","CONVERT_PENDING_ERASURE_REASON","Fact","props","spacing","variant","color","label","component","children","LEAD_PAGE_HELP","pluginDocsHelp","anchor","LeadPropertiesCard","lead","hostId","consentGroup","orgId","leadId","leadStatus","fromCache","basePath","roster","onConvert","onUnqualify","extraMenuItems","banner","extraChips","erasurePending","org","firestore","enqueueSnackbar","routes","followUpSharing","refFor","status","crmLeadStatus","converted","Boolean","convertedContactId","open","isCrmLeadOpen","emailState","readEmailState","leadPhone","String","trim","leadMobile","mobilePhone","doNotCall","notes","setNotes","statusLabelId","notesDirty","setNotesDirty","profile","setProfile","profileDirty","setProfileDirty","saving","setSaving","profileErrors","setProfileErrors","profileSeed","JSON","stringify","CRM_LEAD_PROFILE_KEYS","map","fields","leadSources","leadStatuses","leadPicklists","statusChoices","picklist","storedCustom","custom","setCustom","editProfile","current","done","ref","persist","updatedAt","error","Error","message","customChanged","length","dirty","discard","saveDetails","update","body","revenueError","errors","normalizeCrmLeadProfile","shown","keys","missing","active","join","profileFields","composed","crmLeadComposedName","assign","ready","slice","landed","verdict","subject","unreadable","ok","consent","readMarketingBasis","consentLine","basis","basisAtMs","Date","toLocaleDateString","kind","title","subtitle","help","backHref","section","backLabel","booking","recordId","actions","componentVariant","nativeButton","href","contact","size","companyId","company","dealId","deal","span","disabled","onClick","link","phone","email","name","menuItems","icon","path","destructive","chips","statuses","state","ownerUid","labelFor","direction","xs","md","sx","alignItems","convertedAtMs","toLocaleString","minWidth","id","labelId","crmLeadStatusLabel","onChange","event","choice","find","entry","target","statusLabel","unqualifiedReason","fullWidth","uid","severity","header","contentGutterX","contentGutterY","HeaderProps","action","draft","lists","stored","definition","$id","multiline","minRows","slotProps","htmlInput","maxLength","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-properties-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type {\n AglynOrgBilling,\n ConsentGroup,\n CrmLeadFields,\n CrmLeadProfilePatch,\n} from '@aglyn/aglyn'\nimport { mdiAccountCancelOutline } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n FormControl,\n InputLabel,\n Select,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { deleteField, doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useId, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\nimport { useLeadStatusPicklist } from '../hooks/use-lead-status-picklist'\nimport { useLeadPicklists } from '../hooks/use-lead-picklists'\nimport {\n crmCustomDraftChanges,\n type CrmCustomDraft,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n LeadProfileFields,\n type LeadProfileDraft,\n leadProfileDraftBody,\n leadProfileDraftFrom,\n} from './lead-profile-fields'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmAccountLockChip, CrmEmailGatewayChip } from './crm-email-check'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordEmailActionsZone } from './crm-email-actions-zone'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { LeadStatusChip } from './lead-status-chip'\nimport { leadStatusChoices, leadStatusMenuItems } from './lead-status-options'\nimport {\n crmClientListFields,\n CRM_CLIENT_SEARCH_FIELDS,\n CRM_CLIENT_SORT_FIELDS,\n} from '../model/crm-list-query'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\n/** The Leads list's fields a profile save rewrites; the verdict key is the server's. */\nconst LEAD_CLIENT_LIST_FIELDS = [\n ...CRM_CLIENT_SEARCH_FIELDS,\n // The Lead, Company and Title headers' sort keys (AGL-3680).\n ...CRM_CLIENT_SORT_FIELDS,\n 'leadSourceKey',\n // The lead source's group, read off the org's list (AGL-3577).\n 'leadSourceDirection',\n // Industry and Rating (AGL-3513).\n 'industryKey',\n 'ratingKey',\n] as const\n\nconst NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX\n\n/** The patch as the document takes it: a cleared field is deleted, not blanked. */\nfunction profileWrite(patch: CrmLeadProfilePatch): Record<string, unknown> {\n const write: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n write[key] = value === null ? deleteField() : value\n }\n return write\n}\n\n/**\n * Why Convert is refused while an erasure waits on the person — the same\n * sentence shape the overflow's items carry, so the two read as one state.\n */\nexport const CONVERT_PENDING_ERASURE_REASON = 'An erasure is pending for this person'\n\n/** A label over a value — the record page's one row shape. */\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\n/**\n * The help on a lead's header card — one card, so the page's loading and\n * not-found branches carry the same link as the loaded one.\n */\nexport const LEAD_PAGE_HELP = Aglyn.pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })\n\nexport interface LeadPropertiesCardProps {\n /**\n * The site this lead is acted on as — the mounted one, or at the\n * organization level the lead's own first capturing site (AGL-3278).\n * `null` for a lead no site has captured: the booking door, the call and\n * the email close, because each of them is one site's to offer.\n */\n hostId: string | null\n /**\n * The group the consent basis is read against (AGL-3278): the site's own\n * declared group, resolved by the page. Not `soloConsentGroup(hostId)`,\n * which is what this card used to spell and which cannot see a refusal\n * recorded against a sibling brand — three sites presenting as one sender\n * are one sender to the person unsubscribing from them.\n */\n consentGroup: ConsentGroup\n /**\n * The org the site belongs to, as the page already resolved it — the\n * custom lead fields are ORG-wide (AGL-3272), and a second lookup here\n * would be one more read per record page for an answer already in hand.\n */\n orgId: string | null\n leadId: string\n lead: Record<string, unknown> & CrmLeadFields\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n basePath: string\n roster: OrgMemberOptions\n onConvert: () => void\n /** Opens the Unqualify dialog — with the Unqualified value picked, when the status select named one. */\n onUnqualify: (statusLabel?: string) => void\n /**\n * Items the page adds to the overflow beside Unqualify — the privacy\n * erasure (AGL-2623) lives on the page, because it needs the workspace\n * role and the API, and this card owns the header it must appear in.\n */\n extraMenuItems?: RowActionsMenuItem[]\n /** What the page shows above the facts — the erasure-pending state. */\n banner?: React.ReactNode\n /**\n * Chips the page adds after the owner — the campaigns the lead is filed\n * under (AGL-3274), named from the containers the page reads once for\n * this header and the Campaigns card below it.\n */\n extraChips?: React.ReactNode\n /**\n * An erasure request is waiting on this person (AGL-2623). Convert stays\n * on the page but is refused with the reason, the way the overflow's items\n * are: a conversion filed now would reach the capture door only to be\n * refused there, and the lead itself goes when the request runs.\n */\n erasurePending?: boolean\n /**\n * The org the shell passed: the booking door reads whether the lead's site\n * runs Bookings and whether the plan is entitled to it (AGL-2660), and a\n * logged call reads the activity scope it belongs in (AGL-2661).\n */\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * What the team knows and decides about a lead: status, owner, notes, and\n * the identity and consent the capture recorded (AGL-2608).\n *\n * Two cards. The record's header carries the identity, the status and owner,\n * the consent the capture recorded and the record's actions. **Details**\n * under it carries the profile, the org's own lead fields under **More\n * fields**, and the notes — one card with one Save in its header, the way\n * Salesforce keeps a record's standard and custom fields in one Details\n * section.\n *\n * Status and owner are single-field client writes — the rules let a site\n * admin, editor or author update `hosts/{hostId}/leads`, and a one-field\n * `update` cannot roll anything else back. The details are drafts seeded\n * from the document, so their save goes through `writeGuardedBySeed`: a\n * draft edited over a cached read would otherwise overwrite a newer note or\n * profile with an older one.\n *\n * Converted leads are read-only here, but for the notes. Their status is the\n * conversion, and the header's actions become links to what the conversion\n * made.\n */\nexport function LeadPropertiesCard(props: LeadPropertiesCardProps) {\n const {\n hostId,\n consentGroup,\n orgId,\n leadId,\n lead,\n leadStatus,\n fromCache,\n basePath,\n roster,\n onConvert,\n onUnqualify,\n extraMenuItems = [],\n banner,\n extraChips,\n erasurePending = false,\n org,\n } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const routes = crmRoutes(basePath)\n // A client-direct write owes the org's sharing rules a re-evaluation (AGL-3336).\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n /*\n * Built at USE, not at render (AGL-3275). A lead is an org row, so the org\n * has to be known to address one — and `orgId` is null while the lookup\n * settles. Composing a ref from that null throws during render, which the\n * page shows as a 500 rather than as a card still loading.\n */\n const refFor = () => (orgId ? doc(firestore, 'orgs', orgId, 'leads', leadId) : null)\n const status = Aglyn.crmLeadStatus(lead)\n const converted = Boolean(lead.convertedContactId)\n const open = Aglyn.isCrmLeadOpen(lead) && !converted\n // The last verdict on the address (AGL-3245), as the platform stamped it.\n const emailState = Aglyn.readEmailState(lead)\n /** What a capture that took a number left on the document (AGL-2661). */\n const leadPhone = String(lead['phone'] ?? '').trim()\n const leadMobile = String(lead.mobilePhone ?? '').trim()\n // The person asked not to be phoned (AGL-3513): a hint on every dial control.\n const doNotCall = lead.doNotCall === true\n\n const [notes, setNotes] = useState(String(lead.notes ?? ''))\n // The label's id, so the status combobox is named \"Status\" rather than\n // after the status it shows — see `LeadOwnerSelect`.\n const statusLabelId = useId()\n const [notesDirty, setNotesDirty] = useState(false)\n /*\n * THE LEAD'S OWN PROFILE (AGL-3231, AGL-3513) — the name's parts,\n * company, title, the phones, website, address, tags, lead source,\n * industry, rating, size and revenue — edited in the Details card with\n * the notes and the custom fields, under one Save, the way the contact's\n * Properties card saves. Seeded from the document and guarded on save: a\n * draft edited over a cached read must not overwrite a newer profile\n * with an older one.\n */\n const [profile, setProfile] = useState<LeadProfileDraft>(() => leadProfileDraftFrom(lead))\n const [profileDirty, setProfileDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n const [profileErrors, setProfileErrors] = useState<Record<string, string>>({})\n // The stored profile as one value, so a change to any of its fields reseeds.\n const profileSeed = JSON.stringify(Aglyn.CRM_LEAD_PROFILE_KEYS.map((key) => lead[key] ?? null))\n useEffect(() => {\n if (!profileDirty) setProfile(leadProfileDraftFrom(lead))\n // The draft follows the document until it is edited.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [profileDirty, profileSeed])\n /*\n * THE ORG'S OWN LEAD FIELDS (AGL-3272), edited under the same Save as\n * the profile: one button over one card, so a person filling a lead in\n * does not have to find two.\n *\n * The draft holds only the keys the reader touched, and Save writes the\n * difference as dotted paths — a `custom` map written whole would take\n * out every key this card did not show, which is what a retired field's\n * values and an integration's writes sit under.\n */\n const fields = useContactFieldDefinitions(orgId, 'lead')\n // The org's lead source values (AGL-3298), for the select below.\n const leadSources = useLeadSourcePicklist(orgId)\n // The org's lead status values (AGL-3512): the chip's words and the select's choices.\n const leadStatuses = useLeadStatusPicklist(orgId)\n // The org's Salutation, Industry and Rating lists (AGL-3513).\n const leadPicklists = useLeadPicklists(orgId)\n const statusChoices = useMemo(\n () => leadStatusChoices(leadStatuses.picklist, lead),\n [leadStatuses.picklist, lead],\n )\n const storedCustom = useMemo(() => lead.custom ?? {}, [lead.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n const editProfile = <K extends keyof LeadProfileDraft>(key: K, value: LeadProfileDraft[K]) => {\n setProfile((current) => ({ ...current, [key]: value }))\n setProfileDirty(true)\n }\n // A newer note from the server replaces an UNEDITED draft; an edited one is\n // the reader's, and the guard on save decides whether it may land.\n useEffect(() => {\n if (!notesDirty) setNotes(String(lead.notes ?? ''))\n }, [lead.notes, notesDirty])\n\n /** One update of the lead, and whether it landed. */\n const write = async (fields: Record<string, unknown>, done: string): Promise<boolean> => {\n try {\n const ref = refFor()\n if (!ref) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return false\n }\n await updateDoc(ref, { ...fields, updatedAt: serverTimestamp() })\n followUpSharing('leads', [leadId])\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n return false\n }\n }\n\n /*\n * What Save is offered for: a profile field edited, a custom value that\n * differs from the stored map, or the notes. Touched-and-put-back does\n * not count for a custom value — the draft keeps the key either way, and\n * a Save that wrote nothing would still bump `updatedAt`.\n */\n const customChanged = crmCustomDraftChanges(storedCustom, custom).length > 0\n const dirty = profileDirty || customChanged || notesDirty\n\n /** Every draft back to what the document holds. */\n const discard = () => {\n setProfile(leadProfileDraftFrom(lead))\n setProfileDirty(false)\n setProfileErrors({})\n setCustom({})\n setNotes(String(lead.notes ?? ''))\n setNotesDirty(false)\n }\n\n /*\n * The Details card's one Save: the profile and the custom values while\n * the lead is open, and the notes, in one update. A converted lead's\n * profile is the contact's now, so only its notes are written.\n */\n const saveDetails = async () => {\n const update: Record<string, unknown> = {}\n if (!converted && (profileDirty || customChanged)) {\n const { body, revenueError } = leadProfileDraftBody(profile)\n const { patch, errors } = Aglyn.normalizeCrmLeadProfile(body)\n const shown: Record<string, string> = { ...errors }\n if (revenueError) shown['annualRevenueCents'] = revenueError\n setProfileErrors(shown)\n if (Object.keys(shown).length) return\n /*\n * A required field the reader CLEARED is refused; one the lead has\n * always lacked is not this save's to demand, or a field added after\n * the lead was captured would block every later edit to its company.\n */\n const missing = crmCustomDraftMissingRequired(\n fields.active,\n storedCustom,\n custom,\n 'edit',\n )\n if (missing.length) {\n enqueueSnackbar(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`, {\n variant: 'warning',\n })\n return\n }\n const profileFields = profileWrite(patch)\n // While a first or last name is set, the name is theirs (AGL-3513).\n const composed = Aglyn.crmLeadComposedName(lead, patch)\n if (composed && composed !== lead['name']) profileFields['name'] = composed\n Object.assign(\n update,\n profileFields,\n crmCustomDraftWrites(storedCustom, custom),\n // The company, title, tags and lead source are what the Leads list\n // searches and filters by (AGL-3321).\n crmClientListFields(\n 'leads',\n lead,\n profileFields,\n LEAD_CLIENT_LIST_FIELDS,\n // A direction only from the org's list as the server answered it.\n leadSources.ready ? { leadSources: leadSources.picklist } : {},\n ),\n )\n }\n if (notesDirty) update['notes'] = notes.trim().slice(0, NOTES_MAX)\n if (!Object.keys(update).length) return\n setSaving(true)\n let landed = false\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n landed = await write(update, 'Lead saved')\n },\n )\n setSaving(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The lead could not be saved.', {\n variant: 'warning',\n })\n return\n }\n if (!landed) return\n setProfileDirty(false)\n setNotesDirty(false)\n // The draft is spent: what it held is stored, and the controls read\n // the document again.\n setCustom({})\n }\n\n const consent = Aglyn.readMarketingBasis(lead, consentGroup)\n const consentLine =\n consent.basis === 'granted'\n ? `Opted in to marketing${\n consent.basisAtMs ? ` on ${new Date(consent.basisAtMs).toLocaleDateString()}` : ''\n }`\n : consent.basis === 'declined'\n ? 'Declined marketing'\n : 'No marketing consent recorded — this lead cannot be emailed marketing'\n\n return (\n <>\n <CrmRecordHeader\n kind=\"Lead\"\n title={String(lead['name'] || lead['email'] || leadId)}\n // The name is the heading; the address is the one line under it,\n // unless the address IS the name, in which case there is no second fact.\n subtitle={lead['name'] ? String(lead['email'] ?? '') : undefined}\n help={LEAD_PAGE_HELP}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n // The booking door (AGL-2660), while the lead is still the record\n // being worked: once converted, the contact is where a meeting is\n // booked from, and the links below lead there.\n booking={converted ? undefined : { hostId, org, kind: 'lead', recordId: leadId }}\n actions={\n <>\n {/*\n Once converted, the lead's actions are the records the\n conversion made.\n */}\n {converted ? (\n <>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.contact(String(lead.convertedContactId))}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open contact'}\n </Button>\n {lead.companyId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.company(lead.companyId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open company'}\n </Button>\n ) : null}\n {lead.dealId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.deal(lead.dealId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open deal'}\n </Button>\n ) : null}\n </>\n ) : erasurePending ? (\n <Tooltip title={CONVERT_PENDING_ERASURE_REASON}>\n {/* A disabled button receives no pointer events, so the\n tooltip anchors on the span around it. */}\n <span>\n <Button size=\"small\" variant=\"contained\" disabled>\n {'Convert'}\n </Button>\n </span>\n </Tooltip>\n ) : (\n <Button size=\"small\" variant=\"contained\" onClick={onConvert}>\n {'Convert'}\n </Button>\n )}\n {/* Dial the number the capture carried, and log the call (AGL-2661). */}\n <CrmCallButton\n hostId={hostId}\n org={org}\n link={{ leadId }}\n phone={leadPhone || leadMobile}\n doNotCall={doNotCall}\n />\n {/* An answer in a thread another plugin keeps with them (AGL-4020), ahead of a new message. */}\n <CrmRecordEmailActionsZone\n hostId={hostId}\n org={org}\n kind=\"lead\"\n recordId={leadId}\n name={String(lead['name'] ?? '')}\n email={String(lead['email'] ?? '') || null}\n />\n <CrmSendEmailButton\n hostId={hostId}\n leadId={leadId}\n email={String(lead['email'] ?? '')}\n name={String(lead['name'] ?? '')}\n emailState={emailState}\n />\n </>\n }\n menuItems={[\n ...(open\n ? [\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => onUnqualify(),\n } satisfies RowActionsMenuItem,\n ]\n : []),\n ...extraMenuItems,\n ]}\n chips={\n <>\n <LeadStatusChip lead={lead} statuses={leadStatuses.picklist} />\n {/* The verdict on the address (AGL-3245), beside the status: a\n bounce does not move New or Working, but it is the first\n thing a person deciding whether to write must see. */}\n <CrmEmailStateChip state={emailState} />\n {/* The mail gateway in front of the address, and what it did\n with this site's mail this week (AGL-3328). */}\n <CrmEmailGatewayChip\n hostId={hostId}\n email={String(lead['email'] ?? '')}\n emailState={emailState}\n />\n <CrmAccountLockChip\n hostId={hostId}\n email={String(lead['email'] ?? '')}\n />\n <CrmRecordChip\n label=\"Owner\"\n value={lead.ownerUid ? roster.labelFor(lead.ownerUid) : undefined}\n />\n {extraChips}\n </>\n }\n >\n <Stack spacing={3}>\n {banner}\n {/*\n Only when the capture carried one (AGL-2661): the sign-up door\n writes no phone, and a booking only when its service asks for\n one (AGL-3493), so a row for every lead would often be a\n permanent blank. A form that captures one fills this.\n */}\n {leadPhone ? (\n <Fact label=\"Phone\">\n <CrmPhoneLink phone={leadPhone} doNotCall={doNotCall} />\n </Fact>\n ) : null}\n {leadMobile ? (\n <Fact label=\"Mobile phone\">\n <CrmPhoneLink phone={leadMobile} doNotCall={doNotCall} />\n </Fact>\n ) : null}\n <Fact label=\"Marketing consent\">{consentLine}</Fact>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n {converted ? (\n <Fact label=\"Status\">\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <LeadStatusChip lead={lead} statuses={leadStatuses.picklist} />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {lead.convertedAtMs\n ? `Converted ${new Date(lead.convertedAtMs).toLocaleString()}`\n : 'Converted'}\n </Typography>\n </Stack>\n </Fact>\n ) : (\n <FormControl size=\"small\" sx={{ minWidth: 200 }}>\n <InputLabel id={statusLabelId}>{'Status'}</InputLabel>\n <Select\n labelId={statusLabelId}\n label=\"Status\"\n value={Aglyn.crmLeadStatusLabel(lead, leadStatuses.picklist)}\n onChange={(event) => {\n const choice = statusChoices.find((entry) => entry.label === event.target.value)\n if (!choice) return\n if (choice.status === 'unqualified') {\n onUnqualify(choice.label)\n return\n }\n // The meaning and the org's label for it, together\n // (AGL-3512). Reopening drops the reason with the closed\n // state: a lead being worked again is not \"unqualified\n // because …\".\n void write(\n {\n status: choice.status,\n statusLabel: choice.label,\n ...(status === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n },\n 'Status updated',\n )\n }}\n >\n {leadStatusMenuItems(statusChoices)}\n </Select>\n </FormControl>\n )}\n <LeadOwnerSelect\n value={lead.ownerUid}\n roster={roster}\n fullWidth={false}\n onChange={(uid) =>\n void write({ ownerUid: uid || deleteField() }, uid ? 'Owner assigned' : 'Owner cleared')\n }\n />\n </Stack>\n {status === 'unqualified' && lead.unqualifiedReason ? (\n <Alert severity=\"info\">{`Unqualified: ${lead.unqualifiedReason}`}</Alert>\n ) : null}\n </Stack>\n </CrmRecordHeader>\n {/*\n The profile (AGL-3231), the org's own lead fields (AGL-3272) and the\n notes: what Salesforce keeps on a lead and hands to the contact and\n the account on convert. Read-only once converted — the contact is the\n record then, and the header's links lead there — but for the notes.\n */}\n <CardDisplay\n header={'Details'}\n help={Aglyn.pluginDocsHelp('crmLeads', { anchor: '#lead-details' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {dirty ? (\n <Button size=\"small\" disabled={saving} onClick={discard}>\n {'Discard changes'}\n </Button>\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void saveDetails()}\n disabled={!dirty || saving}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <LeadProfileFields\n draft={profile}\n onChange={editProfile}\n errors={profileErrors}\n disabled={converted}\n leadSources={leadSources.picklist}\n lists={leadPicklists.lists}\n stored={lead}\n />\n {/*\n The org's own lead fields (AGL-3272), after the built-in ones\n because they describe the same record and save with it. Absent,\n not an empty box, while the org defines none.\n */}\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'More fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={converted || saving}\n />\n ))}\n </>\n ) : null}\n <TextField\n size=\"small\"\n label=\"Notes\"\n value={notes}\n onChange={(event) => {\n setNotes(event.target.value)\n setNotesDirty(true)\n }}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: NOTES_MAX } }}\n />\n </Stack>\n </CardDisplay>\n </>\n )\n}\nLeadPropertiesCard.displayName = 'LeadPropertiesCard'\n\nexport default LeadPropertiesCard\n"],"names":["Aglyn","mdiAccountCancelOutline","AppLink","CardDisplay","MdiIcon","useSnackbar","useFirestore","writeGuardedBySeed","Alert","Button","FormControl","InputLabel","Select","Stack","TextField","Tooltip","Typography","deleteField","doc","serverTimestamp","updateDoc","useEffect","useId","useMemo","useState","useContactFieldDefinitions","useLeadSourcePicklist","useLeadStatusPicklist","useLeadPicklists","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","crmRoutes","LeadProfileFields","leadProfileDraftBody","leadProfileDraftFrom","CrmCallButton","CrmPhoneLink","CrmEmailStateChip","CrmAccountLockChip","CrmEmailGatewayChip","CrmRecordChip","CrmRecordHeader","CrmRecordEmailActionsZone","CrmSendEmailButton","LeadOwnerSelect","LeadStatusChip","leadStatusChoices","leadStatusMenuItems","crmClientListFields","CRM_CLIENT_SEARCH_FIELDS","CRM_CLIENT_SORT_FIELDS","useCrmSharingFollowUp","LEAD_CLIENT_LIST_FIELDS","NOTES_MAX","CRM_LEAD_NOTES_MAX","profileWrite","patch","write","key","value","Object","entries","undefined","CONVERT_PENDING_ERASURE_REASON","Fact","props","spacing","variant","color","label","component","children","LEAD_PAGE_HELP","pluginDocsHelp","anchor","LeadPropertiesCard","lead","hostId","consentGroup","orgId","leadId","leadStatus","fromCache","basePath","roster","onConvert","onUnqualify","extraMenuItems","banner","extraChips","erasurePending","org","firestore","enqueueSnackbar","routes","followUpSharing","refFor","status","crmLeadStatus","converted","Boolean","convertedContactId","open","isCrmLeadOpen","emailState","readEmailState","leadPhone","String","trim","leadMobile","mobilePhone","doNotCall","notes","setNotes","statusLabelId","notesDirty","setNotesDirty","profile","setProfile","profileDirty","setProfileDirty","saving","setSaving","profileErrors","setProfileErrors","profileSeed","JSON","stringify","CRM_LEAD_PROFILE_KEYS","map","fields","leadSources","leadStatuses","leadPicklists","statusChoices","picklist","storedCustom","custom","setCustom","editProfile","current","done","ref","persist","updatedAt","error","Error","message","customChanged","length","dirty","discard","saveDetails","update","body","revenueError","errors","normalizeCrmLeadProfile","shown","keys","missing","active","join","profileFields","composed","crmLeadComposedName","assign","ready","slice","landed","verdict","subject","unreadable","ok","consent","readMarketingBasis","consentLine","basis","basisAtMs","Date","toLocaleDateString","kind","title","subtitle","help","backHref","section","backLabel","booking","recordId","actions","componentVariant","nativeButton","href","contact","size","companyId","company","dealId","deal","span","disabled","onClick","link","phone","name","email","menuItems","icon","path","destructive","chips","statuses","state","ownerUid","labelFor","direction","xs","md","sx","alignItems","convertedAtMs","toLocaleString","minWidth","id","labelId","crmLeadStatusLabel","onChange","event","choice","find","entry","target","statusLabel","unqualifiedReason","fullWidth","uid","severity","header","contentGutterX","contentGutterY","HeaderProps","action","draft","lists","stored","definition","$id","multiline","minRows","slotProps","htmlInput","maxLength","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,YAAYA,WAAW,eAAc;AAOrC,SAASC,uBAAuB,QAAQ,yBAAwB;AAChE,SAASC,OAAO,EAAEC,WAAW,EAAEC,OAAO,QAAQ,uBAAsB;AAEpE,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAEEC,YAAY,EACZC,kBAAkB,QACb,iCAAgC;AACvC,SACEC,KAAK,EACLC,MAAM,EACNC,WAAW,EACXC,UAAU,EACVC,MAAM,EACNC,KAAK,EACLC,SAAS,EACTC,OAAO,EACPC,UAAU,QACL,gBAAe;AACtB,SAASC,WAAW,EAAEC,GAAG,EAAEC,eAAe,EAAEC,SAAS,QAAQ,qBAAoB;AACjF,SAASC,SAAS,EAAEC,KAAK,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AAC3D,SAASC,0BAA0B,QAAQ,4CAAwC;AACnF,SAASC,qBAAqB,QAAQ,uCAAmC;AACzE,SAASC,qBAAqB,QAAQ,uCAAmC;AACzE,SAASC,gBAAgB,QAAQ,iCAA6B;AAC9D,SACEC,qBAAqB,EAErBC,6BAA6B,EAC7BC,mBAAmB,EACnBC,oBAAoB,QACf,+BAA2B;AAClC,SAASC,qBAAqB,QAAQ,gCAA4B;AAClE,SAASC,SAAS,QAAQ,yBAAqB;AAC/C,SACEC,iBAAiB,EAEjBC,oBAAoB,EACpBC,oBAAoB,QACf,2BAAuB;AAC9B,SAASC,aAAa,EAAEC,YAAY,QAAQ,wBAAoB;AAChE,SAASC,iBAAiB,QAAQ,4BAAwB;AAC1D,SAASC,kBAAkB,EAAEC,mBAAmB,QAAQ,uBAAmB;AAC3E,SAASC,aAAa,EAAEC,eAAe,QAAQ,yBAAqB;AACpE,SAASC,yBAAyB,QAAQ,8BAA0B;AACpE,SAASC,kBAAkB,QAAQ,6BAAyB;AAE5D,SAASC,eAAe,QAAQ,yBAAqB;AACrD,SAASC,cAAc,QAAQ,wBAAoB;AACnD,SAASC,iBAAiB,EAAEC,mBAAmB,QAAQ,2BAAuB;AAC9E,SACEC,mBAAmB,EACnBC,wBAAwB,EACxBC,sBAAsB,QACjB,6BAAyB;AAChC,SAASC,qBAAqB,QAAQ,8BAA0B;AAEhE,sFAAsF,GACtF,MAAMC,0BAA0B;OAC3BH;IACH,6DAA6D;OAC1DC;IACH;IACA,+DAA+D;IAC/D;IACA,kCAAkC;IAClC;IACA;CACD;AAED,MAAMG,YAAYxD,MAAMyD,kBAAkB;AAE1C,iFAAiF,GACjF,SAASC,aAAaC,KAA0B;IAC9C,MAAMC,QAAiC,CAAC;IACxC,KAAK,MAAM,CAACC,KAAKC,MAAM,IAAIC,OAAOC,OAAO,CAACL,OAAQ;QAChD,IAAIG,UAAUG,WAAW;QACzBL,KAAK,CAACC,IAAI,GAAGC,UAAU,OAAO7C,gBAAgB6C;IAChD;IACA,OAAOF;AACT;AAEA;;;CAGC,GACD,OAAO,MAAMM,iCAAiC,wCAAuC;AAErF,4DAA4D,GAC5D,SAASC,KAAKC,KAAmD;IAC/D,qBACE,MAACvD;QAAMwD,SAAS;;0BACd,KAACrD;gBAAWsD,SAAQ;gBAAUC,OAAM;0BACjCH,MAAMI,KAAK;;0BAEd,KAACxD;gBAAWsD,SAAQ;gBAAQG,WAAU;0BACnCL,MAAMM,QAAQ;;;;AAIvB;AAEA;;;CAGC,GACD,OAAO,MAAMC,iBAAiB3E,MAAM4E,cAAc,CAAC,YAAY;IAAEC,QAAQ;AAAgB,GAAE;AA+D3F;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,mBAAmBV,KAA8B;QAqCtCW,aACCA,mBAIgBA,aAmMJA,aA6EbA,YACCA,cAKAA,cACDA,aA8BCA,cAKAA;IAnW1B,MAAM,EACJC,MAAM,EACNC,YAAY,EACZC,KAAK,EACLC,MAAM,EACNJ,IAAI,EACJK,UAAU,EACVC,SAAS,EACTC,QAAQ,EACRC,MAAM,EACNC,SAAS,EACTC,WAAW,EACXC,iBAAiB,EAAE,EACnBC,MAAM,EACNC,UAAU,EACVC,iBAAiB,KAAK,EACtBC,GAAG,EACJ,GAAG1B;IACJ,MAAM2B,YAAYzF;IAClB,MAAM,EAAE0F,eAAe,EAAE,GAAG3F;IAC5B,MAAM4F,SAAS/D,UAAUoD;IACzB,iFAAiF;IACjF,MAAMY,kBAAkB5C,sBAAsB0B,QAAQE;IACtD;;;;;GAKC,GACD,MAAMiB,SAAS,IAAOjB,QAAQhE,IAAI6E,WAAW,QAAQb,OAAO,SAASC,UAAU;IAC/E,MAAMiB,SAASpG,MAAMqG,aAAa,CAACtB;IACnC,MAAMuB,YAAYC,QAAQxB,KAAKyB,kBAAkB;IACjD,MAAMC,OAAOzG,MAAM0G,aAAa,CAAC3B,SAAS,CAACuB;IAC3C,0EAA0E;IAC1E,MAAMK,aAAa3G,MAAM4G,cAAc,CAAC7B;IACxC,uEAAuE,GACvE,MAAM8B,YAAYC,QAAO/B,cAAAA,IAAI,CAAC,QAAQ,YAAbA,cAAiB,IAAIgC,IAAI;IAClD,MAAMC,aAAaF,QAAO/B,oBAAAA,KAAKkC,WAAW,YAAhBlC,oBAAoB,IAAIgC,IAAI;IACtD,8EAA8E;IAC9E,MAAMG,YAAYnC,KAAKmC,SAAS,KAAK;IAErC,MAAM,CAACC,OAAOC,SAAS,GAAG5F,SAASsF,QAAO/B,cAAAA,KAAKoC,KAAK,YAAVpC,cAAc;IACxD,uEAAuE;IACvE,qDAAqD;IACrD,MAAMsC,gBAAgB/F;IACtB,MAAM,CAACgG,YAAYC,cAAc,GAAG/F,SAAS;IAC7C;;;;;;;;GAQC,GACD,MAAM,CAACgG,SAASC,WAAW,GAAGjG,SAA2B,IAAMa,qBAAqB0C;IACpF,MAAM,CAAC2C,cAAcC,gBAAgB,GAAGnG,SAAS;IACjD,MAAM,CAACoG,QAAQC,UAAU,GAAGrG,SAAS;IACrC,MAAM,CAACsG,eAAeC,iBAAiB,GAAGvG,SAAiC,CAAC;IAC5E,6EAA6E;IAC7E,MAAMwG,cAAcC,KAAKC,SAAS,CAAClI,MAAMmI,qBAAqB,CAACC,GAAG,CAAC,CAACvE;YAAQkB;gBAAAA,YAAAA,IAAI,CAAClB,IAAI,YAATkB,YAAa;;IACzF1D,UAAU;QACR,IAAI,CAACqG,cAAcD,WAAWpF,qBAAqB0C;IACnD,qDAAqD;IACrD,uDAAuD;IACzD,GAAG;QAAC2C;QAAcM;KAAY;IAC9B;;;;;;;;;GASC,GACD,MAAMK,SAAS5G,2BAA2ByD,OAAO;IACjD,iEAAiE;IACjE,MAAMoD,cAAc5G,sBAAsBwD;IAC1C,sFAAsF;IACtF,MAAMqD,eAAe5G,sBAAsBuD;IAC3C,8DAA8D;IAC9D,MAAMsD,gBAAgB5G,iBAAiBsD;IACvC,MAAMuD,gBAAgBlH,QACpB,IAAM0B,kBAAkBsF,aAAaG,QAAQ,EAAE3D,OAC/C;QAACwD,aAAaG,QAAQ;QAAE3D;KAAK;IAE/B,MAAM4D,eAAepH,QAAQ;YAAMwD;gBAAAA,eAAAA,KAAK6D,MAAM,YAAX7D,eAAe,CAAC;OAAG;QAACA,KAAK6D,MAAM;KAAC;IACnE,MAAM,CAACA,QAAQC,UAAU,GAAGrH,SAAyB,CAAC;IAEtD,MAAMsH,cAAc,CAAmCjF,KAAQC;QAC7D2D,WAAW,CAACsB,UAAa,aAAKA;gBAAS,CAAClF,IAAI,EAAEC;;QAC9C6D,gBAAgB;IAClB;IACA,4EAA4E;IAC5E,mEAAmE;IACnEtG,UAAU;YACyB0D;QAAjC,IAAI,CAACuC,YAAYF,SAASN,QAAO/B,cAAAA,KAAKoC,KAAK,YAAVpC,cAAc;IACjD,GAAG;QAACA,KAAKoC,KAAK;QAAEG;KAAW;IAE3B,mDAAmD,GACnD,MAAM1D,QAAQ,OAAOyE,QAAiCW;QACpD,IAAI;YACF,MAAMC,MAAM9C;YACZ,IAAI,CAAC8C,KAAK;gBACRjD,gBAAgB,yDAAyD;oBACvE1B,SAAS;oBACT4E,SAAS;gBACX;gBACA,OAAO;YACT;YACA,MAAM9H,UAAU6H,KAAK,aAAKZ;gBAAQc,WAAWhI;;YAC7C+E,gBAAgB,SAAS;gBAACf;aAAO;YACjCa,gBAAgBgD,MAAM;gBAAE1E,SAAS;gBAAW4E,SAAS;YAAM;YAC3D,OAAO;QACT,EAAE,OAAOE,OAAO;YACdpD,gBACEoD,iBAAiBC,QAAQD,MAAME,OAAO,GAAG,kCACzC;gBAAEhF,SAAS;YAAQ;YAErB,OAAO;QACT;IACF;IAEA;;;;;GAKC,GACD,MAAMiF,gBAAgB1H,sBAAsB8G,cAAcC,QAAQY,MAAM,GAAG;IAC3E,MAAMC,QAAQ/B,gBAAgB6B,iBAAiBjC;IAE/C,iDAAiD,GACjD,MAAMoC,UAAU;YAKE3E;QAJhB0C,WAAWpF,qBAAqB0C;QAChC4C,gBAAgB;QAChBI,iBAAiB,CAAC;QAClBc,UAAU,CAAC;QACXzB,SAASN,QAAO/B,cAAAA,KAAKoC,KAAK,YAAVpC,cAAc;QAC9BwC,cAAc;IAChB;IAEA;;;;GAIC,GACD,MAAMoC,cAAc;QAClB,MAAMC,SAAkC,CAAC;QACzC,IAAI,CAACtD,aAAcoB,CAAAA,gBAAgB6B,aAAY,GAAI;YACjD,MAAM,EAAEM,IAAI,EAAEC,YAAY,EAAE,GAAG1H,qBAAqBoF;YACpD,MAAM,EAAE7D,KAAK,EAAEoG,MAAM,EAAE,GAAG/J,MAAMgK,uBAAuB,CAACH;YACxD,MAAMI,QAAgC,aAAKF;YAC3C,IAAID,cAAcG,KAAK,CAAC,qBAAqB,GAAGH;YAChD/B,iBAAiBkC;YACjB,IAAIlG,OAAOmG,IAAI,CAACD,OAAOT,MAAM,EAAE;YAC/B;;;;OAIC,GACD,MAAMW,UAAUrI,8BACduG,OAAO+B,MAAM,EACbzB,cACAC,QACA;YAEF,IAAIuB,QAAQX,MAAM,EAAE;gBAClBxD,gBAAgB,GAAGmE,QAAQE,IAAI,CAAC,MAAM,CAAC,EAAEF,QAAQX,MAAM,GAAG,IAAI,QAAQ,KAAK,UAAU,CAAC,EAAE;oBACtFlF,SAAS;gBACX;gBACA;YACF;YACA,MAAMgG,gBAAgB5G,aAAaC;YACnC,oEAAoE;YACpE,MAAM4G,WAAWvK,MAAMwK,mBAAmB,CAACzF,MAAMpB;YACjD,IAAI4G,YAAYA,aAAaxF,IAAI,CAAC,OAAO,EAAEuF,aAAa,CAAC,OAAO,GAAGC;YACnExG,OAAO0G,MAAM,CACXb,QACAU,eACAtI,qBAAqB2G,cAAcC,SACnC,mEAAmE;YACnE,sCAAsC;YACtCzF,oBACE,SACA4B,MACAuF,eACA/G,yBACA,kEAAkE;YAClE+E,YAAYoC,KAAK,GAAG;gBAAEpC,aAAaA,YAAYI,QAAQ;YAAC,IAAI,CAAC;QAGnE;QACA,IAAIpB,YAAYsC,MAAM,CAAC,QAAQ,GAAGzC,MAAMJ,IAAI,GAAG4D,KAAK,CAAC,GAAGnH;QACxD,IAAI,CAACO,OAAOmG,IAAI,CAACN,QAAQJ,MAAM,EAAE;QACjC3B,UAAU;QACV,IAAI+C,SAAS;QACb,MAAMC,UAAU,MAAMtK,mBACpB;YAAEuK,SAAS;YAAQzF;YAAW0F,YAAY3F,eAAe;QAAQ,GACjE;YACEwF,SAAS,MAAMhH,MAAMgG,QAAQ;QAC/B;QAEF/B,UAAU;QACV,IAAI,CAACgD,QAAQG,EAAE,EAAE;gBACCH;YAAhB7E,iBAAgB6E,mBAAAA,QAAQvB,OAAO,YAAfuB,mBAAmB,gCAAgC;gBACjEvG,SAAS;YACX;YACA;QACF;QACA,IAAI,CAACsG,QAAQ;QACbjD,gBAAgB;QAChBJ,cAAc;QACd,oEAAoE;QACpE,sBAAsB;QACtBsB,UAAU,CAAC;IACb;IAEA,MAAMoC,UAAUjL,MAAMkL,kBAAkB,CAACnG,MAAME;IAC/C,MAAMkG,cACJF,QAAQG,KAAK,KAAK,YACd,CAAC,qBAAqB,EACpBH,QAAQI,SAAS,GAAG,CAAC,IAAI,EAAE,IAAIC,KAAKL,QAAQI,SAAS,EAAEE,kBAAkB,IAAI,GAAG,IAChF,GACFN,QAAQG,KAAK,KAAK,aAChB,uBACA;IAER,qBACE;;0BACE,KAACxI;gBACC4I,MAAK;gBACLC,OAAO3E,OAAO/B,IAAI,CAAC,OAAO,IAAIA,IAAI,CAAC,QAAQ,IAAII;gBAC/C,iEAAiE;gBACjE,yEAAyE;gBACzEuG,UAAU3G,IAAI,CAAC,OAAO,GAAG+B,QAAO/B,cAAAA,IAAI,CAAC,QAAQ,YAAbA,cAAiB,MAAMd;gBACvD0H,MAAMhH;gBACNiH,UAAU3F,OAAO4F,OAAO,CAAC;gBACzBC,WAAU;gBACV,kEAAkE;gBAClE,kEAAkE;gBAClE,+CAA+C;gBAC/CC,SAASzF,YAAYrC,YAAY;oBAAEe;oBAAQc;oBAAK0F,MAAM;oBAAQQ,UAAU7G;gBAAO;gBAC/E8G,uBACE;;wBAKG3F,0BACC;;8CACE,KAAC7F;oCACCgE,WAAWvE;mCACN;oCAAEgM,kBAAkB;oCAASC,cAAc;gCAAM;oCACtDC,MAAMnG,OAAOoG,OAAO,CAACvF,OAAO/B,KAAKyB,kBAAkB;oCACnD8F,MAAK;oCACLhI,SAAQ;8CAEP;;gCAEFS,KAAKwH,SAAS,iBACb,KAAC9L;oCACCgE,WAAWvE;mCACN;oCAAEgM,kBAAkB;oCAASC,cAAc;gCAAM;oCACtDC,MAAMnG,OAAOuG,OAAO,CAACzH,KAAKwH,SAAS;oCACnCD,MAAK;oCACLhI,SAAQ;8CAEP;sCAED;gCACHS,KAAK0H,MAAM,iBACV,KAAChM;oCACCgE,WAAWvE;mCACN;oCAAEgM,kBAAkB;oCAASC,cAAc;gCAAM;oCACtDC,MAAMnG,OAAOyG,IAAI,CAAC3H,KAAK0H,MAAM;oCAC7BH,MAAK;oCACLhI,SAAQ;8CAEP;sCAED;;6BAEJuB,+BACF,KAAC9E;4BAAQ0K,OAAOvH;sCAGd,cAAA,KAACyI;0CACC,cAAA,KAAClM;oCAAO6L,MAAK;oCAAQhI,SAAQ;oCAAYsI,QAAQ;8CAC9C;;;2CAKP,KAACnM;4BAAO6L,MAAK;4BAAQhI,SAAQ;4BAAYuI,SAASrH;sCAC/C;;sCAIL,KAAClD;4BACC0C,QAAQA;4BACRc,KAAKA;4BACLgH,MAAM;gCAAE3H;4BAAO;4BACf4H,OAAOlG,aAAaG;4BACpBE,WAAWA;;sCAGb,KAACrE;4BACCmC,QAAQA;4BACRc,KAAKA;4BACL0F,MAAK;4BACLQ,UAAU7G;4BACV6H,MAAMlG,QAAO/B,aAAAA,IAAI,CAAC,OAAO,YAAZA,aAAgB;4BAC7BkI,OAAOnG,QAAO/B,eAAAA,IAAI,CAAC,QAAQ,YAAbA,eAAiB,OAAO;;sCAExC,KAACjC;4BACCkC,QAAQA;4BACRG,QAAQA;4BACR8H,OAAOnG,QAAO/B,eAAAA,IAAI,CAAC,QAAQ,YAAbA,eAAiB;4BAC/BiI,MAAMlG,QAAO/B,cAAAA,IAAI,CAAC,OAAO,YAAZA,cAAgB;4BAC7B4B,YAAYA;;;;gBAIlBuG,WAAW;uBACLzG,OACA;wBACE;4BACE5C,KAAK;4BACLW,OAAO;4BACP2I,oBAAM,KAAC/M;gCAAQgN,MAAMnN,wBAAwBmN,IAAI;gCAAEd,MAAM;;4BACzDe,aAAa;4BACbR,SAAS,IAAMpH;wBACjB;qBACD,GACD,EAAE;uBACHC;iBACJ;gBACD4H,qBACE;;sCACE,KAACtK;4BAAe+B,MAAMA;4BAAMwI,UAAUhF,aAAaG,QAAQ;;sCAI3D,KAAClG;4BAAkBgL,OAAO7G;;sCAG1B,KAACjE;4BACCsC,QAAQA;4BACRiI,OAAOnG,QAAO/B,eAAAA,IAAI,CAAC,QAAQ,YAAbA,eAAiB;4BAC/B4B,YAAYA;;sCAEd,KAAClE;4BACCuC,QAAQA;4BACRiI,OAAOnG,QAAO/B,eAAAA,IAAI,CAAC,QAAQ,YAAbA,eAAiB;;sCAEjC,KAACpC;4BACC6B,OAAM;4BACNV,OAAOiB,KAAK0I,QAAQ,GAAGlI,OAAOmI,QAAQ,CAAC3I,KAAK0I,QAAQ,IAAIxJ;;wBAEzD2B;;;0BAIL,cAAA,MAAC/E;oBAAMwD,SAAS;;wBACbsB;wBAOAkB,0BACC,KAAC1C;4BAAKK,OAAM;sCACV,cAAA,KAACjC;gCAAawK,OAAOlG;gCAAWK,WAAWA;;6BAE3C;wBACHF,2BACC,KAAC7C;4BAAKK,OAAM;sCACV,cAAA,KAACjC;gCAAawK,OAAO/F;gCAAYE,WAAWA;;6BAE5C;sCACJ,KAAC/C;4BAAKK,OAAM;sCAAqB2G;;sCACjC,MAACtK;4BAAM8M,WAAW;gCAAEC,IAAI;gCAAUC,IAAI;4BAAM;4BAAGxJ,SAAS;;gCACrDiC,0BACC,KAACnC;oCAAKK,OAAM;8CACV,cAAA,MAAC3D;wCAAM8M,WAAU;wCAAMtJ,SAAS;wCAAGyJ,IAAI;4CAAEC,YAAY;wCAAS;;0DAC5D,KAAC/K;gDAAe+B,MAAMA;gDAAMwI,UAAUhF,aAAaG,QAAQ;;0DAC3D,KAAC1H;gDAAWsD,SAAQ;gDAAQC,OAAM;0DAC/BQ,KAAKiJ,aAAa,GACf,CAAC,UAAU,EAAE,IAAI1C,KAAKvG,KAAKiJ,aAAa,EAAEC,cAAc,IAAI,GAC5D;;;;mDAKV,MAACvN;oCAAY4L,MAAK;oCAAQwB,IAAI;wCAAEI,UAAU;oCAAI;;sDAC5C,KAACvN;4CAAWwN,IAAI9G;sDAAgB;;sDAChC,KAACzG;4CACCwN,SAAS/G;4CACT7C,OAAM;4CACNV,OAAO9D,MAAMqO,kBAAkB,CAACtJ,MAAMwD,aAAaG,QAAQ;4CAC3D4F,UAAU,CAACC;gDACT,MAAMC,SAAS/F,cAAcgG,IAAI,CAAC,CAACC,QAAUA,MAAMlK,KAAK,KAAK+J,MAAMI,MAAM,CAAC7K,KAAK;gDAC/E,IAAI,CAAC0K,QAAQ;gDACb,IAAIA,OAAOpI,MAAM,KAAK,eAAe;oDACnCX,YAAY+I,OAAOhK,KAAK;oDACxB;gDACF;gDACA,mDAAmD;gDACnD,yDAAyD;gDACzD,uDAAuD;gDACvD,cAAc;gDACd,KAAKZ,MACH;oDACEwC,QAAQoI,OAAOpI,MAAM;oDACrBwI,aAAaJ,OAAOhK,KAAK;mDACrB4B,WAAW,gBAAgB;oDAAEyI,mBAAmB5N;gDAAc,IAAI,CAAC,IAEzE;4CAEJ;sDAECiC,oBAAoBuF;;;;8CAI3B,KAAC1F;oCACCe,OAAOiB,KAAK0I,QAAQ;oCACpBlI,QAAQA;oCACRuJ,WAAW;oCACXR,UAAU,CAACS,MACT,KAAKnL,MAAM;4CAAE6J,UAAUsB,OAAO9N;wCAAc,GAAG8N,MAAM,mBAAmB;;;;wBAI7E3I,WAAW,iBAAiBrB,KAAK8J,iBAAiB,iBACjD,KAACrO;4BAAMwO,UAAS;sCAAQ,CAAC,aAAa,EAAEjK,KAAK8J,iBAAiB,EAAE;6BAC9D;;;;0BASR,KAAC1O;gBACC8O,QAAQ;gBACRtD,MAAM3L,MAAM4E,cAAc,CAAC,YAAY;oBAAEC,QAAQ;gBAAgB;gBACjEqK,cAAc;gBACdC,cAAc;gBACdC,aAAa;oBACXC,sBACE,MAACxO;wBAAM8M,WAAU;wBAAMtJ,SAAS;;4BAC7BoF,sBACC,KAAChJ;gCAAO6L,MAAK;gCAAQM,UAAUhF;gCAAQiF,SAASnD;0CAC7C;iCAED;0CACJ,KAACjJ;gCACC6L,MAAK;gCACLhI,SAAQ;gCACRuI,SAAS,IAAM,KAAKlD;gCACpBiD,UAAU,CAACnD,SAAS7B;0CAEnBA,SAAS,YAAY;;;;gBAI9B;0BAEA,cAAA,MAAC/G;oBAAMwD,SAAS;;sCACd,KAAClC;4BACCmN,OAAO9H;4BACP8G,UAAUxF;4BACViB,QAAQjC;4BACR8E,UAAUtG;4BACVgC,aAAaA,YAAYI,QAAQ;4BACjC6G,OAAO/G,cAAc+G,KAAK;4BAC1BC,QAAQzK;;wBAOTsD,OAAO+B,MAAM,CAACZ,MAAM,iBACnB;;8CACE,KAACxI;oCAAWsD,SAAQ;8CAAa;;gCAChC+D,OAAO+B,MAAM,CAAChC,GAAG,CAAC,CAACqH,2BAClB,KAACxN;wCAECwN,YAAYA;wCACZ3L,OAAO/B,oBAAoB4G,cAAcC,QAAQ6G,WAAW5L,GAAG;wCAC/DyK,UAAU,CAACxK,QACT+E,UAAU,CAACE,UAAa,aAAKA;oDAAS,CAAC0G,WAAW5L,GAAG,CAAC,EAAEC;;wCAE1D8I,UAAUtG,aAAasB;uCANlB6H,WAAWC,GAAG;;6BAUvB;sCACJ,KAAC5O;4BACCwL,MAAK;4BACL9H,OAAM;4BACNV,OAAOqD;4BACPmH,UAAU,CAACC;gCACTnH,SAASmH,MAAMI,MAAM,CAAC7K,KAAK;gCAC3ByD,cAAc;4BAChB;4BACAoI,SAAS;4BACTC,SAAS;4BACTd,SAAS;4BACTe,WAAW;gCAAEC,WAAW;oCAAEC,WAAWvM;gCAAU;4BAAE;;;;;;;AAM7D;AACAsB,mBAAmBkL,WAAW,GAAG;AAEjC,eAAelL,mBAAkB"}
|
package/src/lib/plugin.js
CHANGED
|
@@ -29,6 +29,7 @@ import { registerPluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
|
|
|
29
29
|
import { registerPluginTransferResourceUi } from "@aglyn/aglyn/plugin-manager/plugin-transfer-resources";
|
|
30
30
|
import { CRM_RECORD_ATTRIBUTION_ZONE } from "./components/crm-attribution-zone.js";
|
|
31
31
|
import { CRM_RECORD_BOOKING_ZONE } from "./components/crm-booking-zone.js";
|
|
32
|
+
import { CRM_RECORD_EMAIL_ACTIONS_ZONE } from "./components/crm-email-actions-zone-token.js";
|
|
32
33
|
/** Code-split: the CRM hub only loads when opened. */ const CrmConsolePage = lazy(()=>import("./components/crm-console-page.js"));
|
|
33
34
|
/** The dashboard glance, split the same way: it loads where the slot mounts it. */ /** Where a form's fields save on a contact, drawn on the form's own page. */ const FormContactFieldsCard = lazy(()=>import("./components/form-contact-fields-card.js"));
|
|
34
35
|
const CrmTasksDueCard = lazy(()=>import("./components/crm-tasks-due-card.js"));
|
|
@@ -79,6 +80,16 @@ const CrmTasksDueCard = lazy(()=>import("./components/crm-tasks-due-card.js"));
|
|
|
79
80
|
}, {
|
|
80
81
|
pluginId: BUNDLE_ID
|
|
81
82
|
});
|
|
83
|
+
registerPluginZone({
|
|
84
|
+
zone: CRM_RECORD_EMAIL_ACTIONS_ZONE,
|
|
85
|
+
label: 'Email a CRM record in its own thread',
|
|
86
|
+
surface: 'console',
|
|
87
|
+
// One control in a contact's or a lead's header, before Send email.
|
|
88
|
+
layout: 'bare',
|
|
89
|
+
description: 'Before Send email in a contact’s or lead’s header. A widget here offers another way to write to the person, such as answering a thread its plugin keeps; it is handed the site, org, record and address, and writes nothing.'
|
|
90
|
+
}, {
|
|
91
|
+
pluginId: BUNDLE_ID
|
|
92
|
+
});
|
|
82
93
|
registerPluginZone({
|
|
83
94
|
zone: CRM_RECORD_ATTRIBUTION_ZONE,
|
|
84
95
|
label: 'Where a CRM record came from',
|
package/src/lib/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/plugin.ts"],"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\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiCardAccountDetailsOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { CRM_CONSOLE_SECTIONS } from './components/crm-console-sections'\nimport { CrmGlanceCard } from './components/crm-glance-card'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerCrmTransferResourceUis } from './transfer/ui'\nimport { withCrmOrgMount } from './hooks/use-crm-org-mount'\nimport { registerCrmRecordCounts } from './model/crm-record-counts'\nimport { registerCrmRecordRoutes } from './model/crm-record-routes'\nimport { registerCrmRecordLists } from './model/crm-record-lists'\nimport { CRM_SEARCH_SOURCES } from './model/crm-search-sources'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { CRM_RECORD_ATTRIBUTION_ZONE } from './components/crm-attribution-zone'\nimport { CRM_RECORD_BOOKING_ZONE } from './components/crm-booking-zone'\n\n/** Code-split: the CRM hub only loads when opened. */\nconst CrmConsolePage = lazy(\n () => import('./components/crm-console-page'),\n)\n/** The dashboard glance, split the same way: it loads where the slot mounts it. */\n/** Where a form's fields save on a contact, drawn on the form's own page. */\nconst FormContactFieldsCard = lazy(\n () => import('./components/form-contact-fields-card'),\n)\n\nconst CrmTasksDueCard = lazy(\n () => import('./components/crm-tasks-due-card'),\n)\n\n/**\n * What a plan without the CRM sees where the glance card would be (AGL-3977):\n * the suite named, with the plan that includes it. One card, not two — the\n * tasks card has nothing to sell that this one does not.\n */\nconst CRM_GLANCE_LOCKED_PREVIEW = {\n description:\n 'Follow every person your site captures as a lead, contact or deal, ' +\n 'with tasks and reports, from one place.',\n} as const\n\n/**\n * CRM feature plugin (AGL-395, the hub since AGL-2595). Console-only — its\n * records live in Firestore and have no canvas element, so there is no UI\n * bundle. The console half declares the CRM nav + hub through the\n * ConsoleExtension registry (release_crm gate via the nav tab); the\n * contacts section reads the `contactsPerHost` quota off the shell-passed\n * `org`.\n *\n * The plugin id is `crm` (AGL-2595). It was `contacts` while the surface was\n * one list; the id is persisted in every org's `enabledPlugins` and every\n * host's `disabledPlugins`, so the runtime read the old value as this one\n * until the stored documents had been rewritten, and the alias was retired\n * once that backfill reported nothing left (AGL-2614). The `/contacts`\n * address is a URL, not a stored id, and the nav item keeps redirecting it.\n */\nexport function registerCrmConsole(): void {\n // How the Import & export hub names email templates in a workspace package (AGL-3535).\n registerPluginTransferResourceUi('crm.email-templates', { label: 'Email templates' }, { pluginId: BUNDLE_ID })\n registerCrmRecordRoutes()\n // How the import wizard and the export dialog name the CRM's records (AGL-3527).\n registerCrmTransferResourceUis(BUNDLE_ID)\n // The CRM's records as another plugin's picker lists them (AGL-3080).\n registerCrmRecordLists()\n // The leads a site captured and the organization's contacts, counted for\n // another plugin's figure.\n registerCrmRecordCounts()\n registerPluginZone(\n {\n zone: CRM_RECORD_BOOKING_ZONE,\n label: 'Book a meeting from a CRM record',\n surface: 'console',\n // One control in a record header's row of actions, and one beside the\n // composer's fields: the page lays them out, so the zone adds nothing.\n layout: 'bare',\n description:\n 'In a record’s header and beside the one-to-one composer. A widget here offers the site’s bookable services and the link a visitor books one at; it is handed the site, the record and, from the composer, a way to insert the chosen link. It writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: CRM_RECORD_ATTRIBUTION_ZONE,\n label: 'Where a CRM record came from',\n surface: 'console',\n description:\n 'On a contact’s page and in a lead’s history. A widget here says which campaign or link brought the person; it is handed the site, what the record is and its id, and it writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'CRM',\n /*\n * WHO may open the CRM, declared so the shell enforces it.\n *\n * `navTabId` below is a release flag, not authorization: it says whether\n * the surface has shipped, and `FeatureGate` reads it as\n * `released || isStaff`. Nothing above this page decided who among a\n * workspace's members may read it, and the page is a full editor over\n * org-shared people data — names, addresses, order history, notes,\n * consent, and a CSV export of all of it.\n *\n * `data.manage` rather than a key of this plugin's own, because it is\n * the catalog key whose subject is exactly this data. The Firestore\n * rules place contacts in the org-shared data block with datasets and\n * gate writes on `canWriteOrgData()` — owner, admin, editor — which is\n * the population `data.manage` defaults to, so the console gate and the\n * rules agree on the built-in roles by construction. A custom role or a\n * per-member override then narrows the console further, and the rules\n * stay the boundary for the data itself.\n *\n * Minting a `contacts.*` key instead would advertise a control that does\n * not exist. Console contact writes are client-direct against the rules,\n * so a new key would have no server enforcement point to sit on, and a\n * permission a customer can untick that changes nothing is worse than\n * its absence.\n *\n * A SITE COLLABORATOR holding it reaches this page deliberately. They\n * are a real org member document, so this gate is the first thing that\n * has ever asked about them here — and the answer is yes, scoped: the\n * listener filters on `visibleTo`, the rules prove the same predicate\n * per document, and every field the page shows is read through the\n * viewing group's own facet. Refusing them outright would delete a\n * shipped capability rather than close a hole.\n */\n permission: 'data.manage',\n /*\n * WHAT the workspace must have bought (AGL-2851). The CRM is included\n * from Starter and a Free workspace has none of it, so the flag is\n * declared once, here, and the shell refuses every surface this\n * registers on a plan without it: the hub body at both mounts, every\n * section and record beneath it, drawn locked beside the upgrade notice,\n * and the dashboard cards. The nav tab stays, as every plan-locked tab\n * does, because it is the way to that notice. The CRM's routes, `/v1`\n * and the Firestore rules ask the same question where the data is\n * (`server/suite-gate.ts`).\n */\n featureFlag: 'crm',\n // What the console's search finds here, under the two gates above.\n searchSources: CRM_SEARCH_SOURCES,\n /*\n * Dashboard cards (AGL-2599, AGL-2604): the site dashboard's `hostDashboard`\n * slot composes this extension's permission over each, so a card appears\n * only where the CRM is enabled and only for a reader who may open it —\n * registered here rather than imported by the dashboard so the shell's\n * enablement and entitlement gate decides whether a site that never turned\n * the CRM on sees them, the rule every card on that row follows. The tasks\n * card is a glance at the reader's own overdue and due-today work and\n * renders nothing on a workspace that has never made a task; the glance\n * card is four server-counted figures, each a link into the hub.\n *\n * Both name `features.crm`, as the extension does (AGL-2611, AGL-2851):\n * the slot composes the two, so a plan without the CRM gets neither card.\n * The glance card declares a `lockedPreview` (AGL-3977), so in its place\n * the shell draws one locked card naming the plan that includes the CRM;\n * the tasks card stays absent.\n *\n * The same two cards again on the organization's `orgDashboard` slot\n * (AGL-2636) — the org's sites page — under the same flag, with the same\n * ids: the id names the card, and it is one card totaling the whole\n * organization there instead of one site. The shell hands that slot the\n * org mount as a prop, and `withCrmOrgMount` is what puts the mount's\n * provider around each card, since the sites page cannot import one.\n */\n widgets: [\n // Which contact field each of a form's fields saves to, on the form's\n // own page. The forms plugin hosts the zone and writes the form\n // document; what a contact's fields ARE is this plugin's, so the card\n // is too.\n {\n slot: 'formContactFields',\n widgetId: 'crm-form-contact-fields',\n title: 'Saves to contact fields',\n // A contact's custom fields are part of the suite, so a workspace\n // without it has none to map a form's field onto.\n featureFlag: 'crm',\n Component: FormContactFieldsCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: CrmTasksDueCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n lockedPreview: CRM_GLANCE_LOCKED_PREVIEW,\n Component: CrmGlanceCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: withCrmOrgMount(CrmTasksDueCard),\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n lockedPreview: CRM_GLANCE_LOCKED_PREVIEW,\n Component: withCrmOrgMount(CrmGlanceCard),\n },\n ],\n navItems: [\n {\n label: 'CRM',\n href: '/crm',\n // The address the surface had while it was one list. A link kept\n // from then — a bookmark, a docs page, an email — still opens the\n // hub rather than the shell's \"not available\" notice.\n legacyHrefs: ['/contacts'],\n // Sections as ROUTES (AGL-2595): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read and a\n // bare `/crm` lands on the first. Every section inherits this\n // item's `release_crm` gate.\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n },\n Component: CrmConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's CRM hub (AGL-2630), at `/[orgSlug]/crm`: the same\n * sections over every site at once, mounted with no site and the org's\n * sites as `orgMount`. Its listeners carry no scope clause, so the\n * shell's org route refuses a site collaborator on REACH before it asks\n * the `data.manage` declared above; a permission alone, which a\n * collaborator's role can carry, never opens it.\n *\n * It carries the SITE tab's id on purpose: `release_contacts` names\n * `nav-tab-contacts`, so one flag holds both halves of the CRM, and the\n * org half cannot ship while the site half is switched off. First among\n * the organization's plugin tabs, beside Data, the org's other shared\n * collection of records. The help opens the docs heading about this\n * mount. Its old address, `/[orgSlug]/contacts`, is a console redirect\n * (`consoleRedirects`), so it needs no `legacyHrefs` here.\n */\n orgNavItems: [\n {\n label: 'CRM',\n href: '/crm',\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n tabOrder: -100,\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n docsAnchor: '#at-the-organization-level',\n },\n Component: CrmConsolePage,\n },\n ],\n })\n}\n"],"names":["Aglyn","mdiCardAccountDetailsOutline","lazy","CRM_CONSOLE_SECTIONS","CrmGlanceCard","BUNDLE_ID","registerCrmTransferResourceUis","withCrmOrgMount","registerCrmRecordCounts","registerCrmRecordRoutes","registerCrmRecordLists","CRM_SEARCH_SOURCES","registerPluginZone","registerPluginTransferResourceUi","CRM_RECORD_ATTRIBUTION_ZONE","CRM_RECORD_BOOKING_ZONE","CrmConsolePage","FormContactFieldsCard","CrmTasksDueCard","CRM_GLANCE_LOCKED_PREVIEW","description","registerCrmConsole","label","pluginId","zone","surface","layout","registerConsoleExtension","displayName","permission","featureFlag","searchSources","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","hostDashboard","lockedPreview","orgDashboard","navItems","href","legacyHrefs","sections","navTabId","icon","path","header","docsTopic","orgNavItems","tabOrder","docsAnchor"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/plugin.ts"],"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\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiCardAccountDetailsOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { CRM_CONSOLE_SECTIONS } from './components/crm-console-sections'\nimport { CrmGlanceCard } from './components/crm-glance-card'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerCrmTransferResourceUis } from './transfer/ui'\nimport { withCrmOrgMount } from './hooks/use-crm-org-mount'\nimport { registerCrmRecordCounts } from './model/crm-record-counts'\nimport { registerCrmRecordRoutes } from './model/crm-record-routes'\nimport { registerCrmRecordLists } from './model/crm-record-lists'\nimport { CRM_SEARCH_SOURCES } from './model/crm-search-sources'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { CRM_RECORD_ATTRIBUTION_ZONE } from './components/crm-attribution-zone'\nimport { CRM_RECORD_BOOKING_ZONE } from './components/crm-booking-zone'\nimport { CRM_RECORD_EMAIL_ACTIONS_ZONE } from './components/crm-email-actions-zone-token'\n\n/** Code-split: the CRM hub only loads when opened. */\nconst CrmConsolePage = lazy(\n () => import('./components/crm-console-page'),\n)\n/** The dashboard glance, split the same way: it loads where the slot mounts it. */\n/** Where a form's fields save on a contact, drawn on the form's own page. */\nconst FormContactFieldsCard = lazy(\n () => import('./components/form-contact-fields-card'),\n)\n\nconst CrmTasksDueCard = lazy(\n () => import('./components/crm-tasks-due-card'),\n)\n\n/**\n * What a plan without the CRM sees where the glance card would be (AGL-3977):\n * the suite named, with the plan that includes it. One card, not two — the\n * tasks card has nothing to sell that this one does not.\n */\nconst CRM_GLANCE_LOCKED_PREVIEW = {\n description:\n 'Follow every person your site captures as a lead, contact or deal, ' +\n 'with tasks and reports, from one place.',\n} as const\n\n/**\n * CRM feature plugin (AGL-395, the hub since AGL-2595). Console-only — its\n * records live in Firestore and have no canvas element, so there is no UI\n * bundle. The console half declares the CRM nav + hub through the\n * ConsoleExtension registry (release_crm gate via the nav tab); the\n * contacts section reads the `contactsPerHost` quota off the shell-passed\n * `org`.\n *\n * The plugin id is `crm` (AGL-2595). It was `contacts` while the surface was\n * one list; the id is persisted in every org's `enabledPlugins` and every\n * host's `disabledPlugins`, so the runtime read the old value as this one\n * until the stored documents had been rewritten, and the alias was retired\n * once that backfill reported nothing left (AGL-2614). The `/contacts`\n * address is a URL, not a stored id, and the nav item keeps redirecting it.\n */\nexport function registerCrmConsole(): void {\n // How the Import & export hub names email templates in a workspace package (AGL-3535).\n registerPluginTransferResourceUi('crm.email-templates', { label: 'Email templates' }, { pluginId: BUNDLE_ID })\n registerCrmRecordRoutes()\n // How the import wizard and the export dialog name the CRM's records (AGL-3527).\n registerCrmTransferResourceUis(BUNDLE_ID)\n // The CRM's records as another plugin's picker lists them (AGL-3080).\n registerCrmRecordLists()\n // The leads a site captured and the organization's contacts, counted for\n // another plugin's figure.\n registerCrmRecordCounts()\n registerPluginZone(\n {\n zone: CRM_RECORD_BOOKING_ZONE,\n label: 'Book a meeting from a CRM record',\n surface: 'console',\n // One control in a record header's row of actions, and one beside the\n // composer's fields: the page lays them out, so the zone adds nothing.\n layout: 'bare',\n description:\n 'In a record’s header and beside the one-to-one composer. A widget here offers the site’s bookable services and the link a visitor books one at; it is handed the site, the record and, from the composer, a way to insert the chosen link. It writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: CRM_RECORD_EMAIL_ACTIONS_ZONE,\n label: 'Email a CRM record in its own thread',\n surface: 'console',\n // One control in a contact's or a lead's header, before Send email.\n layout: 'bare',\n description:\n 'Before Send email in a contact’s or lead’s header. A widget here offers another way to write to the person, such as answering a thread its plugin keeps; it is handed the site, org, record and address, and writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: CRM_RECORD_ATTRIBUTION_ZONE,\n label: 'Where a CRM record came from',\n surface: 'console',\n description:\n 'On a contact’s page and in a lead’s history. A widget here says which campaign or link brought the person; it is handed the site, what the record is and its id, and it writes nothing.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'CRM',\n /*\n * WHO may open the CRM, declared so the shell enforces it.\n *\n * `navTabId` below is a release flag, not authorization: it says whether\n * the surface has shipped, and `FeatureGate` reads it as\n * `released || isStaff`. Nothing above this page decided who among a\n * workspace's members may read it, and the page is a full editor over\n * org-shared people data — names, addresses, order history, notes,\n * consent, and a CSV export of all of it.\n *\n * `data.manage` rather than a key of this plugin's own, because it is\n * the catalog key whose subject is exactly this data. The Firestore\n * rules place contacts in the org-shared data block with datasets and\n * gate writes on `canWriteOrgData()` — owner, admin, editor — which is\n * the population `data.manage` defaults to, so the console gate and the\n * rules agree on the built-in roles by construction. A custom role or a\n * per-member override then narrows the console further, and the rules\n * stay the boundary for the data itself.\n *\n * Minting a `contacts.*` key instead would advertise a control that does\n * not exist. Console contact writes are client-direct against the rules,\n * so a new key would have no server enforcement point to sit on, and a\n * permission a customer can untick that changes nothing is worse than\n * its absence.\n *\n * A SITE COLLABORATOR holding it reaches this page deliberately. They\n * are a real org member document, so this gate is the first thing that\n * has ever asked about them here — and the answer is yes, scoped: the\n * listener filters on `visibleTo`, the rules prove the same predicate\n * per document, and every field the page shows is read through the\n * viewing group's own facet. Refusing them outright would delete a\n * shipped capability rather than close a hole.\n */\n permission: 'data.manage',\n /*\n * WHAT the workspace must have bought (AGL-2851). The CRM is included\n * from Starter and a Free workspace has none of it, so the flag is\n * declared once, here, and the shell refuses every surface this\n * registers on a plan without it: the hub body at both mounts, every\n * section and record beneath it, drawn locked beside the upgrade notice,\n * and the dashboard cards. The nav tab stays, as every plan-locked tab\n * does, because it is the way to that notice. The CRM's routes, `/v1`\n * and the Firestore rules ask the same question where the data is\n * (`server/suite-gate.ts`).\n */\n featureFlag: 'crm',\n // What the console's search finds here, under the two gates above.\n searchSources: CRM_SEARCH_SOURCES,\n /*\n * Dashboard cards (AGL-2599, AGL-2604): the site dashboard's `hostDashboard`\n * slot composes this extension's permission over each, so a card appears\n * only where the CRM is enabled and only for a reader who may open it —\n * registered here rather than imported by the dashboard so the shell's\n * enablement and entitlement gate decides whether a site that never turned\n * the CRM on sees them, the rule every card on that row follows. The tasks\n * card is a glance at the reader's own overdue and due-today work and\n * renders nothing on a workspace that has never made a task; the glance\n * card is four server-counted figures, each a link into the hub.\n *\n * Both name `features.crm`, as the extension does (AGL-2611, AGL-2851):\n * the slot composes the two, so a plan without the CRM gets neither card.\n * The glance card declares a `lockedPreview` (AGL-3977), so in its place\n * the shell draws one locked card naming the plan that includes the CRM;\n * the tasks card stays absent.\n *\n * The same two cards again on the organization's `orgDashboard` slot\n * (AGL-2636) — the org's sites page — under the same flag, with the same\n * ids: the id names the card, and it is one card totaling the whole\n * organization there instead of one site. The shell hands that slot the\n * org mount as a prop, and `withCrmOrgMount` is what puts the mount's\n * provider around each card, since the sites page cannot import one.\n */\n widgets: [\n // Which contact field each of a form's fields saves to, on the form's\n // own page. The forms plugin hosts the zone and writes the form\n // document; what a contact's fields ARE is this plugin's, so the card\n // is too.\n {\n slot: 'formContactFields',\n widgetId: 'crm-form-contact-fields',\n title: 'Saves to contact fields',\n // A contact's custom fields are part of the suite, so a workspace\n // without it has none to map a form's field onto.\n featureFlag: 'crm',\n Component: FormContactFieldsCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: CrmTasksDueCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n lockedPreview: CRM_GLANCE_LOCKED_PREVIEW,\n Component: CrmGlanceCard,\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-tasks-due',\n title: 'Tasks due',\n featureFlag: 'crm',\n Component: withCrmOrgMount(CrmTasksDueCard),\n },\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.orgDashboard,\n widgetId: 'crm-glance',\n title: 'CRM at a glance',\n featureFlag: 'crm',\n lockedPreview: CRM_GLANCE_LOCKED_PREVIEW,\n Component: withCrmOrgMount(CrmGlanceCard),\n },\n ],\n navItems: [\n {\n label: 'CRM',\n href: '/crm',\n // The address the surface had while it was one list. A link kept\n // from then — a bookmark, a docs page, an email — still opens the\n // hub rather than the shell's \"not available\" notice.\n legacyHrefs: ['/contacts'],\n // Sections as ROUTES (AGL-2595): each is a real URL the shell\n // resolves and gates, so the page mounts the one being read and a\n // bare `/crm` lands on the first. Every section inherits this\n // item's `release_crm` gate.\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n },\n Component: CrmConsolePage,\n },\n ],\n /*\n * The ORGANIZATION's CRM hub (AGL-2630), at `/[orgSlug]/crm`: the same\n * sections over every site at once, mounted with no site and the org's\n * sites as `orgMount`. Its listeners carry no scope clause, so the\n * shell's org route refuses a site collaborator on REACH before it asks\n * the `data.manage` declared above; a permission alone, which a\n * collaborator's role can carry, never opens it.\n *\n * It carries the SITE tab's id on purpose: `release_contacts` names\n * `nav-tab-contacts`, so one flag holds both halves of the CRM, and the\n * org half cannot ship while the site half is switched off. First among\n * the organization's plugin tabs, beside Data, the org's other shared\n * collection of records. The help opens the docs heading about this\n * mount. Its old address, `/[orgSlug]/contacts`, is a console redirect\n * (`consoleRedirects`), so it needs no `legacyHrefs` here.\n */\n orgNavItems: [\n {\n label: 'CRM',\n href: '/crm',\n sections: CRM_CONSOLE_SECTIONS,\n navTabId: 'nav-tab-contacts',\n tabOrder: -100,\n icon: { path: mdiCardAccountDetailsOutline.path },\n header: {\n title: 'CRM',\n icon: { path: mdiCardAccountDetailsOutline.path },\n docsTopic: 'crm',\n docsAnchor: '#at-the-organization-level',\n },\n Component: CrmConsolePage,\n },\n ],\n })\n}\n"],"names":["Aglyn","mdiCardAccountDetailsOutline","lazy","CRM_CONSOLE_SECTIONS","CrmGlanceCard","BUNDLE_ID","registerCrmTransferResourceUis","withCrmOrgMount","registerCrmRecordCounts","registerCrmRecordRoutes","registerCrmRecordLists","CRM_SEARCH_SOURCES","registerPluginZone","registerPluginTransferResourceUi","CRM_RECORD_ATTRIBUTION_ZONE","CRM_RECORD_BOOKING_ZONE","CRM_RECORD_EMAIL_ACTIONS_ZONE","CrmConsolePage","FormContactFieldsCard","CrmTasksDueCard","CRM_GLANCE_LOCKED_PREVIEW","description","registerCrmConsole","label","pluginId","zone","surface","layout","registerConsoleExtension","displayName","permission","featureFlag","searchSources","widgets","slot","widgetId","title","Component","CONSOLE_WIDGET_SLOTS","hostDashboard","lockedPreview","orgDashboard","navItems","href","legacyHrefs","sections","navTabId","icon","path","header","docsTopic","orgNavItems","tabOrder","docsAnchor"],"mappings":"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"}
|