@aglyn/plugins-crm 1.0.0-beta.178 → 1.0.0-beta.179
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/lib/components/contact-detail-page.js +1 -0
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-properties-card.d.ts +6 -0
- package/src/lib/components/contact-properties-card.js +25 -2
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/csv-import-drawer.d.ts +7 -0
- package/src/lib/components/csv-import-drawer.js +7 -0
- package/src/lib/components/csv-import-drawer.js.map +1 -1
- package/src/lib/components/fields-section.js +6 -0
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-import-drawer.d.ts +3 -0
- package/src/lib/components/lead-import-drawer.js +29 -6
- package/src/lib/components/lead-import-drawer.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +10 -12
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-source-select.d.ts +30 -0
- package/src/lib/components/lead-source-select.js +96 -0
- package/src/lib/components/lead-source-select.js.map +1 -0
- package/src/lib/components/lead-source-values-card.d.ts +27 -0
- package/src/lib/components/lead-source-values-card.js +606 -0
- package/src/lib/components/lead-source-values-card.js.map +1 -0
- package/src/lib/components/leads-section.js +100 -4
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-lead-drawer.js +27 -11
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/hooks/use-lead-source-picklist.d.ts +23 -0
- package/src/lib/hooks/use-lead-source-picklist.js +50 -0
- package/src/lib/hooks/use-lead-source-picklist.js.map +1 -0
- package/src/lib/model/contact-record.d.ts +2 -0
- package/src/lib/model/contact-record.js +2 -1
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-update.d.ts +6 -0
- package/src/lib/model/contact-update.js +1 -0
- package/src/lib/model/contact-update.js.map +1 -1
- package/src/lib/model/crm-lead-import.d.ts +10 -2
- package/src/lib/model/crm-lead-import.js +19 -1
- package/src/lib/model/crm-lead-import.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +13 -0
- package/src/lib/model/lead-filters.js +17 -1
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/lead-source-values.d.ts +82 -0
- package/src/lib/model/lead-source-values.js +149 -0
- package/src/lib/model/lead-source-values.js.map +1 -0
- package/src/lib/server/contact-update.js +25 -2
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/lead-create.js +19 -0
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-source-picklist.d.ts +42 -0
- package/src/lib/server/lead-source-picklist.js +52 -0
- package/src/lib/server/lead-source-picklist.js.map +1 -0
- package/src/lib/server/lead-source-values.d.ts +50 -0
- package/src/lib/server/lead-source-values.js +187 -0
- package/src/lib/server/lead-source-values.js.map +1 -0
- package/src/lib/server/leads-import.js +31 -2
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
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.179",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,18 +25,18 @@
|
|
|
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-email-campaigns": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
37
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.179",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.179",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.179",
|
|
31
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.179",
|
|
32
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.179",
|
|
33
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.179",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.179",
|
|
35
|
+
"@aglyn/shared-util-email": "1.0.0-beta.179",
|
|
36
|
+
"@aglyn/shared-util-http": "1.0.0-beta.179",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.179",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.179",
|
|
39
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.179",
|
|
40
40
|
"@dnd-kit/core": "^6.3.1",
|
|
41
41
|
"@dnd-kit/utilities": "^3.2.2",
|
|
42
42
|
"@swc/helpers": "0.5.23",
|
|
@@ -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 { 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 {\n arrayRemove,\n deleteDoc,\n deleteField,\n doc,\n updateDoc,\n} from 'firebase/firestore'\nimport { useParams, useRouter } from 'next/navigation'\nimport { useCallback, useMemo } from 'react'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { CrmCreateSiteDefault, useCrmOrgMount } from '../hooks/use-crm-org-mount'\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 ContactCustomFieldsCard from './contact-custom-fields-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 { 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 { RecordTasksCard } from './record-tasks-card'\nimport { useEmailsHubPath } from './use-emails-hub-path'\nimport { useOrgMembers } from './use-org-members'\n\nconst contactDocsHelp = pluginDocsHelp('contacts', {\n anchor: '#the-contacts-page',\n excerpt:\n \"One person's record: the profile your team keeps on them, where they \" +\n 'came from, what they are filed under, and what the site recorded.',\n})\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), the relationship (sources, attribution,\n * consent, campaign filing) and the timeline. Other CRM sections add a card\n * here by inserting one import and 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 — `planContactDetach` does the counting off `visibleTo`, which\n * is what both enforcement layers evaluate, and the rules refuse a delete by\n * a caller who is not the sole holder. Not the erasure path: a privacy\n * erasure 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. The count on the header is the number; the\n * list is the rows. Built from the route params already in the URL, so\n * no document is read to draw a link, and absent off a site — the\n * org-level mount has no orders list to point at.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const ordersHref =\n params?.orgSlug && params?.host && record?.email\n ? Aglyn.siteRecordLinks({\n orgSlug: String(params.orgSlug),\n host: String(params.host),\n }).ordersByCustomer(record.email)\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 ? Aglyn.siteRecordLinks({\n orgSlug: String(params.orgSlug),\n host: siteSlug,\n }).bookingsByBooker(record.email)\n : null\n\n /*\n * Where a campaign entry on the timeline links to: the email's own report\n * on this site's Emails hub (AGL-2616). Only a campaign THIS site sent can\n * be addressed — a sibling site in the same consent group has an Emails\n * hub of its own under a subdomain this page does not know — so the\n * builder answers `null` for those and the entry draws unlinked.\n */\n const emailsHub = useEmailsHubPath()\n const campaignHref = useCallback(\n (email: Aglyn.ContactCampaignEmail) =>\n emailsHub && email.hostId === hostId\n ? `${emailsHub}/messages/${encodeURIComponent(email.campaignId)}`\n : null,\n [emailsHub, hostId],\n )\n\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 const ref = doc(firestore, scope[0], scope[1], 'contacts', id)\n const plan = Aglyn.planContactDetach(row, consentGroup)\n if (plan.action === 'delete') {\n await deleteDoc(ref)\n } else {\n await updateDoc(ref, {\n ...Object.fromEntries(plan.remove.map((path) => [path, deleteField()])),\n visibleTo: arrayRemove(...plan.removeTokens),\n capturedByHostIds: arrayRemove(...plan.removeHostIds),\n updatedAt: new Date(),\n })\n }\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 plan.action === 'delete'\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 consentGroup,\n enqueueSnackbar,\n firestore,\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 phone={record.phone}\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 <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 <ContactPropertiesCard\n hostId={hostId}\n org={org}\n record={record}\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 {/*\n On a plan without the CRM, the cards that only work a record —\n custom fields, deals, tasks, files — are named in the notice above\n rather than drawn as four upsells in a row.\n */}\n {suiteIncluded ? (\n <ContactCustomFieldsCard hostId={hostId} org={org} contactId={id} basePath={basePath} />\n ) : null}\n <ContactTimelineCard\n hostId={hostId}\n org={org}\n contactId={id}\n contact={row}\n campaignHref={campaignHref}\n suiteLocked={!suiteIncluded}\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","mdiDeleteOutline","mdiMerge","AppLink","MdiIcon","useConfirmationContext","useSnackbar","useFirestore","useFirestoreDoc","Stack","Tooltip","Typography","arrayRemove","deleteDoc","deleteField","doc","updateDoc","useParams","useRouter","useCallback","useMemo","useCrmActivityLogger","CrmCreateSiteDefault","useCrmOrgMount","useCrmScope","contactPrimaryGroup","contactRecordFromDoc","crmRoutes","ContactAssociationsCard","ContactCustomFieldsCard","ContactDuplicatesCard","ContactKnownByCard","ContactMergeDialog","useContactMergeDialog","ContactPropertiesCard","ContactTimelineCard","AddToListButton","useBookingDoor","CrmSendEmailButton","ContactDealsCard","CrmCallButton","CrmEmailStateChip","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","CrmSuiteNotice","crmSuiteIncluded","crmSuiteLockedReason","useErasePersonAction","RecordFilesCard","RecordTasksCard","useEmailsHubPath","useOrgMembers","contactDocsHelp","anchor","excerpt","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","siteRecordLinks","String","ordersByCustomer","bookingDoor","siteSlug","siteSubdomain","bookingsHref","open","bookingsByBooker","emailsHub","campaignHref","encodeURIComponent","campaignId","handleRemove","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","ref","plan","planContactDetach","action","Object","fromEntries","remove","map","path","removeTokens","capturedByHostIds","removeHostIds","updatedAt","Date","type","name","variant","persist","push","section","error","console","allowDuplicate","overflowItems","key","label","icon","size","disabled","disabledReason","onClick","destructive","erase","subject","kind","requestedAtMs","readErasureRequestedAtMs","undefined","help","backHref","backLabel","spacing","subtitle","alternateEmails","join","actions","contactId","link","phone","suiteLocked","emailState","menuItems","booking","recordId","loading","chips","state","value","lifecycleStage","ownerUid","memberName","lastEmailEngagementAtMs","toLocaleString","span","activityTimeLabel","now","ordersCount","href","underline","ltvCents","toFixed","banner","taskLink","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 { 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 {\n arrayRemove,\n deleteDoc,\n deleteField,\n doc,\n updateDoc,\n} from 'firebase/firestore'\nimport { useParams, useRouter } from 'next/navigation'\nimport { useCallback, useMemo } from 'react'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { CrmCreateSiteDefault, useCrmOrgMount } from '../hooks/use-crm-org-mount'\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 ContactCustomFieldsCard from './contact-custom-fields-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 { 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 { RecordTasksCard } from './record-tasks-card'\nimport { useEmailsHubPath } from './use-emails-hub-path'\nimport { useOrgMembers } from './use-org-members'\n\nconst contactDocsHelp = pluginDocsHelp('contacts', {\n anchor: '#the-contacts-page',\n excerpt:\n \"One person's record: the profile your team keeps on them, where they \" +\n 'came from, what they are filed under, and what the site recorded.',\n})\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), the relationship (sources, attribution,\n * consent, campaign filing) and the timeline. Other CRM sections add a card\n * here by inserting one import and 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 — `planContactDetach` does the counting off `visibleTo`, which\n * is what both enforcement layers evaluate, and the rules refuse a delete by\n * a caller who is not the sole holder. Not the erasure path: a privacy\n * erasure 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. The count on the header is the number; the\n * list is the rows. Built from the route params already in the URL, so\n * no document is read to draw a link, and absent off a site — the\n * org-level mount has no orders list to point at.\n */\n const params = useParams<{ orgSlug?: string; host?: string }>()\n const ordersHref =\n params?.orgSlug && params?.host && record?.email\n ? Aglyn.siteRecordLinks({\n orgSlug: String(params.orgSlug),\n host: String(params.host),\n }).ordersByCustomer(record.email)\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 ? Aglyn.siteRecordLinks({\n orgSlug: String(params.orgSlug),\n host: siteSlug,\n }).bookingsByBooker(record.email)\n : null\n\n /*\n * Where a campaign entry on the timeline links to: the email's own report\n * on this site's Emails hub (AGL-2616). Only a campaign THIS site sent can\n * be addressed — a sibling site in the same consent group has an Emails\n * hub of its own under a subdomain this page does not know — so the\n * builder answers `null` for those and the entry draws unlinked.\n */\n const emailsHub = useEmailsHubPath()\n const campaignHref = useCallback(\n (email: Aglyn.ContactCampaignEmail) =>\n emailsHub && email.hostId === hostId\n ? `${emailsHub}/messages/${encodeURIComponent(email.campaignId)}`\n : null,\n [emailsHub, hostId],\n )\n\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 const ref = doc(firestore, scope[0], scope[1], 'contacts', id)\n const plan = Aglyn.planContactDetach(row, consentGroup)\n if (plan.action === 'delete') {\n await deleteDoc(ref)\n } else {\n await updateDoc(ref, {\n ...Object.fromEntries(plan.remove.map((path) => [path, deleteField()])),\n visibleTo: arrayRemove(...plan.removeTokens),\n capturedByHostIds: arrayRemove(...plan.removeHostIds),\n updatedAt: new Date(),\n })\n }\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 plan.action === 'delete'\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 consentGroup,\n enqueueSnackbar,\n firestore,\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 phone={record.phone}\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 <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 <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 {/*\n On a plan without the CRM, the cards that only work a record —\n custom fields, deals, tasks, files — are named in the notice above\n rather than drawn as four upsells in a row.\n */}\n {suiteIncluded ? (\n <ContactCustomFieldsCard hostId={hostId} org={org} contactId={id} basePath={basePath} />\n ) : null}\n <ContactTimelineCard\n hostId={hostId}\n org={org}\n contactId={id}\n contact={row}\n campaignHref={campaignHref}\n suiteLocked={!suiteIncluded}\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","mdiDeleteOutline","mdiMerge","AppLink","MdiIcon","useConfirmationContext","useSnackbar","useFirestore","useFirestoreDoc","Stack","Tooltip","Typography","arrayRemove","deleteDoc","deleteField","doc","updateDoc","useParams","useRouter","useCallback","useMemo","useCrmActivityLogger","CrmCreateSiteDefault","useCrmOrgMount","useCrmScope","contactPrimaryGroup","contactRecordFromDoc","crmRoutes","ContactAssociationsCard","ContactCustomFieldsCard","ContactDuplicatesCard","ContactKnownByCard","ContactMergeDialog","useContactMergeDialog","ContactPropertiesCard","ContactTimelineCard","AddToListButton","useBookingDoor","CrmSendEmailButton","ContactDealsCard","CrmCallButton","CrmEmailStateChip","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","CrmSuiteNotice","crmSuiteIncluded","crmSuiteLockedReason","useErasePersonAction","RecordFilesCard","RecordTasksCard","useEmailsHubPath","useOrgMembers","contactDocsHelp","anchor","excerpt","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","siteRecordLinks","String","ordersByCustomer","bookingDoor","siteSlug","siteSubdomain","bookingsHref","open","bookingsByBooker","emailsHub","campaignHref","encodeURIComponent","campaignId","handleRemove","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","ref","plan","planContactDetach","action","Object","fromEntries","remove","map","path","removeTokens","capturedByHostIds","removeHostIds","updatedAt","Date","type","name","variant","persist","push","section","error","console","allowDuplicate","overflowItems","key","label","icon","size","disabled","disabledReason","onClick","destructive","erase","subject","kind","requestedAtMs","readErasureRequestedAtMs","undefined","help","backHref","backLabel","spacing","subtitle","alternateEmails","join","actions","contactId","link","phone","suiteLocked","emailState","menuItems","booking","recordId","loading","chips","state","value","lifecycleStage","ownerUid","memberName","lastEmailEngagementAtMs","toLocaleString","span","activityTimeLabel","now","ordersCount","href","underline","ltvCents","toFixed","banner","taskLink","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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"}
|
|
@@ -8,6 +8,12 @@ export interface ContactPropertiesCardProps {
|
|
|
8
8
|
org?: Partial<AglynOrgBilling> | null;
|
|
9
9
|
/** The row, flattened through the viewing group's facet. */
|
|
10
10
|
record: ContactRecord;
|
|
11
|
+
/**
|
|
12
|
+
* The org the record belongs to, as the page resolved it — for the org's
|
|
13
|
+
* lead source values (AGL-3298). Null while it settles, when the select
|
|
14
|
+
* offers the starter list and the save still sends only what changed.
|
|
15
|
+
*/
|
|
16
|
+
orgId?: string | null;
|
|
11
17
|
/** The controller whose facet the edits are saved into. */
|
|
12
18
|
consentGroup: ConsentGroup;
|
|
13
19
|
/**
|
|
@@ -26,11 +26,13 @@ import { Button, Grid, IconButton, InputAdornment, MenuItem, Stack, TextField, T
|
|
|
26
26
|
import { useCallback, useEffect, useState } from "react";
|
|
27
27
|
import { useContactUpdate } from "../hooks/use-contact-update.js";
|
|
28
28
|
import { useCrmActivityLogger } from "../hooks/use-crm-activity-logger.js";
|
|
29
|
+
import { useLeadSourcePicklist } from "../hooks/use-lead-source-picklist.js";
|
|
29
30
|
import { parseContactTags } from "../model/contact-record.js";
|
|
30
31
|
import { setContactStage } from "../model/crm-api.js";
|
|
31
32
|
import { CompanyPicker, useCompanyOptions, useCreateCompany } from "./company-picker.js";
|
|
32
33
|
import { addressDraftFrom, ContactAddressFields } from "./contact-address-fields.js";
|
|
33
34
|
import { crmSuiteLockedReason } from "./crm-suite-lock.js";
|
|
35
|
+
import { LeadSourceSelect } from "./lead-source-select.js";
|
|
34
36
|
/**
|
|
35
37
|
* THE RECORD ITSELF: every field a team keeps on a person, in one card with
|
|
36
38
|
* one Save (AGL-2596).
|
|
@@ -85,7 +87,7 @@ import { crmSuiteLockedReason } from "./crm-suite-lock.js";
|
|
|
85
87
|
* from before the picker — keeps it as the label, and the picker offers it
|
|
86
88
|
* as the company to link or create.
|
|
87
89
|
*/ export function ContactPropertiesCard(props) {
|
|
88
|
-
const { hostId, org, record, consentGroup, seed, members, suiteLocked = false } = props;
|
|
90
|
+
const { hostId, org, record, orgId = null, consentGroup, seed, members, suiteLocked = false } = props;
|
|
89
91
|
const { data: user } = useUser();
|
|
90
92
|
const { enqueueSnackbar } = useSnackbar();
|
|
91
93
|
const contactUpdate = useContactUpdate(hostId);
|
|
@@ -109,6 +111,9 @@ import { crmSuiteLockedReason } from "./crm-suite-lock.js";
|
|
|
109
111
|
const [nameOverride, setNameOverride] = useState(record.nameOverride);
|
|
110
112
|
const [phone, setPhone] = useState(record.phone);
|
|
111
113
|
const [jobTitle, setJobTitle] = useState(record.jobTitle);
|
|
114
|
+
// Salesforce's Lead Source (AGL-3298), carried from the lead on conversion.
|
|
115
|
+
const [leadSource, setLeadSource] = useState(record.leadSource);
|
|
116
|
+
const leadSources = useLeadSourcePicklist(orgId);
|
|
112
117
|
const [companyName, setCompanyName] = useState(record.companyName);
|
|
113
118
|
const [companyId, setCompanyId] = useState(record.companyId || null);
|
|
114
119
|
const [ownerUid, setOwnerUid] = useState(record.ownerUid);
|
|
@@ -128,6 +133,7 @@ import { crmSuiteLockedReason } from "./crm-suite-lock.js";
|
|
|
128
133
|
setNameOverride(record.nameOverride);
|
|
129
134
|
setPhone(record.phone);
|
|
130
135
|
setJobTitle(record.jobTitle);
|
|
136
|
+
setLeadSource(record.leadSource);
|
|
131
137
|
setCompanyName(record.companyName);
|
|
132
138
|
setCompanyId(record.companyId || null);
|
|
133
139
|
setOwnerUid(record.ownerUid);
|
|
@@ -154,7 +160,10 @@ import { crmSuiteLockedReason } from "./crm-suite-lock.js";
|
|
|
154
160
|
const set = _extends({
|
|
155
161
|
name: nameOverride.trim().slice(0, 120),
|
|
156
162
|
phone: normalizedPhone != null ? normalizedPhone : '',
|
|
157
|
-
jobTitle: jobTitle.trim().slice(0, 120)
|
|
163
|
+
jobTitle: jobTitle.trim().slice(0, 120)
|
|
164
|
+
}, leadSource !== record.leadSource ? {
|
|
165
|
+
leadSource
|
|
166
|
+
} : {}, {
|
|
158
167
|
address,
|
|
159
168
|
tags: parseContactTags(tags),
|
|
160
169
|
notes: notes.slice(0, 2000)
|
|
@@ -214,6 +223,7 @@ import { crmSuiteLockedReason } from "./crm-suite-lock.js";
|
|
|
214
223
|
enqueueSnackbar,
|
|
215
224
|
siteHostId,
|
|
216
225
|
jobTitle,
|
|
226
|
+
leadSource,
|
|
217
227
|
lifecycleStage,
|
|
218
228
|
logActivity,
|
|
219
229
|
nameOverride,
|
|
@@ -222,6 +232,7 @@ import { crmSuiteLockedReason } from "./crm-suite-lock.js";
|
|
|
222
232
|
phone,
|
|
223
233
|
record.$id,
|
|
224
234
|
record.email,
|
|
235
|
+
record.leadSource,
|
|
225
236
|
record.lifecycleStage,
|
|
226
237
|
record.name,
|
|
227
238
|
seed.fromCache,
|
|
@@ -354,6 +365,18 @@ import { crmSuiteLockedReason } from "./crm-suite-lock.js";
|
|
|
354
365
|
fullWidth: true
|
|
355
366
|
})
|
|
356
367
|
}),
|
|
368
|
+
/*#__PURE__*/ _jsx(Grid, {
|
|
369
|
+
size: {
|
|
370
|
+
xs: 12,
|
|
371
|
+
sm: 6
|
|
372
|
+
},
|
|
373
|
+
children: /*#__PURE__*/ _jsx(LeadSourceSelect, {
|
|
374
|
+
picklist: leadSources.picklist,
|
|
375
|
+
value: leadSource,
|
|
376
|
+
stored: record.leadSource,
|
|
377
|
+
onChange: setLeadSource
|
|
378
|
+
})
|
|
379
|
+
}),
|
|
357
380
|
/*#__PURE__*/ _jsx(Grid, {
|
|
358
381
|
size: {
|
|
359
382
|
xs: 12,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-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 {\n type AglynOrgBilling,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ConsentGroup,\n type ContactLifecycleStage,\n crmTelHref,\n normalizePhone,\n} from '@aglyn/aglyn'\nimport { mdiPhoneOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Grid,\n IconButton,\n InputAdornment,\n MenuItem,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { type ContactRecord, parseContactTags } from '../model/contact-record'\nimport type { ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n CompanyPicker,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n addressDraftFrom,\n ContactAddressFields,\n type AddressDraft,\n} from './contact-address-fields'\nimport { crmSuiteLockedReason } from './crm-suite-lock'\nimport type { OrgMembers } from './use-org-members'\n\nexport interface ContactPropertiesCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /** The controller whose facet the edits are saved into. */\n consentGroup: ConsentGroup\n /**\n * The listener's verdict on the row the drafts were seeded from, for the\n * guard that refuses a save over an unconfirmed read.\n */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The team, for the owner picker and the owner's name. */\n members: OrgMembers\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The owner, the lifecycle stage and the company\n * show, locked, and are left out of a save; `crm/contact-update` refuses\n * such a plan the rest of the profile, the tags and the notes too.\n */\n suiteLocked?: boolean\n}\n\n/**\n * THE RECORD ITSELF: every field a team keeps on a person, in one card with\n * one Save (AGL-2596).\n *\n * ## One save, one request, one guard\n *\n * A field-at-a-time save would be nine requests and nine chances for the\n * stale-seed guard to refuse one of them — so the card holds a draft of the\n * whole profile and sends it once. The guard WRAPS the send: a row seeded\n * from the cache or from a failed read is refused with the message the guard\n * chooses, and what was typed stays in the fields, because a refusal that\n * also emptied the form would read as a save that worked.\n *\n * ## The server writes the facet (AGL-2804)\n *\n * Every field here lives in THIS holder's facet, and the Firestore rules\n * cannot tell one field of a facet from another, so the card writes nothing\n * itself. The draft goes to `crm/contact-update`, which writes each field by\n * dotted path into the holder's facet — never a nested object, which would\n * take every other holder's records with it — clears a blank field rather\n * than storing it empty, keeps the phone's and the company's search echoes\n * at the top of the document, and refuses every field to a plan without\n * the CRM. A refusal is shown in the route's own words.\n *\n * ## The stage goes to its own route\n *\n * The lifecycle stage is the one field an automation listens for:\n * `contactStageChanged` (AGL-2605) is emitted by `crm/contact-stage` after it\n * performs the write, and by nothing else. So when the stage the reader\n * picked differs from the one the record holds, it is sent to that route\n * once the rest of the profile has landed — the stage picked, or `null` for\n * \"Not placed yet\", which clears it and announces nothing. A refusal from\n * that route is reported on its own, with its sentence, after the fields that\n * did save.\n *\n * ## The name is an override\n *\n * The canonical name is shared by every site holding the person, so this\n * card never edits it. What it saves is this holder's own name for them,\n * and the helper says what a blank falls back to, so nobody clears the\n * field expecting the record to go nameless.\n *\n * ## Company is a record, with its name kept beside it\n *\n * The Company field is the picker (AGL-2613): the link is `companyId` in\n * this holder's facet, mirrored into the top-level `companyIds` for the\n * company page's query, and the company's contacts count moves with it —\n * all planned by the route from the stored document, so this card never\n * reasons about another holder's link. The name is sent too, from the picked\n * company, because the list column and the global search read the name and\n * not the id; a record that carries a name with no link — an import, a save\n * from before the picker — keeps it as the label, and the picker offers it\n * as the company to link or create.\n */\nexport function ContactPropertiesCard(props: ContactPropertiesCardProps) {\n const {\n hostId,\n org,\n record,\n consentGroup,\n seed,\n members,\n suiteLocked = false,\n } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n // The site a stage move is routed through: the mounted one, or at the\n // organization level the holder's own (AGL-2630).\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 companies = useCompanyOptions({ hostId, org })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [nameOverride, setNameOverride] = useState(record.nameOverride)\n const [phone, setPhone] = useState(record.phone)\n const [jobTitle, setJobTitle] = useState(record.jobTitle)\n const [companyName, setCompanyName] = useState(record.companyName)\n const [companyId, setCompanyId] = useState<string | null>(\n record.companyId || null,\n )\n const [ownerUid, setOwnerUid] = useState(record.ownerUid)\n const [lifecycleStage, setLifecycleStage] = useState<ContactLifecycleStage | ''>(\n record.lifecycleStage,\n )\n const [tags, setTags] = useState(record.tags.join(', '))\n const [notes, setNotes] = useState(record.notes)\n const [address, setAddress] = useState<AddressDraft>(\n addressDraftFrom(record.address),\n )\n const [phoneError, setPhoneError] = useState('')\n const [saving, setSaving] = useState(false)\n\n /*\n * Re-seeded when the RECORD changes, not when it re-renders. The listener\n * delivers a fresh row on every snapshot, including the one this card's\n * own save produces; re-seeding on each would overwrite what somebody is\n * in the middle of typing with what the server last confirmed.\n */\n const recordId = record.$id\n useEffect(() => {\n setNameOverride(record.nameOverride)\n setPhone(record.phone)\n setJobTitle(record.jobTitle)\n setCompanyName(record.companyName)\n setCompanyId(record.companyId || null)\n setOwnerUid(record.ownerUid)\n setLifecycleStage(record.lifecycleStage)\n setTags(record.tags.join(', '))\n setNotes(record.notes)\n setAddress(addressDraftFrom(record.address))\n setPhoneError('')\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n const handleSave = useCallback(async () => {\n const trimmedPhone = phone.trim()\n const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : ''\n if (trimmedPhone && !normalizedPhone) {\n setPhoneError('Enter it with its country code, like +1 512 555 0107.')\n return\n }\n setPhoneError('')\n // A stage that differs from the record's is the stage route's to write —\n // see the note above.\n const stageChanged = !suiteLocked && lifecycleStage !== record.lifecycleStage\n const set: ContactUpdateFields = {\n name: nameOverride.trim().slice(0, 120),\n phone: normalizedPhone ?? '',\n jobTitle: jobTitle.trim().slice(0, 120),\n address,\n tags: parseContactTags(tags),\n notes: notes.slice(0, 2000),\n // The owner and the company, sent only on a plan that carries the CRM.\n ...(suiteLocked\n ? {}\n : { ownerUid, companyId, companyName: companyName.trim().slice(0, 120) }),\n }\n setSaving(true)\n try {\n const verdict = await writeGuardedBySeed(\n {\n subject: 'contact',\n unreadable: seed.status === 'error',\n fromCache: seed.fromCache,\n },\n () => contactUpdate.updateOne(record.$id, set),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n logActivity('Updated contact', {\n type: 'contact',\n id: record.$id,\n name: record.name || record.email,\n })\n if (stageChanged && siteHostId) {\n try {\n await setContactStage(user, siteHostId, record.$id, lifecycleStage || null)\n } catch (error) {\n // The rest of the profile is saved; only the stage was refused, and\n // the route's own sentence says why.\n return void enqueueSnackbar(\n `Saved, but the stage could not be changed: ${\n error instanceof Error ? error.message : 'the request failed'\n }`,\n { variant: 'warning', persist: false },\n )\n }\n }\n enqueueSnackbar('Contact saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n } finally {\n setSaving(false)\n }\n }, [\n address,\n companyId,\n companyName,\n contactUpdate,\n enqueueSnackbar,\n siteHostId,\n jobTitle,\n lifecycleStage,\n logActivity,\n nameOverride,\n notes,\n ownerUid,\n phone,\n record.$id,\n record.email,\n record.lifecycleStage,\n record.name,\n seed.fromCache,\n seed.status,\n suiteLocked,\n tags,\n user,\n ])\n\n /*\n * An owner the roster no longer lists — somebody who left the team — is\n * still offered as the current value, named by their uid, so the select is\n * never handed a value it has no option for and the reader can see who it\n * was before reassigning.\n */\n const ownerKnown = members.options.some((option) => option.uid === ownerUid)\n\n /** The number in the box as something a dialer takes, or nothing (AGL-2661). */\n const telHref = crmTelHref(phone)\n\n return (\n <CardDisplay\n header={'Properties'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#the-record-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={saving}\n onClick={() => void handleSave()}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n ),\n }}\n >\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Email\"\n value={record.email}\n slotProps={{ input: { readOnly: true } }}\n helperText=\"The identity every site shares — it cannot be edited here.\"\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Name\"\n value={nameOverride}\n onChange={(event) => setNameOverride(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n helperText={\n record.canonicalName\n ? `Your own name for this person. Blank shows the name they gave: ${record.canonicalName}.`\n : 'Your own name for this person.'\n }\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={Boolean(phoneError)}\n helperText={phoneError || 'With the country code, like +1 512 555 0107'}\n fullWidth\n slotProps={{\n input: {\n /*\n * Click-to-call (AGL-2661), off what is IN the box rather\n * than off the saved value: the number on screen is the one\n * a reader means to ring, and `crmTelHref` withholds the\n * link from anything half-typed.\n */\n endAdornment: telHref ? (\n <InputAdornment position=\"end\">\n <Tooltip title={`Call ${phone.trim()}`}>\n <IconButton\n component=\"a\"\n href={telHref}\n size=\"small\"\n edge=\"end\"\n aria-label={`Call ${phone.trim()}`}\n >\n <MdiIcon path={mdiPhoneOutline.path} size={0.8} />\n </IconButton>\n </Tooltip>\n </InputAdornment>\n ) : null,\n },\n }}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => setJobTitle(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={companyId}\n onChange={(id, company) => {\n setCompanyId(id)\n // The label follows the link: the picked name, or nothing\n // once the link is cleared. A company the list cannot name\n // keeps whatever label the record already had.\n setCompanyName(company ? company.name : id ? companyName : '')\n }}\n onCreate={createCompany}\n email={record.email}\n fallbackName={companyName}\n disabled={saving || suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n setLifecycleStage(event.target.value as ContactLifecycleStage | '')\n }\n disabled={suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n fullWidth\n >\n <MenuItem value=\"\">{'Not placed yet'}</MenuItem>\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => setOwnerUid(event.target.value)}\n disabled={suiteLocked}\n helperText={\n suiteLocked\n ? crmSuiteLockedReason()\n : members.ready && !members.options.length\n ? 'The team roster could not be read, so nobody can be picked yet.'\n : 'The team member responsible for this relationship'\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Unassigned'}</MenuItem>\n {ownerUid && !ownerKnown ? (\n <MenuItem value={ownerUid}>{members.memberName(ownerUid)}</MenuItem>\n ) : null}\n {members.options.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {Aglyn.crmMemberPickerLabel(owner)}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => setTags(event.target.value)}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n <ContactAddressFields value={address} onChange={setAddress} />\n </Stack>\n </Grid>\n <Grid size={{ xs: 12 }}>\n <TextField\n size=\"small\"\n label=\"About\"\n value={notes}\n onChange={(event) => setNotes(event.target.value)}\n helperText=\"Notes for your team. Nobody outside this site's group can read them.\"\n multiline\n minRows={3}\n fullWidth\n />\n </Grid>\n </Grid>\n </CardDisplay>\n )\n}\nContactPropertiesCard.displayName = 'ContactPropertiesCard'\n\nexport default ContactPropertiesCard\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","crmTelHref","normalizePhone","mdiPhoneOutline","CardDisplay","MdiIcon","useSnackbar","useUser","writeGuardedBySeed","Button","Grid","IconButton","InputAdornment","MenuItem","Stack","TextField","Tooltip","Typography","useCallback","useEffect","useState","useContactUpdate","useCrmActivityLogger","parseContactTags","setContactStage","CompanyPicker","useCompanyOptions","useCreateCompany","addressDraftFrom","ContactAddressFields","crmSuiteLockedReason","ContactPropertiesCard","props","hostId","org","record","consentGroup","seed","members","suiteLocked","data","user","enqueueSnackbar","contactUpdate","siteHostId","logActivity","companies","createCompany","nameOverride","setNameOverride","phone","setPhone","jobTitle","setJobTitle","companyName","setCompanyName","companyId","setCompanyId","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","join","notes","setNotes","address","setAddress","phoneError","setPhoneError","saving","setSaving","recordId","$id","handleSave","trimmedPhone","trim","normalizedPhone","stageChanged","set","name","slice","verdict","subject","unreadable","status","fromCache","updateOne","ok","message","variant","persist","type","id","email","error","Error","console","allowDuplicate","ownerKnown","options","some","option","uid","telHref","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","color","size","disabled","onClick","container","spacing","xs","sm","label","value","slotProps","input","readOnly","helperText","fullWidth","onChange","event","target","htmlInput","maxLength","canonicalName","Boolean","endAdornment","position","title","component","href","edge","aria-label","path","ready","truncated","company","onCreate","fallbackName","undefined","select","map","stage","length","memberName","owner","crmMemberPickerLabel","placeholder","multiline","minRows","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contact-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 {\n type AglynOrgBilling,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n type ConsentGroup,\n type ContactLifecycleStage,\n crmTelHref,\n normalizePhone,\n} from '@aglyn/aglyn'\nimport { mdiPhoneOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Grid,\n IconButton,\n InputAdornment,\n MenuItem,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useContactUpdate } from '../hooks/use-contact-update'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\nimport { type ContactRecord, parseContactTags } from '../model/contact-record'\nimport type { ContactUpdateFields } from '../model/contact-update'\nimport { setContactStage } from '../model/crm-api'\nimport {\n CompanyPicker,\n useCompanyOptions,\n useCreateCompany,\n} from './company-picker'\nimport {\n addressDraftFrom,\n ContactAddressFields,\n type AddressDraft,\n} from './contact-address-fields'\nimport { crmSuiteLockedReason } from './crm-suite-lock'\nimport { LeadSourceSelect } from './lead-source-select'\nimport type { OrgMembers } from './use-org-members'\n\nexport interface ContactPropertiesCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n /** The org document the shell passed, for the company picker's scope. */\n org?: Partial<AglynOrgBilling> | null\n /** The row, flattened through the viewing group's facet. */\n record: ContactRecord\n /**\n * The org the record belongs to, as the page resolved it — for the org's\n * lead source values (AGL-3298). Null while it settles, when the select\n * offers the starter list and the save still sends only what changed.\n */\n orgId?: string | null\n /** The controller whose facet the edits are saved into. */\n consentGroup: ConsentGroup\n /**\n * The listener's verdict on the row the drafts were seeded from, for the\n * guard that refuses a save over an unconfirmed read.\n */\n seed: { status: 'loading' | 'success' | 'error'; fromCache: boolean }\n /** The team, for the owner picker and the owner's name. */\n members: OrgMembers\n /**\n * The org's plan lacks the CRM (AGL-2788), which the shell mounts no CRM\n * page for (AGL-2851). The owner, the lifecycle stage and the company\n * show, locked, and are left out of a save; `crm/contact-update` refuses\n * such a plan the rest of the profile, the tags and the notes too.\n */\n suiteLocked?: boolean\n}\n\n/**\n * THE RECORD ITSELF: every field a team keeps on a person, in one card with\n * one Save (AGL-2596).\n *\n * ## One save, one request, one guard\n *\n * A field-at-a-time save would be nine requests and nine chances for the\n * stale-seed guard to refuse one of them — so the card holds a draft of the\n * whole profile and sends it once. The guard WRAPS the send: a row seeded\n * from the cache or from a failed read is refused with the message the guard\n * chooses, and what was typed stays in the fields, because a refusal that\n * also emptied the form would read as a save that worked.\n *\n * ## The server writes the facet (AGL-2804)\n *\n * Every field here lives in THIS holder's facet, and the Firestore rules\n * cannot tell one field of a facet from another, so the card writes nothing\n * itself. The draft goes to `crm/contact-update`, which writes each field by\n * dotted path into the holder's facet — never a nested object, which would\n * take every other holder's records with it — clears a blank field rather\n * than storing it empty, keeps the phone's and the company's search echoes\n * at the top of the document, and refuses every field to a plan without\n * the CRM. A refusal is shown in the route's own words.\n *\n * ## The stage goes to its own route\n *\n * The lifecycle stage is the one field an automation listens for:\n * `contactStageChanged` (AGL-2605) is emitted by `crm/contact-stage` after it\n * performs the write, and by nothing else. So when the stage the reader\n * picked differs from the one the record holds, it is sent to that route\n * once the rest of the profile has landed — the stage picked, or `null` for\n * \"Not placed yet\", which clears it and announces nothing. A refusal from\n * that route is reported on its own, with its sentence, after the fields that\n * did save.\n *\n * ## The name is an override\n *\n * The canonical name is shared by every site holding the person, so this\n * card never edits it. What it saves is this holder's own name for them,\n * and the helper says what a blank falls back to, so nobody clears the\n * field expecting the record to go nameless.\n *\n * ## Company is a record, with its name kept beside it\n *\n * The Company field is the picker (AGL-2613): the link is `companyId` in\n * this holder's facet, mirrored into the top-level `companyIds` for the\n * company page's query, and the company's contacts count moves with it —\n * all planned by the route from the stored document, so this card never\n * reasons about another holder's link. The name is sent too, from the picked\n * company, because the list column and the global search read the name and\n * not the id; a record that carries a name with no link — an import, a save\n * from before the picker — keeps it as the label, and the picker offers it\n * as the company to link or create.\n */\nexport function ContactPropertiesCard(props: ContactPropertiesCardProps) {\n const {\n hostId,\n org,\n record,\n orgId = null,\n consentGroup,\n seed,\n members,\n suiteLocked = false,\n } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const contactUpdate = useContactUpdate(hostId)\n // The site a stage move is routed through: the mounted one, or at the\n // organization level the holder's own (AGL-2630).\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 companies = useCompanyOptions({ hostId, org })\n const createCompany = useCreateCompany({ hostId, org })\n\n const [nameOverride, setNameOverride] = useState(record.nameOverride)\n const [phone, setPhone] = useState(record.phone)\n const [jobTitle, setJobTitle] = useState(record.jobTitle)\n // Salesforce's Lead Source (AGL-3298), carried from the lead on conversion.\n const [leadSource, setLeadSource] = useState(record.leadSource)\n const leadSources = useLeadSourcePicklist(orgId)\n const [companyName, setCompanyName] = useState(record.companyName)\n const [companyId, setCompanyId] = useState<string | null>(\n record.companyId || null,\n )\n const [ownerUid, setOwnerUid] = useState(record.ownerUid)\n const [lifecycleStage, setLifecycleStage] = useState<ContactLifecycleStage | ''>(\n record.lifecycleStage,\n )\n const [tags, setTags] = useState(record.tags.join(', '))\n const [notes, setNotes] = useState(record.notes)\n const [address, setAddress] = useState<AddressDraft>(\n addressDraftFrom(record.address),\n )\n const [phoneError, setPhoneError] = useState('')\n const [saving, setSaving] = useState(false)\n\n /*\n * Re-seeded when the RECORD changes, not when it re-renders. The listener\n * delivers a fresh row on every snapshot, including the one this card's\n * own save produces; re-seeding on each would overwrite what somebody is\n * in the middle of typing with what the server last confirmed.\n */\n const recordId = record.$id\n useEffect(() => {\n setNameOverride(record.nameOverride)\n setPhone(record.phone)\n setJobTitle(record.jobTitle)\n setLeadSource(record.leadSource)\n setCompanyName(record.companyName)\n setCompanyId(record.companyId || null)\n setOwnerUid(record.ownerUid)\n setLifecycleStage(record.lifecycleStage)\n setTags(record.tags.join(', '))\n setNotes(record.notes)\n setAddress(addressDraftFrom(record.address))\n setPhoneError('')\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [recordId])\n\n const handleSave = useCallback(async () => {\n const trimmedPhone = phone.trim()\n const normalizedPhone = trimmedPhone ? normalizePhone(trimmedPhone) : ''\n if (trimmedPhone && !normalizedPhone) {\n setPhoneError('Enter it with its country code, like +1 512 555 0107.')\n return\n }\n setPhoneError('')\n // A stage that differs from the record's is the stage route's to write —\n // see the note above.\n const stageChanged = !suiteLocked && lifecycleStage !== record.lifecycleStage\n const set: ContactUpdateFields = {\n name: nameOverride.trim().slice(0, 120),\n phone: normalizedPhone ?? '',\n jobTitle: jobTitle.trim().slice(0, 120),\n // Sent only when changed: the route judges a sent value against the\n // org's list, and an untouched one needs no judging.\n ...(leadSource !== record.leadSource ? { leadSource } : {}),\n address,\n tags: parseContactTags(tags),\n notes: notes.slice(0, 2000),\n // The owner and the company, sent only on a plan that carries the CRM.\n ...(suiteLocked\n ? {}\n : { ownerUid, companyId, companyName: companyName.trim().slice(0, 120) }),\n }\n setSaving(true)\n try {\n const verdict = await writeGuardedBySeed(\n {\n subject: 'contact',\n unreadable: seed.status === 'error',\n fromCache: seed.fromCache,\n },\n () => contactUpdate.updateOne(record.$id, set),\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n logActivity('Updated contact', {\n type: 'contact',\n id: record.$id,\n name: record.name || record.email,\n })\n if (stageChanged && siteHostId) {\n try {\n await setContactStage(user, siteHostId, record.$id, lifecycleStage || null)\n } catch (error) {\n // The rest of the profile is saved; only the stage was refused, and\n // the route's own sentence says why.\n return void enqueueSnackbar(\n `Saved, but the stage could not be changed: ${\n error instanceof Error ? error.message : 'the request failed'\n }`,\n { variant: 'warning', persist: false },\n )\n }\n }\n enqueueSnackbar('Contact saved', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n } finally {\n setSaving(false)\n }\n }, [\n address,\n companyId,\n companyName,\n contactUpdate,\n enqueueSnackbar,\n siteHostId,\n jobTitle,\n leadSource,\n lifecycleStage,\n logActivity,\n nameOverride,\n notes,\n ownerUid,\n phone,\n record.$id,\n record.email,\n record.leadSource,\n record.lifecycleStage,\n record.name,\n seed.fromCache,\n seed.status,\n suiteLocked,\n tags,\n user,\n ])\n\n /*\n * An owner the roster no longer lists — somebody who left the team — is\n * still offered as the current value, named by their uid, so the select is\n * never handed a value it has no option for and the reader can see who it\n * was before reassigning.\n */\n const ownerKnown = members.options.some((option) => option.uid === ownerUid)\n\n /** The number in the box as something a dialer takes, or nothing (AGL-2661). */\n const telHref = crmTelHref(phone)\n\n return (\n <CardDisplay\n header={'Properties'}\n help={Aglyn.pluginDocsHelp('contactRecord', { anchor: '#the-record-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={saving}\n onClick={() => void handleSave()}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n ),\n }}\n >\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Email\"\n value={record.email}\n slotProps={{ input: { readOnly: true } }}\n helperText=\"The identity every site shares — it cannot be edited here.\"\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Name\"\n value={nameOverride}\n onChange={(event) => setNameOverride(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n helperText={\n record.canonicalName\n ? `Your own name for this person. Blank shows the name they gave: ${record.canonicalName}.`\n : 'Your own name for this person.'\n }\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={Boolean(phoneError)}\n helperText={phoneError || 'With the country code, like +1 512 555 0107'}\n fullWidth\n slotProps={{\n input: {\n /*\n * Click-to-call (AGL-2661), off what is IN the box rather\n * than off the saved value: the number on screen is the one\n * a reader means to ring, and `crmTelHref` withholds the\n * link from anything half-typed.\n */\n endAdornment: telHref ? (\n <InputAdornment position=\"end\">\n <Tooltip title={`Call ${phone.trim()}`}>\n <IconButton\n component=\"a\"\n href={telHref}\n size=\"small\"\n edge=\"end\"\n aria-label={`Call ${phone.trim()}`}\n >\n <MdiIcon path={mdiPhoneOutline.path} size={0.8} />\n </IconButton>\n </Tooltip>\n </InputAdornment>\n ) : null,\n },\n }}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => setJobTitle(event.target.value)}\n slotProps={{ htmlInput: { maxLength: 120 } }}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <LeadSourceSelect\n picklist={leadSources.picklist}\n value={leadSource}\n stored={record.leadSource}\n onChange={setLeadSource}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <CompanyPicker\n options={companies.options}\n ready={companies.ready}\n truncated={companies.truncated}\n value={companyId}\n onChange={(id, company) => {\n setCompanyId(id)\n // The label follows the link: the picked name, or nothing\n // once the link is cleared. A company the list cannot name\n // keeps whatever label the record already had.\n setCompanyName(company ? company.name : id ? companyName : '')\n }}\n onCreate={createCompany}\n email={record.email}\n fallbackName={companyName}\n disabled={saving || suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n />\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Lifecycle stage\"\n value={lifecycleStage}\n onChange={(event) =>\n setLifecycleStage(event.target.value as ContactLifecycleStage | '')\n }\n disabled={suiteLocked}\n helperText={suiteLocked ? crmSuiteLockedReason() : undefined}\n fullWidth\n >\n <MenuItem value=\"\">{'Not placed yet'}</MenuItem>\n {CONTACT_LIFECYCLE_STAGES.map((stage) => (\n <MenuItem key={stage} value={stage}>\n {CONTACT_LIFECYCLE_STAGE_LABELS[stage]}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={ownerUid}\n onChange={(event) => setOwnerUid(event.target.value)}\n disabled={suiteLocked}\n helperText={\n suiteLocked\n ? crmSuiteLockedReason()\n : members.ready && !members.options.length\n ? 'The team roster could not be read, so nobody can be picked yet.'\n : 'The team member responsible for this relationship'\n }\n fullWidth\n >\n <MenuItem value=\"\">{'Unassigned'}</MenuItem>\n {ownerUid && !ownerKnown ? (\n <MenuItem value={ownerUid}>{members.memberName(ownerUid)}</MenuItem>\n ) : null}\n {members.options.map((owner) => (\n <MenuItem key={owner.uid} value={owner.uid}>\n {Aglyn.crmMemberPickerLabel(owner)}\n </MenuItem>\n ))}\n </TextField>\n </Grid>\n <Grid size={{ xs: 12, sm: 6 }}>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"vip, beta\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => setTags(event.target.value)}\n fullWidth\n />\n </Grid>\n <Grid size={{ xs: 12 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n <ContactAddressFields value={address} onChange={setAddress} />\n </Stack>\n </Grid>\n <Grid size={{ xs: 12 }}>\n <TextField\n size=\"small\"\n label=\"About\"\n value={notes}\n onChange={(event) => setNotes(event.target.value)}\n helperText=\"Notes for your team. Nobody outside this site's group can read them.\"\n multiline\n minRows={3}\n fullWidth\n />\n </Grid>\n </Grid>\n </CardDisplay>\n )\n}\nContactPropertiesCard.displayName = 'ContactPropertiesCard'\n\nexport default ContactPropertiesCard\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","crmTelHref","normalizePhone","mdiPhoneOutline","CardDisplay","MdiIcon","useSnackbar","useUser","writeGuardedBySeed","Button","Grid","IconButton","InputAdornment","MenuItem","Stack","TextField","Tooltip","Typography","useCallback","useEffect","useState","useContactUpdate","useCrmActivityLogger","useLeadSourcePicklist","parseContactTags","setContactStage","CompanyPicker","useCompanyOptions","useCreateCompany","addressDraftFrom","ContactAddressFields","crmSuiteLockedReason","LeadSourceSelect","ContactPropertiesCard","props","hostId","org","record","orgId","consentGroup","seed","members","suiteLocked","data","user","enqueueSnackbar","contactUpdate","siteHostId","logActivity","companies","createCompany","nameOverride","setNameOverride","phone","setPhone","jobTitle","setJobTitle","leadSource","setLeadSource","leadSources","companyName","setCompanyName","companyId","setCompanyId","ownerUid","setOwnerUid","lifecycleStage","setLifecycleStage","tags","setTags","join","notes","setNotes","address","setAddress","phoneError","setPhoneError","saving","setSaving","recordId","$id","handleSave","trimmedPhone","trim","normalizedPhone","stageChanged","set","name","slice","verdict","subject","unreadable","status","fromCache","updateOne","ok","message","variant","persist","type","id","email","error","Error","console","allowDuplicate","ownerKnown","options","some","option","uid","telHref","header","help","pluginDocsHelp","anchor","contentGutterX","contentGutterY","HeaderProps","action","color","size","disabled","onClick","container","spacing","xs","sm","label","value","slotProps","input","readOnly","helperText","fullWidth","onChange","event","target","htmlInput","maxLength","canonicalName","Boolean","endAdornment","position","title","component","href","edge","aria-label","path","picklist","stored","ready","truncated","company","onCreate","fallbackName","undefined","select","map","stage","length","memberName","owner","crmMemberPickerLabel","placeholder","multiline","minRows","displayName"],"mappings":"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"}
|
|
@@ -100,6 +100,13 @@ export interface CsvImportVocabulary<F extends string, R extends Record<string,
|
|
|
100
100
|
templateFileName: string;
|
|
101
101
|
/** The result panel's collection-specific lines, when there are any. */
|
|
102
102
|
resultExtras?: (result: ImportChunkResult<S>) => ReactNode;
|
|
103
|
+
/**
|
|
104
|
+
* A collection-specific warning about the mapped file, shown above the
|
|
105
|
+
* preview before anything is sent — a value the server will refuse that
|
|
106
|
+
* the operator can fix first. Given every row as `mapRow` reads it;
|
|
107
|
+
* answers nothing when the file is fine.
|
|
108
|
+
*/
|
|
109
|
+
rowsNotice?: (rows: readonly R[]) => ReactNode;
|
|
103
110
|
}
|
|
104
111
|
export interface CsvImportDrawerProps<F extends string, R extends Record<string, unknown>, S extends ImportSkippedRow<string>> {
|
|
105
112
|
open: boolean;
|
|
@@ -172,6 +172,12 @@ export function CsvImportDrawer(props) {
|
|
|
172
172
|
mapping,
|
|
173
173
|
vocabulary
|
|
174
174
|
]);
|
|
175
|
+
// The collection's own warning over the whole mapped file, when it has one.
|
|
176
|
+
const rowsNotice = useMemo(()=>vocabulary.rowsNotice && rows.length ? vocabulary.rowsNotice(rows.map((cells)=>vocabulary.mapRow(cells, mapping))) : null, [
|
|
177
|
+
rows,
|
|
178
|
+
mapping,
|
|
179
|
+
vocabulary
|
|
180
|
+
]);
|
|
175
181
|
const mappedTargets = useMemo(()=>Object.entries(mapping).sort(([a], [b])=>Number(a) - Number(b)).map(([, target])=>target), [
|
|
176
182
|
mapping
|
|
177
183
|
]);
|
|
@@ -442,6 +448,7 @@ export function CsvImportDrawer(props) {
|
|
|
442
448
|
severity: "warning",
|
|
443
449
|
children: vocabulary.requiredWarning
|
|
444
450
|
}) : null,
|
|
451
|
+
columns.length > 0 && requiredMapped ? rowsNotice : null,
|
|
445
452
|
columns.length > 0 && requiredMapped ? /*#__PURE__*/ _jsx(ImportPreviewTable, {
|
|
446
453
|
targets: mappedTargets,
|
|
447
454
|
preview: preview,
|