@aglyn/plugins-crm 1.0.0-beta.233 → 1.0.0-beta.235

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.
Files changed (29) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/company-detail-page.js +3 -5
  3. package/src/lib/components/company-detail-page.js.map +1 -1
  4. package/src/lib/components/company-properties-card.d.ts +11 -0
  5. package/src/lib/components/company-properties-card.js +7 -3
  6. package/src/lib/components/company-properties-card.js.map +1 -1
  7. package/src/lib/components/contact-detail-page.js +2 -3
  8. package/src/lib/components/contact-detail-page.js.map +1 -1
  9. package/src/lib/components/contact-known-by-card.js +1 -1
  10. package/src/lib/components/contact-known-by-card.js.map +1 -1
  11. package/src/lib/components/contact-properties-card.js +1 -1
  12. package/src/lib/components/contact-properties-card.js.map +1 -1
  13. package/src/lib/components/deal-properties-card.js +1 -1
  14. package/src/lib/components/deal-properties-card.js.map +1 -1
  15. package/src/lib/components/lead-campaigns-card.js +1 -1
  16. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  17. package/src/lib/components/lead-detail-page.js +4 -8
  18. package/src/lib/components/lead-detail-page.js.map +1 -1
  19. package/src/lib/components/lead-history-card.js +1 -1
  20. package/src/lib/components/lead-history-card.js.map +1 -1
  21. package/src/lib/components/lead-properties-card.d.ts +10 -0
  22. package/src/lib/components/lead-properties-card.js +8 -4
  23. package/src/lib/components/lead-properties-card.js.map +1 -1
  24. package/src/lib/components/linked-deals-card.js +1 -1
  25. package/src/lib/components/linked-deals-card.js.map +1 -1
  26. package/src/lib/components/record-sharing-card.js +3 -1
  27. package/src/lib/components/record-sharing-card.js.map +1 -1
  28. package/src/lib/components/settings-section.js +3 -1
  29. package/src/lib/components/settings-section.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-history-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { CAPTURED_BY_HOST_FIELD, CONTACT_SOURCE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\n// The component path and NOT the marketing barrel, for the reason the Inbox\n// and the contacts list give: the barrel is the tenant loader's entry point\n// for the plugin's SITE half, and a console card named there ships to every\n// published page.\nimport {\n CrmRecordAttributionZone,\n useHasCrmRecordAttribution,\n} from './crm-attribution-zone'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Chip, Stack, Typography } from '@mui/material'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\n\n/**\n * The surfaces `addHostLead` names — `signup`, `booking`, `form:{formId}`,\n * `import` — as they read on screen. A form's id is not a name, so it is\n * shown as the kind with the id beside it rather than as an opaque token.\n * The bare kind `form` is the lifecycle backfill's spelling for a person\n * whose timeline kept no form id (AGL-2631), and reads as the kind, the\n * way the contact's own source chip does.\n */\nexport function leadSourceLabel(source: string): string {\n if (source === 'signup') return 'Sign-up'\n if (source === 'booking') return 'Booking'\n if (source === 'import') return CONTACT_SOURCE_LABELS.import\n if (source === 'form') return CONTACT_SOURCE_LABELS.form\n // A lead entered by hand or over the REST API (AGL-3231).\n if (source === 'manual') return 'Added by hand'\n if (source === 'api') return 'API'\n if (source.startsWith('form:')) return `Form ${source.slice('form:'.length)}`\n return source\n}\n\n/** Every surface that produced a capture — the array, or the older single field. */\nexport function leadSources(lead: Record<string, unknown>): string[] {\n const sources = lead['sources']\n if (Array.isArray(sources) && sources.length) {\n return sources.map((source) => String(source))\n }\n return typeof lead['source'] === 'string' && lead['source'] ? [lead['source']] : []\n}\n\n/** Epoch millis or a Firestore timestamp, as a local date-time, or a dash. */\nexport function leadTimeLabel(value: unknown): string {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return new Date(value).toLocaleString()\n }\n const asDate = (value as { toDate?: () => Date } | null | undefined)?.toDate?.()\n return asDate ? asDate.toLocaleString() : '—'\n}\n\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\nexport interface LeadHistoryCardProps {\n /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Named on \"Captured on\" when the document\n * carries no capture list of its own, and the one site the attribution\n * zone is asked about — with none the zone is not drawn, as on the\n * contact's Associations card.\n */\n hostId: string | null\n leadId: string\n lead: Record<string, unknown>\n}\n\n/**\n * What the capture door recorded about this person (AGL-2608): every\n * surface that produced a capture, how many times, and when the first and\n * the latest happened — the fields `addHostLead` keeps on the one document\n * per person — plus the campaign the first capture is credited to.\n *\n * Read-only by construction. Nothing here is the team's to edit: it is what\n * the visitor did, and the CRM's working state lives on the card above it.\n */\nexport function LeadHistoryCard(props: LeadHistoryCardProps) {\n const { hostId, leadId, lead } = props\n // The caption introduces what another plugin draws; with none loaded it\n // would introduce nothing.\n const hasAttribution = useHasCrmRecordAttribution()\n // Sites read by NAME at the organization level, as they do in the Leads\n // list's \"Known by\" column; under a site there is no mount and an id is\n // all there is to print.\n const mount = useCrmOrgMount()\n const sources = leadSources(lead)\n const capturedBy = lead[CAPTURED_BY_HOST_FIELD]\n const count = Number(lead['submissionCount'] ?? 0) || (sources.length ? 1 : 0)\n return (\n <CardDisplay\n header={'Captured history'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={3}>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={3}>\n <Fact label=\"First seen\">\n {leadTimeLabel(lead['firstSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Last seen\">\n {leadTimeLabel(lead['lastSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Captures\">{count ? String(count) : '—'}</Fact>\n <Fact label=\"Captured on\">\n {Array.isArray(capturedBy) && capturedBy.length\n ? capturedBy\n .map((id) => mount?.siteName(String(id)) ?? String(id))\n .join(', ')\n : (hostId ?? '—')}\n </Fact>\n </Stack>\n <Fact label=\"Sources\">\n {sources.length ? (\n <Stack useFlexGap direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip key={source} size=\"small\" variant=\"outlined\" label={leadSourceLabel(source)} />\n ))}\n </Stack>\n ) : (\n '—'\n )}\n </Fact>\n {hasAttribution && hostId ? (\n <Stack spacing={1}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Where this lead came from'}\n </Typography>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"lead\"\n recordId={leadId}\n />\n {/* Attribution is which campaign brought the person; the\n filing on the Campaigns card is the team's own (AGL-3274),\n and a reader who sees one campaign here and another there\n is owed the difference. */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Filing is separate — see Filed under campaigns.'}\n </Typography>\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadHistoryCard.displayName = 'LeadHistoryCard'\n\nexport default LeadHistoryCard\n"],"names":["CAPTURED_BY_HOST_FIELD","CONTACT_SOURCE_LABELS","pluginDocsHelp","CrmRecordAttributionZone","useHasCrmRecordAttribution","CardDisplay","Chip","Stack","Typography","useCrmOrgMount","leadSourceLabel","source","import","form","startsWith","slice","length","leadSources","lead","sources","Array","isArray","map","String","leadTimeLabel","value","Number","isFinite","Date","toLocaleString","asDate","toDate","Fact","props","spacing","variant","color","label","component","children","LeadHistoryCard","hostId","leadId","hasAttribution","mount","capturedBy","count","header","help","anchor","contentGutterX","contentGutterY","direction","xs","md","id","siteName","join","useFlexGap","sx","flexWrap","rowGap","size","recordKind","recordId","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-history-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { CAPTURED_BY_HOST_FIELD, CONTACT_SOURCE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\n// The component path and NOT the marketing barrel, for the reason the Inbox\n// and the contacts list give: the barrel is the tenant loader's entry point\n// for the plugin's SITE half, and a console card named there ships to every\n// published page.\nimport {\n CrmRecordAttributionZone,\n useHasCrmRecordAttribution,\n} from './crm-attribution-zone'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Chip, Stack, Typography } from '@mui/material'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\n\n/**\n * The surfaces `addHostLead` names — `signup`, `booking`, `form:{formId}`,\n * `import` — as they read on screen. A form's id is not a name, so it is\n * shown as the kind with the id beside it rather than as an opaque token.\n * The bare kind `form` is the lifecycle backfill's spelling for a person\n * whose timeline kept no form id (AGL-2631), and reads as the kind, the\n * way the contact's own source chip does.\n */\nexport function leadSourceLabel(source: string): string {\n if (source === 'signup') return 'Sign-up'\n if (source === 'booking') return 'Booking'\n if (source === 'import') return CONTACT_SOURCE_LABELS.import\n if (source === 'form') return CONTACT_SOURCE_LABELS.form\n // A lead entered by hand or over the REST API (AGL-3231).\n if (source === 'manual') return 'Added by hand'\n if (source === 'api') return 'API'\n if (source.startsWith('form:')) return `Form ${source.slice('form:'.length)}`\n return source\n}\n\n/** Every surface that produced a capture — the array, or the older single field. */\nexport function leadSources(lead: Record<string, unknown>): string[] {\n const sources = lead['sources']\n if (Array.isArray(sources) && sources.length) {\n return sources.map((source) => String(source))\n }\n return typeof lead['source'] === 'string' && lead['source'] ? [lead['source']] : []\n}\n\n/** Epoch millis or a Firestore timestamp, as a local date-time, or a dash. */\nexport function leadTimeLabel(value: unknown): string {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return new Date(value).toLocaleString()\n }\n const asDate = (value as { toDate?: () => Date } | null | undefined)?.toDate?.()\n return asDate ? asDate.toLocaleString() : '—'\n}\n\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\nexport interface LeadHistoryCardProps {\n /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Named on \"Captured on\" when the document\n * carries no capture list of its own, and the one site the attribution\n * zone is asked about — with none the zone is not drawn, as on the\n * contact's Associations card.\n */\n hostId: string | null\n leadId: string\n lead: Record<string, unknown>\n}\n\n/**\n * What the capture door recorded about this person (AGL-2608): every\n * surface that produced a capture, how many times, and when the first and\n * the latest happened — the fields `addHostLead` keeps on the one document\n * per person — plus the campaign the first capture is credited to.\n *\n * Read-only by construction. Nothing here is the team's to edit: it is what\n * the visitor did, and the CRM's working state lives on the card above it.\n */\nexport function LeadHistoryCard(props: LeadHistoryCardProps) {\n const { hostId, leadId, lead } = props\n // The caption introduces what another plugin draws; with none loaded it\n // would introduce nothing.\n const hasAttribution = useHasCrmRecordAttribution()\n // Sites read by NAME at the organization level, as they do in the Leads\n // list's \"Known by\" column; under a site there is no mount and an id is\n // all there is to print.\n const mount = useCrmOrgMount()\n const sources = leadSources(lead)\n const capturedBy = lead[CAPTURED_BY_HOST_FIELD]\n const count = Number(lead['submissionCount'] ?? 0) || (sources.length ? 1 : 0)\n return (\n <CardDisplay\n header={'Captured history'}\n help={pluginDocsHelp('crmLeads', { anchor: '#captured-history' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={3}>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={3}>\n <Fact label=\"First seen\">\n {leadTimeLabel(lead['firstSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Last seen\">\n {leadTimeLabel(lead['lastSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Captures\">{count ? String(count) : '—'}</Fact>\n <Fact label=\"Captured on\">\n {Array.isArray(capturedBy) && capturedBy.length\n ? capturedBy\n .map((id) => mount?.siteName(String(id)) ?? String(id))\n .join(', ')\n : (hostId ?? '—')}\n </Fact>\n </Stack>\n <Fact label=\"Sources\">\n {sources.length ? (\n <Stack useFlexGap direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip key={source} size=\"small\" variant=\"outlined\" label={leadSourceLabel(source)} />\n ))}\n </Stack>\n ) : (\n '—'\n )}\n </Fact>\n {hasAttribution && hostId ? (\n <Stack spacing={1}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Where this lead came from'}\n </Typography>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"lead\"\n recordId={leadId}\n />\n {/* Attribution is which campaign brought the person; the\n filing on the Campaigns card is the team's own (AGL-3274),\n and a reader who sees one campaign here and another there\n is owed the difference. */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Filing is separate — see Filed under campaigns.'}\n </Typography>\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadHistoryCard.displayName = 'LeadHistoryCard'\n\nexport default LeadHistoryCard\n"],"names":["CAPTURED_BY_HOST_FIELD","CONTACT_SOURCE_LABELS","pluginDocsHelp","CrmRecordAttributionZone","useHasCrmRecordAttribution","CardDisplay","Chip","Stack","Typography","useCrmOrgMount","leadSourceLabel","source","import","form","startsWith","slice","length","leadSources","lead","sources","Array","isArray","map","String","leadTimeLabel","value","Number","isFinite","Date","toLocaleString","asDate","toDate","Fact","props","spacing","variant","color","label","component","children","LeadHistoryCard","hostId","leadId","hasAttribution","mount","capturedBy","count","header","help","anchor","contentGutterX","contentGutterY","direction","xs","md","id","siteName","join","useFlexGap","sx","flexWrap","rowGap","size","recordKind","recordId","displayName"],"mappings":"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"}
@@ -7,6 +7,16 @@ import type { OrgMemberOptions } from '../hooks/use-org-member-options';
7
7
  * sentence shape the overflow's items carry, so the two read as one state.
8
8
  */
9
9
  export declare const CONVERT_PENDING_ERASURE_REASON = "An erasure is pending for this person";
10
+ /**
11
+ * The help on a lead's header card — one card, so the page's loading and
12
+ * not-found branches carry the same link as the loaded one.
13
+ */
14
+ export declare const LEAD_PAGE_HELP: {
15
+ title: string;
16
+ excerpt: import("react").ReactNode;
17
+ excerptText: string;
18
+ href: string;
19
+ };
10
20
  export interface LeadPropertiesCardProps {
11
21
  /**
12
22
  * The site this lead is acted on as — the mounted one, or at the
@@ -83,6 +83,12 @@ const NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX;
83
83
  ]
84
84
  });
85
85
  }
86
+ /**
87
+ * The help on a lead's header card — one card, so the page's loading and
88
+ * not-found branches carry the same link as the loaded one.
89
+ */ export const LEAD_PAGE_HELP = Aglyn.pluginDocsHelp('crmLeads', {
90
+ anchor: '#a-leads-page'
91
+ });
86
92
  /**
87
93
  * What the team knows and decides about a lead: status, owner, notes, and
88
94
  * the identity and consent the capture recorded (AGL-2608).
@@ -314,9 +320,7 @@ const NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX;
314
320
  // The name is the heading; the address is the one line under it,
315
321
  // unless the address IS the name, in which case there is no second fact.
316
322
  subtitle: lead['name'] ? String((_lead_email = lead['email']) != null ? _lead_email : '') : undefined,
317
- help: Aglyn.pluginDocsHelp('crmLeads', {
318
- anchor: '#working-a-lead-from-the-row'
319
- }),
323
+ help: LEAD_PAGE_HELP,
320
324
  backHref: routes.section('leads'),
321
325
  backLabel: "Back to leads",
322
326
  // The booking door (AGL-2660), while the lead is still the record
@@ -545,7 +549,7 @@ const NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX;
545
549
  /*#__PURE__*/ _jsx(CardDisplay, {
546
550
  header: 'Details',
547
551
  help: Aglyn.pluginDocsHelp('crmLeads', {
548
- anchor: '#a-leads-page'
552
+ anchor: '#lead-details'
549
553
  }),
550
554
  contentGutterX: true,
551
555
  contentGutterY: true,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-properties-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type {\n AglynOrgBilling,\n ConsentGroup,\n CrmLeadFields,\n CrmLeadProfilePatch,\n} from '@aglyn/aglyn'\nimport { mdiAccountCancelOutline } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n FormControl,\n InputLabel,\n Select,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { deleteField, doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useId, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\nimport { useLeadStatusPicklist } from '../hooks/use-lead-status-picklist'\nimport { useLeadPicklists } from '../hooks/use-lead-picklists'\nimport {\n crmCustomDraftChanges,\n type CrmCustomDraft,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n LeadProfileFields,\n type LeadProfileDraft,\n leadProfileDraftBody,\n leadProfileDraftFrom,\n} from './lead-profile-fields'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmAccountLockChip, CrmEmailGatewayChip } from './crm-email-check'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { LeadStatusChip } from './lead-status-chip'\nimport { leadStatusChoices, leadStatusMenuItems } from './lead-status-options'\nimport {\n crmClientListFields,\n CRM_CLIENT_SEARCH_FIELDS,\n CRM_CLIENT_SORT_FIELDS,\n} from '../model/crm-list-query'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\n/** The Leads list's fields a profile save rewrites; the verdict key is the server's. */\nconst LEAD_CLIENT_LIST_FIELDS = [\n ...CRM_CLIENT_SEARCH_FIELDS,\n // The Lead, Company and Title headers' sort keys (AGL-3680).\n ...CRM_CLIENT_SORT_FIELDS,\n 'leadSourceKey',\n // The lead source's group, read off the org's list (AGL-3577).\n 'leadSourceDirection',\n // Industry and Rating (AGL-3513).\n 'industryKey',\n 'ratingKey',\n] as const\n\nconst NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX\n\n/** The patch as the document takes it: a cleared field is deleted, not blanked. */\nfunction profileWrite(patch: CrmLeadProfilePatch): Record<string, unknown> {\n const write: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n write[key] = value === null ? deleteField() : value\n }\n return write\n}\n\n/**\n * Why Convert is refused while an erasure waits on the person — the same\n * sentence shape the overflow's items carry, so the two read as one state.\n */\nexport const CONVERT_PENDING_ERASURE_REASON = 'An erasure is pending for this person'\n\n/** A label over a value — the record page's one row shape. */\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\nexport interface LeadPropertiesCardProps {\n /**\n * The site this lead is acted on as — the mounted one, or at the\n * organization level the lead's own first capturing site (AGL-3278).\n * `null` for a lead no site has captured: the booking door, the call and\n * the email close, because each of them is one site's to offer.\n */\n hostId: string | null\n /**\n * The group the consent basis is read against (AGL-3278): the site's own\n * declared group, resolved by the page. Not `soloConsentGroup(hostId)`,\n * which is what this card used to spell and which cannot see a refusal\n * recorded against a sibling brand — three sites presenting as one sender\n * are one sender to the person unsubscribing from them.\n */\n consentGroup: ConsentGroup\n /**\n * The org the site belongs to, as the page already resolved it — the\n * custom lead fields are ORG-wide (AGL-3272), and a second lookup here\n * would be one more read per record page for an answer already in hand.\n */\n orgId: string | null\n leadId: string\n lead: Record<string, unknown> & CrmLeadFields\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n basePath: string\n roster: OrgMemberOptions\n onConvert: () => void\n /** Opens the Unqualify dialog — with the Unqualified value picked, when the status select named one. */\n onUnqualify: (statusLabel?: string) => void\n /**\n * Items the page adds to the overflow beside Unqualify — the privacy\n * erasure (AGL-2623) lives on the page, because it needs the workspace\n * role and the API, and this card owns the header it must appear in.\n */\n extraMenuItems?: RowActionsMenuItem[]\n /** What the page shows above the facts — the erasure-pending state. */\n banner?: React.ReactNode\n /**\n * Chips the page adds after the owner — the campaigns the lead is filed\n * under (AGL-3274), named from the containers the page reads once for\n * this header and the Campaigns card below it.\n */\n extraChips?: React.ReactNode\n /**\n * An erasure request is waiting on this person (AGL-2623). Convert stays\n * on the page but is refused with the reason, the way the overflow's items\n * are: a conversion filed now would reach the capture door only to be\n * refused there, and the lead itself goes when the request runs.\n */\n erasurePending?: boolean\n /**\n * The org the shell passed: the booking door reads whether the lead's site\n * runs Bookings and whether the plan is entitled to it (AGL-2660), and a\n * logged call reads the activity scope it belongs in (AGL-2661).\n */\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * What the team knows and decides about a lead: status, owner, notes, and\n * the identity and consent the capture recorded (AGL-2608).\n *\n * Two cards. The record's header carries the identity, the status and owner,\n * the consent the capture recorded and the record's actions. **Details**\n * under it carries the profile, the org's own lead fields under **More\n * fields**, and the notes — one card with one Save in its header, the way\n * Salesforce keeps a record's standard and custom fields in one Details\n * section.\n *\n * Status and owner are single-field client writes — the rules let a site\n * admin, editor or author update `hosts/{hostId}/leads`, and a one-field\n * `update` cannot roll anything else back. The details are drafts seeded\n * from the document, so their save goes through `writeGuardedBySeed`: a\n * draft edited over a cached read would otherwise overwrite a newer note or\n * profile with an older one.\n *\n * Converted leads are read-only here, but for the notes. Their status is the\n * conversion, and the header's actions become links to what the conversion\n * made.\n */\nexport function LeadPropertiesCard(props: LeadPropertiesCardProps) {\n const {\n hostId,\n consentGroup,\n orgId,\n leadId,\n lead,\n leadStatus,\n fromCache,\n basePath,\n roster,\n onConvert,\n onUnqualify,\n extraMenuItems = [],\n banner,\n extraChips,\n erasurePending = false,\n org,\n } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const routes = crmRoutes(basePath)\n // A client-direct write owes the org's sharing rules a re-evaluation (AGL-3336).\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n /*\n * Built at USE, not at render (AGL-3275). A lead is an org row, so the org\n * has to be known to address one — and `orgId` is null while the lookup\n * settles. Composing a ref from that null throws during render, which the\n * page shows as a 500 rather than as a card still loading.\n */\n const refFor = () => (orgId ? doc(firestore, 'orgs', orgId, 'leads', leadId) : null)\n const status = Aglyn.crmLeadStatus(lead)\n const converted = Boolean(lead.convertedContactId)\n const open = Aglyn.isCrmLeadOpen(lead) && !converted\n // The last verdict on the address (AGL-3245), as the platform stamped it.\n const emailState = Aglyn.readEmailState(lead)\n /** What a capture that took a number left on the document (AGL-2661). */\n const leadPhone = String(lead['phone'] ?? '').trim()\n const leadMobile = String(lead.mobilePhone ?? '').trim()\n // The person asked not to be phoned (AGL-3513): a hint on every dial control.\n const doNotCall = lead.doNotCall === true\n\n const [notes, setNotes] = useState(String(lead.notes ?? ''))\n // The label's id, so the status combobox is named \"Status\" rather than\n // after the status it shows — see `LeadOwnerSelect`.\n const statusLabelId = useId()\n const [notesDirty, setNotesDirty] = useState(false)\n /*\n * THE LEAD'S OWN PROFILE (AGL-3231, AGL-3513) — the name's parts,\n * company, title, the phones, website, address, tags, lead source,\n * industry, rating, size and revenue — edited in the Details card with\n * the notes and the custom fields, under one Save, the way the contact's\n * Properties card saves. Seeded from the document and guarded on save: a\n * draft edited over a cached read must not overwrite a newer profile\n * with an older one.\n */\n const [profile, setProfile] = useState<LeadProfileDraft>(() => leadProfileDraftFrom(lead))\n const [profileDirty, setProfileDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n const [profileErrors, setProfileErrors] = useState<Record<string, string>>({})\n // The stored profile as one value, so a change to any of its fields reseeds.\n const profileSeed = JSON.stringify(Aglyn.CRM_LEAD_PROFILE_KEYS.map((key) => lead[key] ?? null))\n useEffect(() => {\n if (!profileDirty) setProfile(leadProfileDraftFrom(lead))\n // The draft follows the document until it is edited.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [profileDirty, profileSeed])\n /*\n * THE ORG'S OWN LEAD FIELDS (AGL-3272), edited under the same Save as\n * the profile: one button over one card, so a person filling a lead in\n * does not have to find two.\n *\n * The draft holds only the keys the reader touched, and Save writes the\n * difference as dotted paths — a `custom` map written whole would take\n * out every key this card did not show, which is what a retired field's\n * values and an integration's writes sit under.\n */\n const fields = useContactFieldDefinitions(orgId, 'lead')\n // The org's lead source values (AGL-3298), for the select below.\n const leadSources = useLeadSourcePicklist(orgId)\n // The org's lead status values (AGL-3512): the chip's words and the select's choices.\n const leadStatuses = useLeadStatusPicklist(orgId)\n // The org's Salutation, Industry and Rating lists (AGL-3513).\n const leadPicklists = useLeadPicklists(orgId)\n const statusChoices = useMemo(\n () => leadStatusChoices(leadStatuses.picklist, lead),\n [leadStatuses.picklist, lead],\n )\n const storedCustom = useMemo(() => lead.custom ?? {}, [lead.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n const editProfile = <K extends keyof LeadProfileDraft>(key: K, value: LeadProfileDraft[K]) => {\n setProfile((current) => ({ ...current, [key]: value }))\n setProfileDirty(true)\n }\n // A newer note from the server replaces an UNEDITED draft; an edited one is\n // the reader's, and the guard on save decides whether it may land.\n useEffect(() => {\n if (!notesDirty) setNotes(String(lead.notes ?? ''))\n }, [lead.notes, notesDirty])\n\n /** One update of the lead, and whether it landed. */\n const write = async (fields: Record<string, unknown>, done: string): Promise<boolean> => {\n try {\n const ref = refFor()\n if (!ref) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return false\n }\n await updateDoc(ref, { ...fields, updatedAt: serverTimestamp() })\n followUpSharing('leads', [leadId])\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n return false\n }\n }\n\n /*\n * What Save is offered for: a profile field edited, a custom value that\n * differs from the stored map, or the notes. Touched-and-put-back does\n * not count for a custom value — the draft keeps the key either way, and\n * a Save that wrote nothing would still bump `updatedAt`.\n */\n const customChanged = crmCustomDraftChanges(storedCustom, custom).length > 0\n const dirty = profileDirty || customChanged || notesDirty\n\n /** Every draft back to what the document holds. */\n const discard = () => {\n setProfile(leadProfileDraftFrom(lead))\n setProfileDirty(false)\n setProfileErrors({})\n setCustom({})\n setNotes(String(lead.notes ?? ''))\n setNotesDirty(false)\n }\n\n /*\n * The Details card's one Save: the profile and the custom values while\n * the lead is open, and the notes, in one update. A converted lead's\n * profile is the contact's now, so only its notes are written.\n */\n const saveDetails = async () => {\n const update: Record<string, unknown> = {}\n if (!converted && (profileDirty || customChanged)) {\n const { body, revenueError } = leadProfileDraftBody(profile)\n const { patch, errors } = Aglyn.normalizeCrmLeadProfile(body)\n const shown: Record<string, string> = { ...errors }\n if (revenueError) shown['annualRevenueCents'] = revenueError\n setProfileErrors(shown)\n if (Object.keys(shown).length) return\n /*\n * A required field the reader CLEARED is refused; one the lead has\n * always lacked is not this save's to demand, or a field added after\n * the lead was captured would block every later edit to its company.\n */\n const missing = crmCustomDraftMissingRequired(\n fields.active,\n storedCustom,\n custom,\n 'edit',\n )\n if (missing.length) {\n enqueueSnackbar(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`, {\n variant: 'warning',\n })\n return\n }\n const profileFields = profileWrite(patch)\n // While a first or last name is set, the name is theirs (AGL-3513).\n const composed = Aglyn.crmLeadComposedName(lead, patch)\n if (composed && composed !== lead['name']) profileFields['name'] = composed\n Object.assign(\n update,\n profileFields,\n crmCustomDraftWrites(storedCustom, custom),\n // The company, title, tags and lead source are what the Leads list\n // searches and filters by (AGL-3321).\n crmClientListFields(\n 'leads',\n lead,\n profileFields,\n LEAD_CLIENT_LIST_FIELDS,\n // A direction only from the org's list as the server answered it.\n leadSources.ready ? { leadSources: leadSources.picklist } : {},\n ),\n )\n }\n if (notesDirty) update['notes'] = notes.trim().slice(0, NOTES_MAX)\n if (!Object.keys(update).length) return\n setSaving(true)\n let landed = false\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n landed = await write(update, 'Lead saved')\n },\n )\n setSaving(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The lead could not be saved.', {\n variant: 'warning',\n })\n return\n }\n if (!landed) return\n setProfileDirty(false)\n setNotesDirty(false)\n // The draft is spent: what it held is stored, and the controls read\n // the document again.\n setCustom({})\n }\n\n const consent = Aglyn.readMarketingBasis(lead, consentGroup)\n const consentLine =\n consent.basis === 'granted'\n ? `Opted in to marketing${\n consent.basisAtMs ? ` on ${new Date(consent.basisAtMs).toLocaleDateString()}` : ''\n }`\n : consent.basis === 'declined'\n ? 'Declined marketing'\n : 'No marketing consent recorded — this lead cannot be emailed marketing'\n\n return (\n <>\n <CrmRecordHeader\n kind=\"Lead\"\n title={String(lead['name'] || lead['email'] || leadId)}\n // The name is the heading; the address is the one line under it,\n // unless the address IS the name, in which case there is no second fact.\n subtitle={lead['name'] ? String(lead['email'] ?? '') : undefined}\n help={Aglyn.pluginDocsHelp('crmLeads', { anchor: '#working-a-lead-from-the-row' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n // The booking door (AGL-2660), while the lead is still the record\n // being worked: once converted, the contact is where a meeting is\n // booked from, and the links below lead there.\n booking={converted ? undefined : { hostId, org, kind: 'lead', recordId: leadId }}\n actions={\n <>\n {/*\n Once converted, the lead's actions are the records the\n conversion made.\n */}\n {converted ? (\n <>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.contact(String(lead.convertedContactId))}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open contact'}\n </Button>\n {lead.companyId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.company(lead.companyId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open company'}\n </Button>\n ) : null}\n {lead.dealId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.deal(lead.dealId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open deal'}\n </Button>\n ) : null}\n </>\n ) : erasurePending ? (\n <Tooltip title={CONVERT_PENDING_ERASURE_REASON}>\n {/* A disabled button receives no pointer events, so the\n tooltip anchors on the span around it. */}\n <span>\n <Button size=\"small\" variant=\"contained\" disabled>\n {'Convert'}\n </Button>\n </span>\n </Tooltip>\n ) : (\n <Button size=\"small\" variant=\"contained\" onClick={onConvert}>\n {'Convert'}\n </Button>\n )}\n {/* Dial the number the capture carried, and log the call (AGL-2661). */}\n <CrmCallButton\n hostId={hostId}\n org={org}\n link={{ leadId }}\n phone={leadPhone || leadMobile}\n doNotCall={doNotCall}\n />\n <CrmSendEmailButton\n hostId={hostId}\n leadId={leadId}\n email={String(lead['email'] ?? '')}\n name={String(lead['name'] ?? '')}\n emailState={emailState}\n />\n </>\n }\n menuItems={[\n ...(open\n ? [\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => onUnqualify(),\n } satisfies RowActionsMenuItem,\n ]\n : []),\n ...extraMenuItems,\n ]}\n chips={\n <>\n <LeadStatusChip lead={lead} statuses={leadStatuses.picklist} />\n {/* The verdict on the address (AGL-3245), beside the status: a\n bounce does not move New or Working, but it is the first\n thing a person deciding whether to write must see. */}\n <CrmEmailStateChip state={emailState} />\n {/* The mail gateway in front of the address, and what it did\n with this site's mail this week (AGL-3328). */}\n <CrmEmailGatewayChip\n hostId={hostId}\n email={String(lead['email'] ?? '')}\n emailState={emailState}\n />\n <CrmAccountLockChip\n hostId={hostId}\n email={String(lead['email'] ?? '')}\n />\n <CrmRecordChip\n label=\"Owner\"\n value={lead.ownerUid ? roster.labelFor(lead.ownerUid) : undefined}\n />\n {extraChips}\n </>\n }\n >\n <Stack spacing={3}>\n {banner}\n {/*\n Only when the capture carried one (AGL-2661): the sign-up door\n writes no phone, and a booking only when its service asks for\n one (AGL-3493), so a row for every lead would often be a\n permanent blank. A form that captures one fills this.\n */}\n {leadPhone ? (\n <Fact label=\"Phone\">\n <CrmPhoneLink phone={leadPhone} doNotCall={doNotCall} />\n </Fact>\n ) : null}\n {leadMobile ? (\n <Fact label=\"Mobile phone\">\n <CrmPhoneLink phone={leadMobile} doNotCall={doNotCall} />\n </Fact>\n ) : null}\n <Fact label=\"Marketing consent\">{consentLine}</Fact>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n {converted ? (\n <Fact label=\"Status\">\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <LeadStatusChip lead={lead} statuses={leadStatuses.picklist} />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {lead.convertedAtMs\n ? `Converted ${new Date(lead.convertedAtMs).toLocaleString()}`\n : 'Converted'}\n </Typography>\n </Stack>\n </Fact>\n ) : (\n <FormControl size=\"small\" sx={{ minWidth: 200 }}>\n <InputLabel id={statusLabelId}>{'Status'}</InputLabel>\n <Select\n labelId={statusLabelId}\n label=\"Status\"\n value={Aglyn.crmLeadStatusLabel(lead, leadStatuses.picklist)}\n onChange={(event) => {\n const choice = statusChoices.find((entry) => entry.label === event.target.value)\n if (!choice) return\n if (choice.status === 'unqualified') {\n onUnqualify(choice.label)\n return\n }\n // The meaning and the org's label for it, together\n // (AGL-3512). Reopening drops the reason with the closed\n // state: a lead being worked again is not \"unqualified\n // because …\".\n void write(\n {\n status: choice.status,\n statusLabel: choice.label,\n ...(status === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n },\n 'Status updated',\n )\n }}\n >\n {leadStatusMenuItems(statusChoices)}\n </Select>\n </FormControl>\n )}\n <LeadOwnerSelect\n value={lead.ownerUid}\n roster={roster}\n fullWidth={false}\n onChange={(uid) =>\n void write({ ownerUid: uid || deleteField() }, uid ? 'Owner assigned' : 'Owner cleared')\n }\n />\n </Stack>\n {status === 'unqualified' && lead.unqualifiedReason ? (\n <Alert severity=\"info\">{`Unqualified: ${lead.unqualifiedReason}`}</Alert>\n ) : null}\n </Stack>\n </CrmRecordHeader>\n {/*\n The profile (AGL-3231), the org's own lead fields (AGL-3272) and the\n notes: what Salesforce keeps on a lead and hands to the contact and\n the account on convert. Read-only once converted — the contact is the\n record then, and the header's links lead there — but for the notes.\n */}\n <CardDisplay\n header={'Details'}\n help={Aglyn.pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {dirty ? (\n <Button size=\"small\" disabled={saving} onClick={discard}>\n {'Discard changes'}\n </Button>\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void saveDetails()}\n disabled={!dirty || saving}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <LeadProfileFields\n draft={profile}\n onChange={editProfile}\n errors={profileErrors}\n disabled={converted}\n leadSources={leadSources.picklist}\n lists={leadPicklists.lists}\n stored={lead}\n />\n {/*\n The org's own lead fields (AGL-3272), after the built-in ones\n because they describe the same record and save with it. Absent,\n not an empty box, while the org defines none.\n */}\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'More fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={converted || saving}\n />\n ))}\n </>\n ) : null}\n <TextField\n size=\"small\"\n label=\"Notes\"\n value={notes}\n onChange={(event) => {\n setNotes(event.target.value)\n setNotesDirty(true)\n }}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: NOTES_MAX } }}\n />\n </Stack>\n </CardDisplay>\n </>\n )\n}\nLeadPropertiesCard.displayName = 'LeadPropertiesCard'\n\nexport default LeadPropertiesCard\n"],"names":["Aglyn","mdiAccountCancelOutline","AppLink","CardDisplay","MdiIcon","useSnackbar","useFirestore","writeGuardedBySeed","Alert","Button","FormControl","InputLabel","Select","Stack","TextField","Tooltip","Typography","deleteField","doc","serverTimestamp","updateDoc","useEffect","useId","useMemo","useState","useContactFieldDefinitions","useLeadSourcePicklist","useLeadStatusPicklist","useLeadPicklists","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","crmRoutes","LeadProfileFields","leadProfileDraftBody","leadProfileDraftFrom","CrmCallButton","CrmPhoneLink","CrmEmailStateChip","CrmAccountLockChip","CrmEmailGatewayChip","CrmRecordChip","CrmRecordHeader","CrmSendEmailButton","LeadOwnerSelect","LeadStatusChip","leadStatusChoices","leadStatusMenuItems","crmClientListFields","CRM_CLIENT_SEARCH_FIELDS","CRM_CLIENT_SORT_FIELDS","useCrmSharingFollowUp","LEAD_CLIENT_LIST_FIELDS","NOTES_MAX","CRM_LEAD_NOTES_MAX","profileWrite","patch","write","key","value","Object","entries","undefined","CONVERT_PENDING_ERASURE_REASON","Fact","props","spacing","variant","color","label","component","children","LeadPropertiesCard","lead","hostId","consentGroup","orgId","leadId","leadStatus","fromCache","basePath","roster","onConvert","onUnqualify","extraMenuItems","banner","extraChips","erasurePending","org","firestore","enqueueSnackbar","routes","followUpSharing","refFor","status","crmLeadStatus","converted","Boolean","convertedContactId","open","isCrmLeadOpen","emailState","readEmailState","leadPhone","String","trim","leadMobile","mobilePhone","doNotCall","notes","setNotes","statusLabelId","notesDirty","setNotesDirty","profile","setProfile","profileDirty","setProfileDirty","saving","setSaving","profileErrors","setProfileErrors","profileSeed","JSON","stringify","CRM_LEAD_PROFILE_KEYS","map","fields","leadSources","leadStatuses","leadPicklists","statusChoices","picklist","storedCustom","custom","setCustom","editProfile","current","done","ref","persist","updatedAt","error","Error","message","customChanged","length","dirty","discard","saveDetails","update","body","revenueError","errors","normalizeCrmLeadProfile","shown","keys","missing","active","join","profileFields","composed","crmLeadComposedName","assign","ready","slice","landed","verdict","subject","unreadable","ok","consent","readMarketingBasis","consentLine","basis","basisAtMs","Date","toLocaleDateString","kind","title","subtitle","help","pluginDocsHelp","anchor","backHref","section","backLabel","booking","recordId","actions","componentVariant","nativeButton","href","contact","size","companyId","company","dealId","deal","span","disabled","onClick","link","phone","email","name","menuItems","icon","path","destructive","chips","statuses","state","ownerUid","labelFor","direction","xs","md","sx","alignItems","convertedAtMs","toLocaleString","minWidth","id","labelId","crmLeadStatusLabel","onChange","event","choice","find","entry","target","statusLabel","unqualifiedReason","fullWidth","uid","severity","header","contentGutterX","contentGutterY","HeaderProps","action","draft","lists","stored","definition","$id","multiline","minRows","slotProps","htmlInput","maxLength","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-properties-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type {\n AglynOrgBilling,\n ConsentGroup,\n CrmLeadFields,\n CrmLeadProfilePatch,\n} from '@aglyn/aglyn'\nimport { mdiAccountCancelOutline } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport type { RowActionsMenuItem } from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n FormControl,\n InputLabel,\n Select,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { deleteField, doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useId, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useLeadSourcePicklist } from '../hooks/use-lead-source-picklist'\nimport { useLeadStatusPicklist } from '../hooks/use-lead-status-picklist'\nimport { useLeadPicklists } from '../hooks/use-lead-picklists'\nimport {\n crmCustomDraftChanges,\n type CrmCustomDraft,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n LeadProfileFields,\n type LeadProfileDraft,\n leadProfileDraftBody,\n leadProfileDraftFrom,\n} from './lead-profile-fields'\nimport { CrmCallButton, CrmPhoneLink } from './crm-call-actions'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { CrmAccountLockChip, CrmEmailGatewayChip } from './crm-email-check'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmSendEmailButton } from './crm-send-email-button'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { LeadStatusChip } from './lead-status-chip'\nimport { leadStatusChoices, leadStatusMenuItems } from './lead-status-options'\nimport {\n crmClientListFields,\n CRM_CLIENT_SEARCH_FIELDS,\n CRM_CLIENT_SORT_FIELDS,\n} from '../model/crm-list-query'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\n/** The Leads list's fields a profile save rewrites; the verdict key is the server's. */\nconst LEAD_CLIENT_LIST_FIELDS = [\n ...CRM_CLIENT_SEARCH_FIELDS,\n // The Lead, Company and Title headers' sort keys (AGL-3680).\n ...CRM_CLIENT_SORT_FIELDS,\n 'leadSourceKey',\n // The lead source's group, read off the org's list (AGL-3577).\n 'leadSourceDirection',\n // Industry and Rating (AGL-3513).\n 'industryKey',\n 'ratingKey',\n] as const\n\nconst NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX\n\n/** The patch as the document takes it: a cleared field is deleted, not blanked. */\nfunction profileWrite(patch: CrmLeadProfilePatch): Record<string, unknown> {\n const write: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n write[key] = value === null ? deleteField() : value\n }\n return write\n}\n\n/**\n * Why Convert is refused while an erasure waits on the person — the same\n * sentence shape the overflow's items carry, so the two read as one state.\n */\nexport const CONVERT_PENDING_ERASURE_REASON = 'An erasure is pending for this person'\n\n/** A label over a value — the record page's one row shape. */\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\n/**\n * The help on a lead's header card — one card, so the page's loading and\n * not-found branches carry the same link as the loaded one.\n */\nexport const LEAD_PAGE_HELP = Aglyn.pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })\n\nexport interface LeadPropertiesCardProps {\n /**\n * The site this lead is acted on as — the mounted one, or at the\n * organization level the lead's own first capturing site (AGL-3278).\n * `null` for a lead no site has captured: the booking door, the call and\n * the email close, because each of them is one site's to offer.\n */\n hostId: string | null\n /**\n * The group the consent basis is read against (AGL-3278): the site's own\n * declared group, resolved by the page. Not `soloConsentGroup(hostId)`,\n * which is what this card used to spell and which cannot see a refusal\n * recorded against a sibling brand — three sites presenting as one sender\n * are one sender to the person unsubscribing from them.\n */\n consentGroup: ConsentGroup\n /**\n * The org the site belongs to, as the page already resolved it — the\n * custom lead fields are ORG-wide (AGL-3272), and a second lookup here\n * would be one more read per record page for an answer already in hand.\n */\n orgId: string | null\n leadId: string\n lead: Record<string, unknown> & CrmLeadFields\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n basePath: string\n roster: OrgMemberOptions\n onConvert: () => void\n /** Opens the Unqualify dialog — with the Unqualified value picked, when the status select named one. */\n onUnqualify: (statusLabel?: string) => void\n /**\n * Items the page adds to the overflow beside Unqualify — the privacy\n * erasure (AGL-2623) lives on the page, because it needs the workspace\n * role and the API, and this card owns the header it must appear in.\n */\n extraMenuItems?: RowActionsMenuItem[]\n /** What the page shows above the facts — the erasure-pending state. */\n banner?: React.ReactNode\n /**\n * Chips the page adds after the owner — the campaigns the lead is filed\n * under (AGL-3274), named from the containers the page reads once for\n * this header and the Campaigns card below it.\n */\n extraChips?: React.ReactNode\n /**\n * An erasure request is waiting on this person (AGL-2623). Convert stays\n * on the page but is refused with the reason, the way the overflow's items\n * are: a conversion filed now would reach the capture door only to be\n * refused there, and the lead itself goes when the request runs.\n */\n erasurePending?: boolean\n /**\n * The org the shell passed: the booking door reads whether the lead's site\n * runs Bookings and whether the plan is entitled to it (AGL-2660), and a\n * logged call reads the activity scope it belongs in (AGL-2661).\n */\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * What the team knows and decides about a lead: status, owner, notes, and\n * the identity and consent the capture recorded (AGL-2608).\n *\n * Two cards. The record's header carries the identity, the status and owner,\n * the consent the capture recorded and the record's actions. **Details**\n * under it carries the profile, the org's own lead fields under **More\n * fields**, and the notes — one card with one Save in its header, the way\n * Salesforce keeps a record's standard and custom fields in one Details\n * section.\n *\n * Status and owner are single-field client writes — the rules let a site\n * admin, editor or author update `hosts/{hostId}/leads`, and a one-field\n * `update` cannot roll anything else back. The details are drafts seeded\n * from the document, so their save goes through `writeGuardedBySeed`: a\n * draft edited over a cached read would otherwise overwrite a newer note or\n * profile with an older one.\n *\n * Converted leads are read-only here, but for the notes. Their status is the\n * conversion, and the header's actions become links to what the conversion\n * made.\n */\nexport function LeadPropertiesCard(props: LeadPropertiesCardProps) {\n const {\n hostId,\n consentGroup,\n orgId,\n leadId,\n lead,\n leadStatus,\n fromCache,\n basePath,\n roster,\n onConvert,\n onUnqualify,\n extraMenuItems = [],\n banner,\n extraChips,\n erasurePending = false,\n org,\n } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const routes = crmRoutes(basePath)\n // A client-direct write owes the org's sharing rules a re-evaluation (AGL-3336).\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n /*\n * Built at USE, not at render (AGL-3275). A lead is an org row, so the org\n * has to be known to address one — and `orgId` is null while the lookup\n * settles. Composing a ref from that null throws during render, which the\n * page shows as a 500 rather than as a card still loading.\n */\n const refFor = () => (orgId ? doc(firestore, 'orgs', orgId, 'leads', leadId) : null)\n const status = Aglyn.crmLeadStatus(lead)\n const converted = Boolean(lead.convertedContactId)\n const open = Aglyn.isCrmLeadOpen(lead) && !converted\n // The last verdict on the address (AGL-3245), as the platform stamped it.\n const emailState = Aglyn.readEmailState(lead)\n /** What a capture that took a number left on the document (AGL-2661). */\n const leadPhone = String(lead['phone'] ?? '').trim()\n const leadMobile = String(lead.mobilePhone ?? '').trim()\n // The person asked not to be phoned (AGL-3513): a hint on every dial control.\n const doNotCall = lead.doNotCall === true\n\n const [notes, setNotes] = useState(String(lead.notes ?? ''))\n // The label's id, so the status combobox is named \"Status\" rather than\n // after the status it shows — see `LeadOwnerSelect`.\n const statusLabelId = useId()\n const [notesDirty, setNotesDirty] = useState(false)\n /*\n * THE LEAD'S OWN PROFILE (AGL-3231, AGL-3513) — the name's parts,\n * company, title, the phones, website, address, tags, lead source,\n * industry, rating, size and revenue — edited in the Details card with\n * the notes and the custom fields, under one Save, the way the contact's\n * Properties card saves. Seeded from the document and guarded on save: a\n * draft edited over a cached read must not overwrite a newer profile\n * with an older one.\n */\n const [profile, setProfile] = useState<LeadProfileDraft>(() => leadProfileDraftFrom(lead))\n const [profileDirty, setProfileDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n const [profileErrors, setProfileErrors] = useState<Record<string, string>>({})\n // The stored profile as one value, so a change to any of its fields reseeds.\n const profileSeed = JSON.stringify(Aglyn.CRM_LEAD_PROFILE_KEYS.map((key) => lead[key] ?? null))\n useEffect(() => {\n if (!profileDirty) setProfile(leadProfileDraftFrom(lead))\n // The draft follows the document until it is edited.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [profileDirty, profileSeed])\n /*\n * THE ORG'S OWN LEAD FIELDS (AGL-3272), edited under the same Save as\n * the profile: one button over one card, so a person filling a lead in\n * does not have to find two.\n *\n * The draft holds only the keys the reader touched, and Save writes the\n * difference as dotted paths — a `custom` map written whole would take\n * out every key this card did not show, which is what a retired field's\n * values and an integration's writes sit under.\n */\n const fields = useContactFieldDefinitions(orgId, 'lead')\n // The org's lead source values (AGL-3298), for the select below.\n const leadSources = useLeadSourcePicklist(orgId)\n // The org's lead status values (AGL-3512): the chip's words and the select's choices.\n const leadStatuses = useLeadStatusPicklist(orgId)\n // The org's Salutation, Industry and Rating lists (AGL-3513).\n const leadPicklists = useLeadPicklists(orgId)\n const statusChoices = useMemo(\n () => leadStatusChoices(leadStatuses.picklist, lead),\n [leadStatuses.picklist, lead],\n )\n const storedCustom = useMemo(() => lead.custom ?? {}, [lead.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n const editProfile = <K extends keyof LeadProfileDraft>(key: K, value: LeadProfileDraft[K]) => {\n setProfile((current) => ({ ...current, [key]: value }))\n setProfileDirty(true)\n }\n // A newer note from the server replaces an UNEDITED draft; an edited one is\n // the reader's, and the guard on save decides whether it may land.\n useEffect(() => {\n if (!notesDirty) setNotes(String(lead.notes ?? ''))\n }, [lead.notes, notesDirty])\n\n /** One update of the lead, and whether it landed. */\n const write = async (fields: Record<string, unknown>, done: string): Promise<boolean> => {\n try {\n const ref = refFor()\n if (!ref) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return false\n }\n await updateDoc(ref, { ...fields, updatedAt: serverTimestamp() })\n followUpSharing('leads', [leadId])\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n return false\n }\n }\n\n /*\n * What Save is offered for: a profile field edited, a custom value that\n * differs from the stored map, or the notes. Touched-and-put-back does\n * not count for a custom value — the draft keeps the key either way, and\n * a Save that wrote nothing would still bump `updatedAt`.\n */\n const customChanged = crmCustomDraftChanges(storedCustom, custom).length > 0\n const dirty = profileDirty || customChanged || notesDirty\n\n /** Every draft back to what the document holds. */\n const discard = () => {\n setProfile(leadProfileDraftFrom(lead))\n setProfileDirty(false)\n setProfileErrors({})\n setCustom({})\n setNotes(String(lead.notes ?? ''))\n setNotesDirty(false)\n }\n\n /*\n * The Details card's one Save: the profile and the custom values while\n * the lead is open, and the notes, in one update. A converted lead's\n * profile is the contact's now, so only its notes are written.\n */\n const saveDetails = async () => {\n const update: Record<string, unknown> = {}\n if (!converted && (profileDirty || customChanged)) {\n const { body, revenueError } = leadProfileDraftBody(profile)\n const { patch, errors } = Aglyn.normalizeCrmLeadProfile(body)\n const shown: Record<string, string> = { ...errors }\n if (revenueError) shown['annualRevenueCents'] = revenueError\n setProfileErrors(shown)\n if (Object.keys(shown).length) return\n /*\n * A required field the reader CLEARED is refused; one the lead has\n * always lacked is not this save's to demand, or a field added after\n * the lead was captured would block every later edit to its company.\n */\n const missing = crmCustomDraftMissingRequired(\n fields.active,\n storedCustom,\n custom,\n 'edit',\n )\n if (missing.length) {\n enqueueSnackbar(`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} required.`, {\n variant: 'warning',\n })\n return\n }\n const profileFields = profileWrite(patch)\n // While a first or last name is set, the name is theirs (AGL-3513).\n const composed = Aglyn.crmLeadComposedName(lead, patch)\n if (composed && composed !== lead['name']) profileFields['name'] = composed\n Object.assign(\n update,\n profileFields,\n crmCustomDraftWrites(storedCustom, custom),\n // The company, title, tags and lead source are what the Leads list\n // searches and filters by (AGL-3321).\n crmClientListFields(\n 'leads',\n lead,\n profileFields,\n LEAD_CLIENT_LIST_FIELDS,\n // A direction only from the org's list as the server answered it.\n leadSources.ready ? { leadSources: leadSources.picklist } : {},\n ),\n )\n }\n if (notesDirty) update['notes'] = notes.trim().slice(0, NOTES_MAX)\n if (!Object.keys(update).length) return\n setSaving(true)\n let landed = false\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n landed = await write(update, 'Lead saved')\n },\n )\n setSaving(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The lead could not be saved.', {\n variant: 'warning',\n })\n return\n }\n if (!landed) return\n setProfileDirty(false)\n setNotesDirty(false)\n // The draft is spent: what it held is stored, and the controls read\n // the document again.\n setCustom({})\n }\n\n const consent = Aglyn.readMarketingBasis(lead, consentGroup)\n const consentLine =\n consent.basis === 'granted'\n ? `Opted in to marketing${\n consent.basisAtMs ? ` on ${new Date(consent.basisAtMs).toLocaleDateString()}` : ''\n }`\n : consent.basis === 'declined'\n ? 'Declined marketing'\n : 'No marketing consent recorded — this lead cannot be emailed marketing'\n\n return (\n <>\n <CrmRecordHeader\n kind=\"Lead\"\n title={String(lead['name'] || lead['email'] || leadId)}\n // The name is the heading; the address is the one line under it,\n // unless the address IS the name, in which case there is no second fact.\n subtitle={lead['name'] ? String(lead['email'] ?? '') : undefined}\n help={LEAD_PAGE_HELP}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n // The booking door (AGL-2660), while the lead is still the record\n // being worked: once converted, the contact is where a meeting is\n // booked from, and the links below lead there.\n booking={converted ? undefined : { hostId, org, kind: 'lead', recordId: leadId }}\n actions={\n <>\n {/*\n Once converted, the lead's actions are the records the\n conversion made.\n */}\n {converted ? (\n <>\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.contact(String(lead.convertedContactId))}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open contact'}\n </Button>\n {lead.companyId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.company(lead.companyId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open company'}\n </Button>\n ) : null}\n {lead.dealId ? (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={routes.deal(lead.dealId)}\n size=\"small\"\n variant=\"outlined\"\n >\n {'Open deal'}\n </Button>\n ) : null}\n </>\n ) : erasurePending ? (\n <Tooltip title={CONVERT_PENDING_ERASURE_REASON}>\n {/* A disabled button receives no pointer events, so the\n tooltip anchors on the span around it. */}\n <span>\n <Button size=\"small\" variant=\"contained\" disabled>\n {'Convert'}\n </Button>\n </span>\n </Tooltip>\n ) : (\n <Button size=\"small\" variant=\"contained\" onClick={onConvert}>\n {'Convert'}\n </Button>\n )}\n {/* Dial the number the capture carried, and log the call (AGL-2661). */}\n <CrmCallButton\n hostId={hostId}\n org={org}\n link={{ leadId }}\n phone={leadPhone || leadMobile}\n doNotCall={doNotCall}\n />\n <CrmSendEmailButton\n hostId={hostId}\n leadId={leadId}\n email={String(lead['email'] ?? '')}\n name={String(lead['name'] ?? '')}\n emailState={emailState}\n />\n </>\n }\n menuItems={[\n ...(open\n ? [\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />,\n destructive: true,\n onClick: () => onUnqualify(),\n } satisfies RowActionsMenuItem,\n ]\n : []),\n ...extraMenuItems,\n ]}\n chips={\n <>\n <LeadStatusChip lead={lead} statuses={leadStatuses.picklist} />\n {/* The verdict on the address (AGL-3245), beside the status: a\n bounce does not move New or Working, but it is the first\n thing a person deciding whether to write must see. */}\n <CrmEmailStateChip state={emailState} />\n {/* The mail gateway in front of the address, and what it did\n with this site's mail this week (AGL-3328). */}\n <CrmEmailGatewayChip\n hostId={hostId}\n email={String(lead['email'] ?? '')}\n emailState={emailState}\n />\n <CrmAccountLockChip\n hostId={hostId}\n email={String(lead['email'] ?? '')}\n />\n <CrmRecordChip\n label=\"Owner\"\n value={lead.ownerUid ? roster.labelFor(lead.ownerUid) : undefined}\n />\n {extraChips}\n </>\n }\n >\n <Stack spacing={3}>\n {banner}\n {/*\n Only when the capture carried one (AGL-2661): the sign-up door\n writes no phone, and a booking only when its service asks for\n one (AGL-3493), so a row for every lead would often be a\n permanent blank. A form that captures one fills this.\n */}\n {leadPhone ? (\n <Fact label=\"Phone\">\n <CrmPhoneLink phone={leadPhone} doNotCall={doNotCall} />\n </Fact>\n ) : null}\n {leadMobile ? (\n <Fact label=\"Mobile phone\">\n <CrmPhoneLink phone={leadMobile} doNotCall={doNotCall} />\n </Fact>\n ) : null}\n <Fact label=\"Marketing consent\">{consentLine}</Fact>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n {converted ? (\n <Fact label=\"Status\">\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <LeadStatusChip lead={lead} statuses={leadStatuses.picklist} />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {lead.convertedAtMs\n ? `Converted ${new Date(lead.convertedAtMs).toLocaleString()}`\n : 'Converted'}\n </Typography>\n </Stack>\n </Fact>\n ) : (\n <FormControl size=\"small\" sx={{ minWidth: 200 }}>\n <InputLabel id={statusLabelId}>{'Status'}</InputLabel>\n <Select\n labelId={statusLabelId}\n label=\"Status\"\n value={Aglyn.crmLeadStatusLabel(lead, leadStatuses.picklist)}\n onChange={(event) => {\n const choice = statusChoices.find((entry) => entry.label === event.target.value)\n if (!choice) return\n if (choice.status === 'unqualified') {\n onUnqualify(choice.label)\n return\n }\n // The meaning and the org's label for it, together\n // (AGL-3512). Reopening drops the reason with the closed\n // state: a lead being worked again is not \"unqualified\n // because …\".\n void write(\n {\n status: choice.status,\n statusLabel: choice.label,\n ...(status === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n },\n 'Status updated',\n )\n }}\n >\n {leadStatusMenuItems(statusChoices)}\n </Select>\n </FormControl>\n )}\n <LeadOwnerSelect\n value={lead.ownerUid}\n roster={roster}\n fullWidth={false}\n onChange={(uid) =>\n void write({ ownerUid: uid || deleteField() }, uid ? 'Owner assigned' : 'Owner cleared')\n }\n />\n </Stack>\n {status === 'unqualified' && lead.unqualifiedReason ? (\n <Alert severity=\"info\">{`Unqualified: ${lead.unqualifiedReason}`}</Alert>\n ) : null}\n </Stack>\n </CrmRecordHeader>\n {/*\n The profile (AGL-3231), the org's own lead fields (AGL-3272) and the\n notes: what Salesforce keeps on a lead and hands to the contact and\n the account on convert. Read-only once converted — the contact is the\n record then, and the header's links lead there — but for the notes.\n */}\n <CardDisplay\n header={'Details'}\n help={Aglyn.pluginDocsHelp('crmLeads', { anchor: '#lead-details' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Stack direction=\"row\" spacing={1}>\n {dirty ? (\n <Button size=\"small\" disabled={saving} onClick={discard}>\n {'Discard changes'}\n </Button>\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void saveDetails()}\n disabled={!dirty || saving}\n >\n {saving ? 'Saving…' : 'Save'}\n </Button>\n </Stack>\n ),\n }}\n >\n <Stack spacing={2}>\n <LeadProfileFields\n draft={profile}\n onChange={editProfile}\n errors={profileErrors}\n disabled={converted}\n leadSources={leadSources.picklist}\n lists={leadPicklists.lists}\n stored={lead}\n />\n {/*\n The org's own lead fields (AGL-3272), after the built-in ones\n because they describe the same record and save with it. Absent,\n not an empty box, while the org defines none.\n */}\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'More fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={converted || saving}\n />\n ))}\n </>\n ) : null}\n <TextField\n size=\"small\"\n label=\"Notes\"\n value={notes}\n onChange={(event) => {\n setNotes(event.target.value)\n setNotesDirty(true)\n }}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: NOTES_MAX } }}\n />\n </Stack>\n </CardDisplay>\n </>\n )\n}\nLeadPropertiesCard.displayName = 'LeadPropertiesCard'\n\nexport default LeadPropertiesCard\n"],"names":["Aglyn","mdiAccountCancelOutline","AppLink","CardDisplay","MdiIcon","useSnackbar","useFirestore","writeGuardedBySeed","Alert","Button","FormControl","InputLabel","Select","Stack","TextField","Tooltip","Typography","deleteField","doc","serverTimestamp","updateDoc","useEffect","useId","useMemo","useState","useContactFieldDefinitions","useLeadSourcePicklist","useLeadStatusPicklist","useLeadPicklists","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","crmRoutes","LeadProfileFields","leadProfileDraftBody","leadProfileDraftFrom","CrmCallButton","CrmPhoneLink","CrmEmailStateChip","CrmAccountLockChip","CrmEmailGatewayChip","CrmRecordChip","CrmRecordHeader","CrmSendEmailButton","LeadOwnerSelect","LeadStatusChip","leadStatusChoices","leadStatusMenuItems","crmClientListFields","CRM_CLIENT_SEARCH_FIELDS","CRM_CLIENT_SORT_FIELDS","useCrmSharingFollowUp","LEAD_CLIENT_LIST_FIELDS","NOTES_MAX","CRM_LEAD_NOTES_MAX","profileWrite","patch","write","key","value","Object","entries","undefined","CONVERT_PENDING_ERASURE_REASON","Fact","props","spacing","variant","color","label","component","children","LEAD_PAGE_HELP","pluginDocsHelp","anchor","LeadPropertiesCard","lead","hostId","consentGroup","orgId","leadId","leadStatus","fromCache","basePath","roster","onConvert","onUnqualify","extraMenuItems","banner","extraChips","erasurePending","org","firestore","enqueueSnackbar","routes","followUpSharing","refFor","status","crmLeadStatus","converted","Boolean","convertedContactId","open","isCrmLeadOpen","emailState","readEmailState","leadPhone","String","trim","leadMobile","mobilePhone","doNotCall","notes","setNotes","statusLabelId","notesDirty","setNotesDirty","profile","setProfile","profileDirty","setProfileDirty","saving","setSaving","profileErrors","setProfileErrors","profileSeed","JSON","stringify","CRM_LEAD_PROFILE_KEYS","map","fields","leadSources","leadStatuses","leadPicklists","statusChoices","picklist","storedCustom","custom","setCustom","editProfile","current","done","ref","persist","updatedAt","error","Error","message","customChanged","length","dirty","discard","saveDetails","update","body","revenueError","errors","normalizeCrmLeadProfile","shown","keys","missing","active","join","profileFields","composed","crmLeadComposedName","assign","ready","slice","landed","verdict","subject","unreadable","ok","consent","readMarketingBasis","consentLine","basis","basisAtMs","Date","toLocaleDateString","kind","title","subtitle","help","backHref","section","backLabel","booking","recordId","actions","componentVariant","nativeButton","href","contact","size","companyId","company","dealId","deal","span","disabled","onClick","link","phone","email","name","menuItems","icon","path","destructive","chips","statuses","state","ownerUid","labelFor","direction","xs","md","sx","alignItems","convertedAtMs","toLocaleString","minWidth","id","labelId","crmLeadStatusLabel","onChange","event","choice","find","entry","target","statusLabel","unqualifiedReason","fullWidth","uid","severity","header","contentGutterX","contentGutterY","HeaderProps","action","draft","lists","stored","definition","$id","multiline","minRows","slotProps","htmlInput","maxLength","displayName"],"mappings":"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"}
@@ -93,7 +93,7 @@ import { DealEditDrawer } from "./deal-edit-drawer.js";
93
93
  /*#__PURE__*/ _jsx(CardDisplay, {
94
94
  header: 'Deals',
95
95
  help: pluginDocsHelp('deals', {
96
- anchor: '#a-deals-page'
96
+ anchor: '#deals-on-a-record'
97
97
  }),
98
98
  HeaderProps: {
99
99
  action: /*#__PURE__*/ _jsx(Button, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/linked-deals-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 { dealContactRolesOf, dealStageById, pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport { type CrmOrgDoc, useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmFoldsScope } from '../hooks/use-crm-list-query'\nimport { useContactRoleDeals, useLinkedDeals } from '../hooks/use-deals'\nimport { usePipeline } from '../hooks/use-pipeline'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n type DealDoc,\n DEAL_STATUS_LABELS,\n formatMoney,\n} from '../model/deal-board-model'\nimport type { DealFormValues } from '../model/deal-form-model'\nimport { DealEditDrawer } from './deal-edit-drawer'\n\nexport interface LinkedDealsCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org: CrmOrgDoc\n /** The CRM surface's own path, for links into deals. */\n basePath: string\n /** Which record this card sits on. */\n link: { contactId: string; contactName?: string } | { companyId: string; companyName?: string }\n}\n\n/**\n * The deals that name one record, on that record's page (AGL-2598).\n *\n * One bounded listener over the indexed link — `(visibleTo, contactId |\n * companyId, updatedAt DESC)` — and a \"New deal\" that opens the shared\n * drawer with this record already chosen, so a deal started from a person's\n * page is linked to them without anybody searching for them again. The\n * contact card and the company card are this component with the link\n * named; the two exports below exist so the pages that host them import\n * one name each.\n *\n * On a contact's page the card also lists the deals the person is on in\n * any other role (AGL-3521) — a second listener, `useContactRoleDeals`,\n * merged by id — and names the part they play on each.\n */\nexport function LinkedDealsCard(props: LinkedDealsCardProps) {\n const { hostId, org, basePath, link } = props\n const routes = crmRoutes(basePath)\n const scope = useCrmScope({ hostId, org })\n const pipelineState = usePipeline(scope.orgId, {\n hostId,\n org: (org ?? null) as Record<string, unknown> | null,\n })\n const { data: linked, status } = useLinkedDeals(\n scope.orgId,\n scope.visibleTo,\n 'contactId' in link ? { contactId: link.contactId } : { companyId: link.companyId },\n )\n const contactId = 'contactId' in link ? link.contactId : null\n const foldsScope = useCrmFoldsScope(scope.orgId, scope.visibleTo)\n const { data: roleDeals } = useContactRoleDeals(scope.orgId, scope.visibleTo, foldsScope, contactId)\n // The two answers as one list, newest first; a deal in both is listed once.\n const deals = useMemo(() => {\n const byId = new Map<string, DealDoc>()\n for (const deal of [...(linked ?? []), ...(roleDeals ?? [])]) byId.set(deal.$id, deal)\n return [...byId.values()].sort((a, b) => updatedMs(b) - updatedMs(a))\n }, [linked, roleDeals])\n const [creating, setCreating] = useState(false)\n const defaults = useMemo<Partial<DealFormValues>>(\n () =>\n 'contactId' in link\n ? { contactId: link.contactId, contactName: link.contactName ?? '' }\n : { companyId: link.companyId, companyName: link.companyName ?? '' },\n [link],\n )\n\n return (\n <>\n <CardDisplay\n header={'Deals'}\n help={pluginDocsHelp('deals', { anchor: '#a-deals-page' })}\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipelineState.pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n {status === 'success' && deals.length === 0 ? (\n <EmptyStateComponent\n compact\n label={'No deals yet'}\n description={'A deal started here is linked to this record from the first save.'}\n action={\n <Button\n size=\"small\"\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipelineState.pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n }\n />\n ) : (\n <Stack spacing={1}>\n {deals.map((deal) => {\n const stage = dealStageById(\n pipelineState.pipelineById(deal.pipelineId),\n deal.stageId,\n )\n return (\n <Stack\n useFlexGap\n key={deal.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Typography variant=\"body2\" sx={{ flex: 1, minWidth: 160 }} noWrap>\n <AppLink href={routes.deal(deal.$id)}>{deal.title || 'Untitled deal'}</AppLink>\n </Typography>\n {contactId ? <ContactRoleChip deal={deal} contactId={contactId} /> : null}\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={\n deal.status === 'open'\n ? (stage?.name ?? deal.stageId)\n : DEAL_STATUS_LABELS[deal.status]\n }\n color={deal.status === 'won' ? 'success' : 'default'}\n />\n <Typography variant=\"body2\" sx={{ minWidth: 90, textAlign: 'right' }}>\n {typeof deal.amountCents === 'number'\n ? formatMoney(deal.amountCents, deal.currency)\n : '—'}\n </Typography>\n </Stack>\n )\n })}\n </Stack>\n )}\n </CardDisplay>\n <DealEditDrawer\n open={creating}\n onClose={() => setCreating(false)}\n hostId={hostId}\n org={org}\n defaults={defaults}\n pipelines={pipelineState.activePipelines}\n defaultPipeline={pipelineState.pipeline}\n />\n </>\n )\n}\nLinkedDealsCard.displayName = 'LinkedDealsCard'\n\n/** When a deal was last written, for the merged list's order. */\nfunction updatedMs(deal: DealDoc): number {\n const value = (deal as { updatedAt?: unknown }).updatedAt\n if (value instanceof Date) return value.getTime()\n const stamp = value as { toMillis?: () => number } | null | undefined\n return typeof stamp?.toMillis === 'function' ? stamp.toMillis() : 0\n}\n\n/** The part this contact plays on a deal — its role, and Primary when they are. */\nfunction ContactRoleChip(props: { deal: DealDoc; contactId: string }) {\n const row = dealContactRolesOf(props.deal).find((entry) => entry.contactId === props.contactId)\n if (!row) return null\n const label = [row.role, row.primary ? 'Primary' : null].filter(Boolean).join(' · ')\n return label ? <Chip size=\"small\" label={label} /> : null\n}\n\nexport default LinkedDealsCard\n"],"names":["dealContactRolesOf","dealStageById","pluginDocsHelp","mdiPlus","AppLink","CardDisplay","MdiIcon","EmptyStateComponent","Button","Chip","Stack","Typography","useMemo","useState","useCrmScope","useCrmFoldsScope","useContactRoleDeals","useLinkedDeals","usePipeline","crmRoutes","DEAL_STATUS_LABELS","formatMoney","DealEditDrawer","LinkedDealsCard","props","hostId","org","basePath","link","routes","scope","pipelineState","orgId","data","linked","status","visibleTo","contactId","companyId","foldsScope","roleDeals","deals","byId","Map","deal","set","$id","values","sort","a","b","updatedMs","creating","setCreating","defaults","contactName","companyName","header","help","anchor","HeaderProps","action","size","startIcon","path","disabled","pipeline","onClick","contentGutterX","contentGutterY","length","compact","label","description","variant","spacing","map","stage","pipelineById","pipelineId","stageId","useFlexGap","direction","sx","alignItems","flexWrap","rowGap","flex","minWidth","noWrap","href","title","ContactRoleChip","name","color","textAlign","amountCents","currency","open","onClose","pipelines","activePipelines","defaultPipeline","displayName","value","updatedAt","Date","getTime","stamp","toMillis","row","find","entry","role","primary","filter","Boolean","join"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/linked-deals-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 { dealContactRolesOf, dealStageById, pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport { type CrmOrgDoc, useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmFoldsScope } from '../hooks/use-crm-list-query'\nimport { useContactRoleDeals, useLinkedDeals } from '../hooks/use-deals'\nimport { usePipeline } from '../hooks/use-pipeline'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n type DealDoc,\n DEAL_STATUS_LABELS,\n formatMoney,\n} from '../model/deal-board-model'\nimport type { DealFormValues } from '../model/deal-form-model'\nimport { DealEditDrawer } from './deal-edit-drawer'\n\nexport interface LinkedDealsCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org: CrmOrgDoc\n /** The CRM surface's own path, for links into deals. */\n basePath: string\n /** Which record this card sits on. */\n link: { contactId: string; contactName?: string } | { companyId: string; companyName?: string }\n}\n\n/**\n * The deals that name one record, on that record's page (AGL-2598).\n *\n * One bounded listener over the indexed link — `(visibleTo, contactId |\n * companyId, updatedAt DESC)` — and a \"New deal\" that opens the shared\n * drawer with this record already chosen, so a deal started from a person's\n * page is linked to them without anybody searching for them again. The\n * contact card and the company card are this component with the link\n * named; the two exports below exist so the pages that host them import\n * one name each.\n *\n * On a contact's page the card also lists the deals the person is on in\n * any other role (AGL-3521) — a second listener, `useContactRoleDeals`,\n * merged by id — and names the part they play on each.\n */\nexport function LinkedDealsCard(props: LinkedDealsCardProps) {\n const { hostId, org, basePath, link } = props\n const routes = crmRoutes(basePath)\n const scope = useCrmScope({ hostId, org })\n const pipelineState = usePipeline(scope.orgId, {\n hostId,\n org: (org ?? null) as Record<string, unknown> | null,\n })\n const { data: linked, status } = useLinkedDeals(\n scope.orgId,\n scope.visibleTo,\n 'contactId' in link ? { contactId: link.contactId } : { companyId: link.companyId },\n )\n const contactId = 'contactId' in link ? link.contactId : null\n const foldsScope = useCrmFoldsScope(scope.orgId, scope.visibleTo)\n const { data: roleDeals } = useContactRoleDeals(scope.orgId, scope.visibleTo, foldsScope, contactId)\n // The two answers as one list, newest first; a deal in both is listed once.\n const deals = useMemo(() => {\n const byId = new Map<string, DealDoc>()\n for (const deal of [...(linked ?? []), ...(roleDeals ?? [])]) byId.set(deal.$id, deal)\n return [...byId.values()].sort((a, b) => updatedMs(b) - updatedMs(a))\n }, [linked, roleDeals])\n const [creating, setCreating] = useState(false)\n const defaults = useMemo<Partial<DealFormValues>>(\n () =>\n 'contactId' in link\n ? { contactId: link.contactId, contactName: link.contactName ?? '' }\n : { companyId: link.companyId, companyName: link.companyName ?? '' },\n [link],\n )\n\n return (\n <>\n <CardDisplay\n header={'Deals'}\n help={pluginDocsHelp('deals', { anchor: '#deals-on-a-record' })}\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipelineState.pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n {status === 'success' && deals.length === 0 ? (\n <EmptyStateComponent\n compact\n label={'No deals yet'}\n description={'A deal started here is linked to this record from the first save.'}\n action={\n <Button\n size=\"small\"\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipelineState.pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n }\n />\n ) : (\n <Stack spacing={1}>\n {deals.map((deal) => {\n const stage = dealStageById(\n pipelineState.pipelineById(deal.pipelineId),\n deal.stageId,\n )\n return (\n <Stack\n useFlexGap\n key={deal.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Typography variant=\"body2\" sx={{ flex: 1, minWidth: 160 }} noWrap>\n <AppLink href={routes.deal(deal.$id)}>{deal.title || 'Untitled deal'}</AppLink>\n </Typography>\n {contactId ? <ContactRoleChip deal={deal} contactId={contactId} /> : null}\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={\n deal.status === 'open'\n ? (stage?.name ?? deal.stageId)\n : DEAL_STATUS_LABELS[deal.status]\n }\n color={deal.status === 'won' ? 'success' : 'default'}\n />\n <Typography variant=\"body2\" sx={{ minWidth: 90, textAlign: 'right' }}>\n {typeof deal.amountCents === 'number'\n ? formatMoney(deal.amountCents, deal.currency)\n : '—'}\n </Typography>\n </Stack>\n )\n })}\n </Stack>\n )}\n </CardDisplay>\n <DealEditDrawer\n open={creating}\n onClose={() => setCreating(false)}\n hostId={hostId}\n org={org}\n defaults={defaults}\n pipelines={pipelineState.activePipelines}\n defaultPipeline={pipelineState.pipeline}\n />\n </>\n )\n}\nLinkedDealsCard.displayName = 'LinkedDealsCard'\n\n/** When a deal was last written, for the merged list's order. */\nfunction updatedMs(deal: DealDoc): number {\n const value = (deal as { updatedAt?: unknown }).updatedAt\n if (value instanceof Date) return value.getTime()\n const stamp = value as { toMillis?: () => number } | null | undefined\n return typeof stamp?.toMillis === 'function' ? stamp.toMillis() : 0\n}\n\n/** The part this contact plays on a deal — its role, and Primary when they are. */\nfunction ContactRoleChip(props: { deal: DealDoc; contactId: string }) {\n const row = dealContactRolesOf(props.deal).find((entry) => entry.contactId === props.contactId)\n if (!row) return null\n const label = [row.role, row.primary ? 'Primary' : null].filter(Boolean).join(' · ')\n return label ? <Chip size=\"small\" label={label} /> : null\n}\n\nexport default LinkedDealsCard\n"],"names":["dealContactRolesOf","dealStageById","pluginDocsHelp","mdiPlus","AppLink","CardDisplay","MdiIcon","EmptyStateComponent","Button","Chip","Stack","Typography","useMemo","useState","useCrmScope","useCrmFoldsScope","useContactRoleDeals","useLinkedDeals","usePipeline","crmRoutes","DEAL_STATUS_LABELS","formatMoney","DealEditDrawer","LinkedDealsCard","props","hostId","org","basePath","link","routes","scope","pipelineState","orgId","data","linked","status","visibleTo","contactId","companyId","foldsScope","roleDeals","deals","byId","Map","deal","set","$id","values","sort","a","b","updatedMs","creating","setCreating","defaults","contactName","companyName","header","help","anchor","HeaderProps","action","size","startIcon","path","disabled","pipeline","onClick","contentGutterX","contentGutterY","length","compact","label","description","variant","spacing","map","stage","pipelineById","pipelineId","stageId","useFlexGap","direction","sx","alignItems","flexWrap","rowGap","flex","minWidth","noWrap","href","title","ContactRoleChip","name","color","textAlign","amountCents","currency","open","onClose","pipelines","activePipelines","defaultPipeline","displayName","value","updatedAt","Date","getTime","stamp","toMillis","row","find","entry","role","primary","filter","Boolean","join"],"mappings":"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"}
@@ -113,7 +113,9 @@ const when = (atMs)=>atMs > 0 ? new Date(atMs).toLocaleDateString() : '';
113
113
  const heldHostIds = (heldLine == null ? void 0 : heldLine.kind) === 'held' ? heldLine.hostIds : [];
114
114
  return /*#__PURE__*/ _jsxs(CardDisplay, {
115
115
  header: 'Sharing',
116
- help: pluginDocsHelp('crmSharing'),
116
+ help: pluginDocsHelp('crmSharing', {
117
+ anchor: '#sharing-card'
118
+ }),
117
119
  contentGutterX: true,
118
120
  contentGutterY: true,
119
121
  HeaderProps: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/record-sharing-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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiClose } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, IconButton, Stack, Typography } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport { useCrmSharingRules, useCrmSharingSites } from '../hooks/use-crm-sharing'\nimport type { CrmOrgDoc } from '../hooks/use-crm-scope'\nimport {\n CRM_SHARING_OBJECT_LABELS,\n type CrmShareChip,\n crmShareChipFor,\n crmShareChipLabel,\n type CrmSharingObject,\n describeRecordSharing,\n describeShareTargets,\n} from '../model/crm-sharing'\nimport { callCrmSharing } from '../model/crm-sharing-api'\nimport { crmTaskCallScope } from '../model/task-routes'\nimport { useCanManageCrmSettings } from './settings-section'\nimport { ShareRecordsDialog } from './share-records-dialog'\n\n/**\n * The chip a list row or a record header shows when this site sees the\n * record only because it was shared with it: \"Shared by Dana\".\n */\nexport function CrmShareChipView(props: {\n chip: CrmShareChip | null\n org: CrmOrgDoc\n}) {\n const rules = useCrmSharingRules(props.org)\n if (!props.chip) return null\n return (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color=\"info\"\n label={crmShareChipLabel(props.chip, rules)}\n sx={{ maxWidth: 220 }}\n />\n )\n}\nCrmShareChipView.displayName = 'CrmShareChipView'\n\nexport interface RecordSharingCardProps {\n object: CrmSharingObject\n id: string\n record: Readonly<Record<string, unknown>> | null | undefined\n /** The mounted site, or `null` at the organization level. */\n hostId: string | null\n orgId: string | null\n org: CrmOrgDoc\n /** The viewing site's consent-group sites, for \"shared with this site\". */\n viewingHostIds?: readonly string[]\n}\n\nconst when = (atMs: number) => (atMs > 0 ? new Date(atMs).toLocaleDateString() : '')\n\n/**\n * SHARING — where this record is visible, and why (AGL-3336).\n *\n * One line for the sites that hold it (its consent group, or the site that\n * created it), then one per hand share, naming who shared it and when, with\n * the one-click unshare, and one per sharing rule. The held line has no\n * unshare: visibility a site holds is not the sharing's to take away. A\n * rule's line is removed by editing the rule, in Settings.\n *\n * The header's \"Share with sites…\" is an org manager's, and so is the\n * unshare; every other reader sees the lines and nothing to press. For a\n * reader on a site that sees the record only through a read-only share,\n * the card says so, since the page's edits will be refused.\n */\nexport function RecordSharingCard(props: RecordSharingCardProps) {\n const { object, id, record, hostId, orgId, org, viewingHostIds = [] } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId ?? undefined)\n const sites = useCrmSharingSites({ hostId, orgId, enabled: canManage })\n const rules = useCrmSharingRules(org)\n const lines = useMemo(() => describeRecordSharing(record), [record])\n const chip = useMemo(() => crmShareChipFor(record, viewingHostIds), [record, viewingHostIds])\n const [sharing, setSharing] = useState(false)\n const [busyKey, setBusyKey] = useState<string | null>(null)\n const scope = crmTaskCallScope(hostId, orgId)\n const label = CRM_SHARING_OBJECT_LABELS[object]\n const shared = lines.some((line) => line.kind !== 'held')\n if (!record || (!shared && !(roleReady && canManage))) return null\n\n const unshare = async (key: string, token: string) => {\n if (!scope) return\n setBusyKey(key)\n try {\n await callCrmSharing(user, scope, 'unshare', {\n object,\n ids: [id],\n targets: token === 'org' ? 'all' : [token.slice('host:'.length)],\n })\n enqueueSnackbar('No longer shared', { variant: 'success', persist: false })\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyKey(null)\n }\n }\n\n const heldLine = lines.find((line) => line.kind === 'held')\n const heldHostIds = heldLine?.kind === 'held' ? heldLine.hostIds : []\n return (\n <CardDisplay\n header={'Sharing'}\n help={pluginDocsHelp('crmSharing')}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action:\n roleReady && canManage ? (\n <Button variant=\"contained\" size=\"small\" onClick={() => setSharing(true)}>\n {'Share with sites…'}\n </Button>\n ) : null,\n }}\n >\n <Stack spacing={1.5}>\n {chip ? (\n <Typography variant=\"body2\">\n {chip.access === 'edit'\n ? `Shared with this site to read and edit. ${crmShareChipLabel(chip, rules)}.`\n : `Shared with this site read-only: you can see this ${label.one} and log activity ` +\n `and tasks on it, but not change it. ${crmShareChipLabel(chip, rules)}.`}\n </Typography>\n ) : null}\n {lines.map((line) => {\n if (line.kind === 'held') {\n return (\n <Typography key=\"held\" variant=\"body2\" color=\"text.secondary\">\n {line.allSites\n ? 'Visible to every site in the organization.'\n : line.hostIds.length\n ? `Held by ${line.hostIds.map(sites.siteName).join(', ')} — where it was captured.`\n : 'Held by no site.'}\n </Typography>\n )\n }\n if (line.kind === 'manual') {\n const target = describeShareTargets([line.token], sites.siteName)\n return (\n <Stack key={line.key} direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\" sx={{ flex: 1, minWidth: 0 }}>\n {`${target} — shared by ${line.byName || 'a manager'}${\n line.atMs ? ` on ${when(line.atMs)}` : ''\n }, ${line.access === 'edit' ? 'read and edit' : 'read-only'}`}\n </Typography>\n {canManage ? (\n <IconButton\n size=\"small\"\n disabled={busyKey !== null}\n onClick={() => void unshare(line.key, line.token)}\n >\n <MdiIcon path={mdiClose.path} size={0.7} />\n <SrOnly>{`Stop sharing with ${target}`}</SrOnly>\n </IconButton>\n ) : null}\n </Stack>\n )\n }\n const rule = rules.find((entry) => entry.id === line.ruleId)\n return (\n <Typography key={line.key} variant=\"body2\">\n {`${describeShareTargets(line.tokens, sites.siteName)} — by the rule ${\n rule ? `“${rule.name}”` : 'that shares it'\n }, ${line.access === 'edit' ? 'read and edit' : 'read-only'}`}\n </Typography>\n )\n })}\n {!shared ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Not shared with any other site. Sharing lets another site see this ${label.one}; it does not share the person's consent.`}\n </Typography>\n ) : null}\n </Stack>\n <ShareRecordsDialog\n open={sharing}\n onClose={() => setSharing(false)}\n object={object}\n ids={[id]}\n scope={scope}\n sites={sites.sites}\n sitesReady={sites.ready}\n heldHostIds={heldHostIds}\n />\n </CardDisplay>\n )\n}\nRecordSharingCard.displayName = 'RecordSharingCard'\n\n/**\n * \"Share with sites…\" on a list's bulk bar (AGL-3336): the same dialog as\n * the record page, over the selection. Drawn for an org manager only.\n */\nexport function CrmBulkShareButton(props: {\n object: CrmSharingObject\n ids: readonly string[]\n hostId: string | null\n orgId: string | null\n disabled?: boolean\n}) {\n const { object, ids, hostId, orgId, disabled } = props\n const { canManage, ready } = useCanManageCrmSettings(orgId ?? undefined)\n const sites = useCrmSharingSites({ hostId, orgId, enabled: canManage })\n const [open, setOpen] = useState(false)\n if (!ready || !canManage) return null\n return (\n <>\n <Button size=\"small\" disabled={disabled || !ids.length} onClick={() => setOpen(true)}>\n {'Share with sites…'}\n </Button>\n <ShareRecordsDialog\n open={open}\n onClose={() => setOpen(false)}\n object={object}\n ids={ids}\n scope={crmTaskCallScope(hostId, orgId)}\n sites={sites.sites}\n sitesReady={sites.ready}\n />\n </>\n )\n}\nCrmBulkShareButton.displayName = 'CrmBulkShareButton'\n\nexport default RecordSharingCard\n"],"names":["pluginDocsHelp","mdiClose","CardDisplay","MdiIcon","SrOnly","useSnackbar","useUser","Button","Chip","IconButton","Stack","Typography","useMemo","useState","useCrmSharingRules","useCrmSharingSites","CRM_SHARING_OBJECT_LABELS","crmShareChipFor","crmShareChipLabel","describeRecordSharing","describeShareTargets","callCrmSharing","crmTaskCallScope","useCanManageCrmSettings","ShareRecordsDialog","CrmShareChipView","props","rules","org","chip","size","variant","color","label","sx","maxWidth","displayName","when","atMs","Date","toLocaleDateString","RecordSharingCard","object","id","record","hostId","orgId","viewingHostIds","data","user","enqueueSnackbar","canManage","ready","roleReady","undefined","sites","enabled","lines","sharing","setSharing","busyKey","setBusyKey","scope","shared","some","line","kind","unshare","key","token","ids","targets","slice","length","persist","error","message","allowDuplicate","heldLine","find","heldHostIds","hostIds","header","help","contentGutterX","contentGutterY","HeaderProps","action","onClick","spacing","access","one","map","allSites","siteName","join","target","direction","alignItems","flex","minWidth","byName","disabled","path","rule","entry","ruleId","tokens","name","open","onClose","sitesReady","CrmBulkShareButton","setOpen"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,cAAc,QAAQ,eAAc;AAC7C,SAASC,QAAQ,QAAQ,yBAAwB;AACjD,SAASC,WAAW,EAAEC,OAAO,EAAEC,MAAM,QAAQ,uBAAsB;AACnE,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAASC,OAAO,QAAQ,iCAAgC;AACxD,SAASC,MAAM,EAAEC,IAAI,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAC3E,SAASC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACzC,SAASC,kBAAkB,EAAEC,kBAAkB,QAAQ,8BAA0B;AAEjF,SACEC,yBAAyB,EAEzBC,eAAe,EACfC,iBAAiB,EAEjBC,qBAAqB,EACrBC,oBAAoB,QACf,0BAAsB;AAC7B,SAASC,cAAc,QAAQ,8BAA0B;AACzD,SAASC,gBAAgB,QAAQ,0BAAsB;AACvD,SAASC,uBAAuB,QAAQ,wBAAoB;AAC5D,SAASC,kBAAkB,QAAQ,4BAAwB;AAE3D;;;CAGC,GACD,OAAO,SAASC,iBAAiBC,KAGhC;IACC,MAAMC,QAAQb,mBAAmBY,MAAME,GAAG;IAC1C,IAAI,CAACF,MAAMG,IAAI,EAAE,OAAO;IACxB,qBACE,KAACrB;QACCsB,MAAK;QACLC,SAAQ;QACRC,OAAM;QACNC,OAAOf,kBAAkBQ,MAAMG,IAAI,EAAEF;QACrCO,IAAI;YAAEC,UAAU;QAAI;;AAG1B;AACAV,iBAAiBW,WAAW,GAAG;AAc/B,MAAMC,OAAO,CAACC,OAAkBA,OAAO,IAAI,IAAIC,KAAKD,MAAME,kBAAkB,KAAK;AAEjF;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,kBAAkBf,KAA6B;IAC7D,MAAM,EAAEgB,MAAM,EAAEC,EAAE,EAAEC,MAAM,EAAEC,MAAM,EAAEC,KAAK,EAAElB,GAAG,EAAEmB,iBAAiB,EAAE,EAAE,GAAGrB;IACxE,MAAM,EAAEsB,MAAMC,IAAI,EAAE,GAAG3C;IACvB,MAAM,EAAE4C,eAAe,EAAE,GAAG7C;IAC5B,MAAM,EAAE8C,SAAS,EAAEC,OAAOC,SAAS,EAAE,GAAG9B,wBAAwBuB,gBAAAA,QAASQ;IACzE,MAAMC,QAAQxC,mBAAmB;QAAE8B;QAAQC;QAAOU,SAASL;IAAU;IACrE,MAAMxB,QAAQb,mBAAmBc;IACjC,MAAM6B,QAAQ7C,QAAQ,IAAMO,sBAAsByB,SAAS;QAACA;KAAO;IACnE,MAAMf,OAAOjB,QAAQ,IAAMK,gBAAgB2B,QAAQG,iBAAiB;QAACH;QAAQG;KAAe;IAC5F,MAAM,CAACW,SAASC,WAAW,GAAG9C,SAAS;IACvC,MAAM,CAAC+C,SAASC,WAAW,GAAGhD,SAAwB;IACtD,MAAMiD,QAAQxC,iBAAiBuB,QAAQC;IACvC,MAAMb,QAAQjB,yBAAyB,CAAC0B,OAAO;IAC/C,MAAMqB,SAASN,MAAMO,IAAI,CAAC,CAACC,OAASA,KAAKC,IAAI,KAAK;IAClD,IAAI,CAACtB,UAAW,CAACmB,UAAU,CAAEV,CAAAA,aAAaF,SAAQ,GAAK,OAAO;IAE9D,MAAMgB,UAAU,OAAOC,KAAaC;QAClC,IAAI,CAACP,OAAO;QACZD,WAAWO;QACX,IAAI;YACF,MAAM/C,eAAe4B,MAAMa,OAAO,WAAW;gBAC3CpB;gBACA4B,KAAK;oBAAC3B;iBAAG;gBACT4B,SAASF,UAAU,QAAQ,QAAQ;oBAACA,MAAMG,KAAK,CAAC,QAAQC,MAAM;iBAAE;YAClE;YACAvB,gBAAgB,oBAAoB;gBAAEnB,SAAS;gBAAW2C,SAAS;YAAM;QAC3E,EAAE,OAAOC,OAAO;YACdzB,gBAAgB,AAACyB,MAAgBC,OAAO,EAAE;gBAAE7C,SAAS;gBAAS8C,gBAAgB;YAAK;QACrF,SAAU;YACRhB,WAAW;QACb;IACF;IAEA,MAAMiB,WAAWrB,MAAMsB,IAAI,CAAC,CAACd,OAASA,KAAKC,IAAI,KAAK;IACpD,MAAMc,cAAcF,CAAAA,4BAAAA,SAAUZ,IAAI,MAAK,SAASY,SAASG,OAAO,GAAG,EAAE;IACrE,qBACE,MAAC/E;QACCgF,QAAQ;QACRC,MAAMnF,eAAe;QACrBoF,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,QACElC,aAAaF,0BACX,KAAC5C;gBAAOwB,SAAQ;gBAAYD,MAAK;gBAAQ0D,SAAS,IAAM7B,WAAW;0BAChE;iBAED;QACR;;0BAEA,MAACjD;gBAAM+E,SAAS;;oBACb5D,qBACC,KAAClB;wBAAWoB,SAAQ;kCACjBF,KAAK6D,MAAM,KAAK,SACb,CAAC,wCAAwC,EAAExE,kBAAkBW,MAAMF,OAAO,CAAC,CAAC,GAC5E,CAAC,kDAAkD,EAAEM,MAAM0D,GAAG,CAAC,kBAAkB,CAAC,GAClF,CAAC,oCAAoC,EAAEzE,kBAAkBW,MAAMF,OAAO,CAAC,CAAC;yBAE5E;oBACH8B,MAAMmC,GAAG,CAAC,CAAC3B;wBACV,IAAIA,KAAKC,IAAI,KAAK,QAAQ;4BACxB,qBACE,KAACvD;gCAAsBoB,SAAQ;gCAAQC,OAAM;0CAC1CiC,KAAK4B,QAAQ,GACV,+CACA5B,KAAKgB,OAAO,CAACR,MAAM,GACjB,CAAC,QAAQ,EAAER,KAAKgB,OAAO,CAACW,GAAG,CAACrC,MAAMuC,QAAQ,EAAEC,IAAI,CAAC,MAAM,yBAAyB,CAAC,GACjF;+BALQ;wBAQpB;wBACA,IAAI9B,KAAKC,IAAI,KAAK,UAAU;4BAC1B,MAAM8B,SAAS5E,qBAAqB;gCAAC6C,KAAKI,KAAK;6BAAC,EAAEd,MAAMuC,QAAQ;4BAChE,qBACE,MAACpF;gCAAqBuF,WAAU;gCAAMR,SAAS;gCAAGvD,IAAI;oCAAEgE,YAAY;gCAAS;;kDAC3E,KAACvF;wCAAWoB,SAAQ;wCAAQG,IAAI;4CAAEiE,MAAM;4CAAGC,UAAU;wCAAE;kDACpD,GAAGJ,OAAO,aAAa,EAAE/B,KAAKoC,MAAM,IAAI,cACvCpC,KAAK3B,IAAI,GAAG,CAAC,IAAI,EAAED,KAAK4B,KAAK3B,IAAI,GAAG,GAAG,GACxC,EAAE,EAAE2B,KAAKyB,MAAM,KAAK,SAAS,kBAAkB,aAAa;;oCAE9DvC,0BACC,MAAC1C;wCACCqB,MAAK;wCACLwE,UAAU1C,YAAY;wCACtB4B,SAAS,IAAM,KAAKrB,QAAQF,KAAKG,GAAG,EAAEH,KAAKI,KAAK;;0DAEhD,KAAClE;gDAAQoG,MAAMtG,SAASsG,IAAI;gDAAEzE,MAAM;;0DACpC,KAAC1B;0DAAQ,CAAC,kBAAkB,EAAE4F,QAAQ;;;yCAEtC;;+BAfM/B,KAAKG,GAAG;wBAkBxB;wBACA,MAAMoC,OAAO7E,MAAMoD,IAAI,CAAC,CAAC0B,QAAUA,MAAM9D,EAAE,KAAKsB,KAAKyC,MAAM;wBAC3D,qBACE,KAAC/F;4BAA0BoB,SAAQ;sCAChC,GAAGX,qBAAqB6C,KAAK0C,MAAM,EAAEpD,MAAMuC,QAAQ,EAAE,eAAe,EACnEU,OAAO,CAAC,CAAC,EAAEA,KAAKI,IAAI,CAAC,CAAC,CAAC,GAAG,iBAC3B,EAAE,EAAE3C,KAAKyB,MAAM,KAAK,SAAS,kBAAkB,aAAa;2BAH9CzB,KAAKG,GAAG;oBAM7B;oBACC,CAACL,uBACA,KAACpD;wBAAWoB,SAAQ;wBAAUC,OAAM;kCACjC,CAAC,mEAAmE,EAAEC,MAAM0D,GAAG,CAAC,yCAAyC,CAAC;yBAE3H;;;0BAEN,KAACnE;gBACCqF,MAAMnD;gBACNoD,SAAS,IAAMnD,WAAW;gBAC1BjB,QAAQA;gBACR4B,KAAK;oBAAC3B;iBAAG;gBACTmB,OAAOA;gBACPP,OAAOA,MAAMA,KAAK;gBAClBwD,YAAYxD,MAAMH,KAAK;gBACvB4B,aAAaA;;;;AAIrB;AACAvC,kBAAkBL,WAAW,GAAG;AAEhC;;;CAGC,GACD,OAAO,SAAS4E,mBAAmBtF,KAMlC;IACC,MAAM,EAAEgB,MAAM,EAAE4B,GAAG,EAAEzB,MAAM,EAAEC,KAAK,EAAEwD,QAAQ,EAAE,GAAG5E;IACjD,MAAM,EAAEyB,SAAS,EAAEC,KAAK,EAAE,GAAG7B,wBAAwBuB,gBAAAA,QAASQ;IAC9D,MAAMC,QAAQxC,mBAAmB;QAAE8B;QAAQC;QAAOU,SAASL;IAAU;IACrE,MAAM,CAAC0D,MAAMI,QAAQ,GAAGpG,SAAS;IACjC,IAAI,CAACuC,SAAS,CAACD,WAAW,OAAO;IACjC,qBACE;;0BACE,KAAC5C;gBAAOuB,MAAK;gBAAQwE,UAAUA,YAAY,CAAChC,IAAIG,MAAM;gBAAEe,SAAS,IAAMyB,QAAQ;0BAC5E;;0BAEH,KAACzF;gBACCqF,MAAMA;gBACNC,SAAS,IAAMG,QAAQ;gBACvBvE,QAAQA;gBACR4B,KAAKA;gBACLR,OAAOxC,iBAAiBuB,QAAQC;gBAChCS,OAAOA,MAAMA,KAAK;gBAClBwD,YAAYxD,MAAMH,KAAK;;;;AAI/B;AACA4D,mBAAmB5E,WAAW,GAAG;AAEjC,eAAeK,kBAAiB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/record-sharing-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 { pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiClose } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, IconButton, Stack, Typography } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport { useCrmSharingRules, useCrmSharingSites } from '../hooks/use-crm-sharing'\nimport type { CrmOrgDoc } from '../hooks/use-crm-scope'\nimport {\n CRM_SHARING_OBJECT_LABELS,\n type CrmShareChip,\n crmShareChipFor,\n crmShareChipLabel,\n type CrmSharingObject,\n describeRecordSharing,\n describeShareTargets,\n} from '../model/crm-sharing'\nimport { callCrmSharing } from '../model/crm-sharing-api'\nimport { crmTaskCallScope } from '../model/task-routes'\nimport { useCanManageCrmSettings } from './settings-section'\nimport { ShareRecordsDialog } from './share-records-dialog'\n\n/**\n * The chip a list row or a record header shows when this site sees the\n * record only because it was shared with it: \"Shared by Dana\".\n */\nexport function CrmShareChipView(props: {\n chip: CrmShareChip | null\n org: CrmOrgDoc\n}) {\n const rules = useCrmSharingRules(props.org)\n if (!props.chip) return null\n return (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n color=\"info\"\n label={crmShareChipLabel(props.chip, rules)}\n sx={{ maxWidth: 220 }}\n />\n )\n}\nCrmShareChipView.displayName = 'CrmShareChipView'\n\nexport interface RecordSharingCardProps {\n object: CrmSharingObject\n id: string\n record: Readonly<Record<string, unknown>> | null | undefined\n /** The mounted site, or `null` at the organization level. */\n hostId: string | null\n orgId: string | null\n org: CrmOrgDoc\n /** The viewing site's consent-group sites, for \"shared with this site\". */\n viewingHostIds?: readonly string[]\n}\n\nconst when = (atMs: number) => (atMs > 0 ? new Date(atMs).toLocaleDateString() : '')\n\n/**\n * SHARING — where this record is visible, and why (AGL-3336).\n *\n * One line for the sites that hold it (its consent group, or the site that\n * created it), then one per hand share, naming who shared it and when, with\n * the one-click unshare, and one per sharing rule. The held line has no\n * unshare: visibility a site holds is not the sharing's to take away. A\n * rule's line is removed by editing the rule, in Settings.\n *\n * The header's \"Share with sites…\" is an org manager's, and so is the\n * unshare; every other reader sees the lines and nothing to press. For a\n * reader on a site that sees the record only through a read-only share,\n * the card says so, since the page's edits will be refused.\n */\nexport function RecordSharingCard(props: RecordSharingCardProps) {\n const { object, id, record, hostId, orgId, org, viewingHostIds = [] } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId ?? undefined)\n const sites = useCrmSharingSites({ hostId, orgId, enabled: canManage })\n const rules = useCrmSharingRules(org)\n const lines = useMemo(() => describeRecordSharing(record), [record])\n const chip = useMemo(() => crmShareChipFor(record, viewingHostIds), [record, viewingHostIds])\n const [sharing, setSharing] = useState(false)\n const [busyKey, setBusyKey] = useState<string | null>(null)\n const scope = crmTaskCallScope(hostId, orgId)\n const label = CRM_SHARING_OBJECT_LABELS[object]\n const shared = lines.some((line) => line.kind !== 'held')\n if (!record || (!shared && !(roleReady && canManage))) return null\n\n const unshare = async (key: string, token: string) => {\n if (!scope) return\n setBusyKey(key)\n try {\n await callCrmSharing(user, scope, 'unshare', {\n object,\n ids: [id],\n targets: token === 'org' ? 'all' : [token.slice('host:'.length)],\n })\n enqueueSnackbar('No longer shared', { variant: 'success', persist: false })\n } catch (error) {\n enqueueSnackbar((error as Error).message, { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyKey(null)\n }\n }\n\n const heldLine = lines.find((line) => line.kind === 'held')\n const heldHostIds = heldLine?.kind === 'held' ? heldLine.hostIds : []\n return (\n <CardDisplay\n header={'Sharing'}\n help={pluginDocsHelp('crmSharing', { anchor: '#sharing-card' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action:\n roleReady && canManage ? (\n <Button variant=\"contained\" size=\"small\" onClick={() => setSharing(true)}>\n {'Share with sites…'}\n </Button>\n ) : null,\n }}\n >\n <Stack spacing={1.5}>\n {chip ? (\n <Typography variant=\"body2\">\n {chip.access === 'edit'\n ? `Shared with this site to read and edit. ${crmShareChipLabel(chip, rules)}.`\n : `Shared with this site read-only: you can see this ${label.one} and log activity ` +\n `and tasks on it, but not change it. ${crmShareChipLabel(chip, rules)}.`}\n </Typography>\n ) : null}\n {lines.map((line) => {\n if (line.kind === 'held') {\n return (\n <Typography key=\"held\" variant=\"body2\" color=\"text.secondary\">\n {line.allSites\n ? 'Visible to every site in the organization.'\n : line.hostIds.length\n ? `Held by ${line.hostIds.map(sites.siteName).join(', ')} — where it was captured.`\n : 'Held by no site.'}\n </Typography>\n )\n }\n if (line.kind === 'manual') {\n const target = describeShareTargets([line.token], sites.siteName)\n return (\n <Stack key={line.key} direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\" sx={{ flex: 1, minWidth: 0 }}>\n {`${target} — shared by ${line.byName || 'a manager'}${\n line.atMs ? ` on ${when(line.atMs)}` : ''\n }, ${line.access === 'edit' ? 'read and edit' : 'read-only'}`}\n </Typography>\n {canManage ? (\n <IconButton\n size=\"small\"\n disabled={busyKey !== null}\n onClick={() => void unshare(line.key, line.token)}\n >\n <MdiIcon path={mdiClose.path} size={0.7} />\n <SrOnly>{`Stop sharing with ${target}`}</SrOnly>\n </IconButton>\n ) : null}\n </Stack>\n )\n }\n const rule = rules.find((entry) => entry.id === line.ruleId)\n return (\n <Typography key={line.key} variant=\"body2\">\n {`${describeShareTargets(line.tokens, sites.siteName)} — by the rule ${\n rule ? `“${rule.name}”` : 'that shares it'\n }, ${line.access === 'edit' ? 'read and edit' : 'read-only'}`}\n </Typography>\n )\n })}\n {!shared ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Not shared with any other site. Sharing lets another site see this ${label.one}; it does not share the person's consent.`}\n </Typography>\n ) : null}\n </Stack>\n <ShareRecordsDialog\n open={sharing}\n onClose={() => setSharing(false)}\n object={object}\n ids={[id]}\n scope={scope}\n sites={sites.sites}\n sitesReady={sites.ready}\n heldHostIds={heldHostIds}\n />\n </CardDisplay>\n )\n}\nRecordSharingCard.displayName = 'RecordSharingCard'\n\n/**\n * \"Share with sites…\" on a list's bulk bar (AGL-3336): the same dialog as\n * the record page, over the selection. Drawn for an org manager only.\n */\nexport function CrmBulkShareButton(props: {\n object: CrmSharingObject\n ids: readonly string[]\n hostId: string | null\n orgId: string | null\n disabled?: boolean\n}) {\n const { object, ids, hostId, orgId, disabled } = props\n const { canManage, ready } = useCanManageCrmSettings(orgId ?? undefined)\n const sites = useCrmSharingSites({ hostId, orgId, enabled: canManage })\n const [open, setOpen] = useState(false)\n if (!ready || !canManage) return null\n return (\n <>\n <Button size=\"small\" disabled={disabled || !ids.length} onClick={() => setOpen(true)}>\n {'Share with sites…'}\n </Button>\n <ShareRecordsDialog\n open={open}\n onClose={() => setOpen(false)}\n object={object}\n ids={ids}\n scope={crmTaskCallScope(hostId, orgId)}\n sites={sites.sites}\n sitesReady={sites.ready}\n />\n </>\n )\n}\nCrmBulkShareButton.displayName = 'CrmBulkShareButton'\n\nexport default RecordSharingCard\n"],"names":["pluginDocsHelp","mdiClose","CardDisplay","MdiIcon","SrOnly","useSnackbar","useUser","Button","Chip","IconButton","Stack","Typography","useMemo","useState","useCrmSharingRules","useCrmSharingSites","CRM_SHARING_OBJECT_LABELS","crmShareChipFor","crmShareChipLabel","describeRecordSharing","describeShareTargets","callCrmSharing","crmTaskCallScope","useCanManageCrmSettings","ShareRecordsDialog","CrmShareChipView","props","rules","org","chip","size","variant","color","label","sx","maxWidth","displayName","when","atMs","Date","toLocaleDateString","RecordSharingCard","object","id","record","hostId","orgId","viewingHostIds","data","user","enqueueSnackbar","canManage","ready","roleReady","undefined","sites","enabled","lines","sharing","setSharing","busyKey","setBusyKey","scope","shared","some","line","kind","unshare","key","token","ids","targets","slice","length","persist","error","message","allowDuplicate","heldLine","find","heldHostIds","hostIds","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","onClick","spacing","access","one","map","allSites","siteName","join","target","direction","alignItems","flex","minWidth","byName","disabled","path","rule","entry","ruleId","tokens","name","open","onClose","sitesReady","CrmBulkShareButton","setOpen"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,cAAc,QAAQ,eAAc;AAC7C,SAASC,QAAQ,QAAQ,yBAAwB;AACjD,SAASC,WAAW,EAAEC,OAAO,EAAEC,MAAM,QAAQ,uBAAsB;AACnE,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAASC,OAAO,QAAQ,iCAAgC;AACxD,SAASC,MAAM,EAAEC,IAAI,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAC3E,SAASC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACzC,SAASC,kBAAkB,EAAEC,kBAAkB,QAAQ,8BAA0B;AAEjF,SACEC,yBAAyB,EAEzBC,eAAe,EACfC,iBAAiB,EAEjBC,qBAAqB,EACrBC,oBAAoB,QACf,0BAAsB;AAC7B,SAASC,cAAc,QAAQ,8BAA0B;AACzD,SAASC,gBAAgB,QAAQ,0BAAsB;AACvD,SAASC,uBAAuB,QAAQ,wBAAoB;AAC5D,SAASC,kBAAkB,QAAQ,4BAAwB;AAE3D;;;CAGC,GACD,OAAO,SAASC,iBAAiBC,KAGhC;IACC,MAAMC,QAAQb,mBAAmBY,MAAME,GAAG;IAC1C,IAAI,CAACF,MAAMG,IAAI,EAAE,OAAO;IACxB,qBACE,KAACrB;QACCsB,MAAK;QACLC,SAAQ;QACRC,OAAM;QACNC,OAAOf,kBAAkBQ,MAAMG,IAAI,EAAEF;QACrCO,IAAI;YAAEC,UAAU;QAAI;;AAG1B;AACAV,iBAAiBW,WAAW,GAAG;AAc/B,MAAMC,OAAO,CAACC,OAAkBA,OAAO,IAAI,IAAIC,KAAKD,MAAME,kBAAkB,KAAK;AAEjF;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,kBAAkBf,KAA6B;IAC7D,MAAM,EAAEgB,MAAM,EAAEC,EAAE,EAAEC,MAAM,EAAEC,MAAM,EAAEC,KAAK,EAAElB,GAAG,EAAEmB,iBAAiB,EAAE,EAAE,GAAGrB;IACxE,MAAM,EAAEsB,MAAMC,IAAI,EAAE,GAAG3C;IACvB,MAAM,EAAE4C,eAAe,EAAE,GAAG7C;IAC5B,MAAM,EAAE8C,SAAS,EAAEC,OAAOC,SAAS,EAAE,GAAG9B,wBAAwBuB,gBAAAA,QAASQ;IACzE,MAAMC,QAAQxC,mBAAmB;QAAE8B;QAAQC;QAAOU,SAASL;IAAU;IACrE,MAAMxB,QAAQb,mBAAmBc;IACjC,MAAM6B,QAAQ7C,QAAQ,IAAMO,sBAAsByB,SAAS;QAACA;KAAO;IACnE,MAAMf,OAAOjB,QAAQ,IAAMK,gBAAgB2B,QAAQG,iBAAiB;QAACH;QAAQG;KAAe;IAC5F,MAAM,CAACW,SAASC,WAAW,GAAG9C,SAAS;IACvC,MAAM,CAAC+C,SAASC,WAAW,GAAGhD,SAAwB;IACtD,MAAMiD,QAAQxC,iBAAiBuB,QAAQC;IACvC,MAAMb,QAAQjB,yBAAyB,CAAC0B,OAAO;IAC/C,MAAMqB,SAASN,MAAMO,IAAI,CAAC,CAACC,OAASA,KAAKC,IAAI,KAAK;IAClD,IAAI,CAACtB,UAAW,CAACmB,UAAU,CAAEV,CAAAA,aAAaF,SAAQ,GAAK,OAAO;IAE9D,MAAMgB,UAAU,OAAOC,KAAaC;QAClC,IAAI,CAACP,OAAO;QACZD,WAAWO;QACX,IAAI;YACF,MAAM/C,eAAe4B,MAAMa,OAAO,WAAW;gBAC3CpB;gBACA4B,KAAK;oBAAC3B;iBAAG;gBACT4B,SAASF,UAAU,QAAQ,QAAQ;oBAACA,MAAMG,KAAK,CAAC,QAAQC,MAAM;iBAAE;YAClE;YACAvB,gBAAgB,oBAAoB;gBAAEnB,SAAS;gBAAW2C,SAAS;YAAM;QAC3E,EAAE,OAAOC,OAAO;YACdzB,gBAAgB,AAACyB,MAAgBC,OAAO,EAAE;gBAAE7C,SAAS;gBAAS8C,gBAAgB;YAAK;QACrF,SAAU;YACRhB,WAAW;QACb;IACF;IAEA,MAAMiB,WAAWrB,MAAMsB,IAAI,CAAC,CAACd,OAASA,KAAKC,IAAI,KAAK;IACpD,MAAMc,cAAcF,CAAAA,4BAAAA,SAAUZ,IAAI,MAAK,SAASY,SAASG,OAAO,GAAG,EAAE;IACrE,qBACE,MAAC/E;QACCgF,QAAQ;QACRC,MAAMnF,eAAe,cAAc;YAAEoF,QAAQ;QAAgB;QAC7DC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,QACEnC,aAAaF,0BACX,KAAC5C;gBAAOwB,SAAQ;gBAAYD,MAAK;gBAAQ2D,SAAS,IAAM9B,WAAW;0BAChE;iBAED;QACR;;0BAEA,MAACjD;gBAAMgF,SAAS;;oBACb7D,qBACC,KAAClB;wBAAWoB,SAAQ;kCACjBF,KAAK8D,MAAM,KAAK,SACb,CAAC,wCAAwC,EAAEzE,kBAAkBW,MAAMF,OAAO,CAAC,CAAC,GAC5E,CAAC,kDAAkD,EAAEM,MAAM2D,GAAG,CAAC,kBAAkB,CAAC,GAClF,CAAC,oCAAoC,EAAE1E,kBAAkBW,MAAMF,OAAO,CAAC,CAAC;yBAE5E;oBACH8B,MAAMoC,GAAG,CAAC,CAAC5B;wBACV,IAAIA,KAAKC,IAAI,KAAK,QAAQ;4BACxB,qBACE,KAACvD;gCAAsBoB,SAAQ;gCAAQC,OAAM;0CAC1CiC,KAAK6B,QAAQ,GACV,+CACA7B,KAAKgB,OAAO,CAACR,MAAM,GACjB,CAAC,QAAQ,EAAER,KAAKgB,OAAO,CAACY,GAAG,CAACtC,MAAMwC,QAAQ,EAAEC,IAAI,CAAC,MAAM,yBAAyB,CAAC,GACjF;+BALQ;wBAQpB;wBACA,IAAI/B,KAAKC,IAAI,KAAK,UAAU;4BAC1B,MAAM+B,SAAS7E,qBAAqB;gCAAC6C,KAAKI,KAAK;6BAAC,EAAEd,MAAMwC,QAAQ;4BAChE,qBACE,MAACrF;gCAAqBwF,WAAU;gCAAMR,SAAS;gCAAGxD,IAAI;oCAAEiE,YAAY;gCAAS;;kDAC3E,KAACxF;wCAAWoB,SAAQ;wCAAQG,IAAI;4CAAEkE,MAAM;4CAAGC,UAAU;wCAAE;kDACpD,GAAGJ,OAAO,aAAa,EAAEhC,KAAKqC,MAAM,IAAI,cACvCrC,KAAK3B,IAAI,GAAG,CAAC,IAAI,EAAED,KAAK4B,KAAK3B,IAAI,GAAG,GAAG,GACxC,EAAE,EAAE2B,KAAK0B,MAAM,KAAK,SAAS,kBAAkB,aAAa;;oCAE9DxC,0BACC,MAAC1C;wCACCqB,MAAK;wCACLyE,UAAU3C,YAAY;wCACtB6B,SAAS,IAAM,KAAKtB,QAAQF,KAAKG,GAAG,EAAEH,KAAKI,KAAK;;0DAEhD,KAAClE;gDAAQqG,MAAMvG,SAASuG,IAAI;gDAAE1E,MAAM;;0DACpC,KAAC1B;0DAAQ,CAAC,kBAAkB,EAAE6F,QAAQ;;;yCAEtC;;+BAfMhC,KAAKG,GAAG;wBAkBxB;wBACA,MAAMqC,OAAO9E,MAAMoD,IAAI,CAAC,CAAC2B,QAAUA,MAAM/D,EAAE,KAAKsB,KAAK0C,MAAM;wBAC3D,qBACE,KAAChG;4BAA0BoB,SAAQ;sCAChC,GAAGX,qBAAqB6C,KAAK2C,MAAM,EAAErD,MAAMwC,QAAQ,EAAE,eAAe,EACnEU,OAAO,CAAC,CAAC,EAAEA,KAAKI,IAAI,CAAC,CAAC,CAAC,GAAG,iBAC3B,EAAE,EAAE5C,KAAK0B,MAAM,KAAK,SAAS,kBAAkB,aAAa;2BAH9C1B,KAAKG,GAAG;oBAM7B;oBACC,CAACL,uBACA,KAACpD;wBAAWoB,SAAQ;wBAAUC,OAAM;kCACjC,CAAC,mEAAmE,EAAEC,MAAM2D,GAAG,CAAC,yCAAyC,CAAC;yBAE3H;;;0BAEN,KAACpE;gBACCsF,MAAMpD;gBACNqD,SAAS,IAAMpD,WAAW;gBAC1BjB,QAAQA;gBACR4B,KAAK;oBAAC3B;iBAAG;gBACTmB,OAAOA;gBACPP,OAAOA,MAAMA,KAAK;gBAClByD,YAAYzD,MAAMH,KAAK;gBACvB4B,aAAaA;;;;AAIrB;AACAvC,kBAAkBL,WAAW,GAAG;AAEhC;;;CAGC,GACD,OAAO,SAAS6E,mBAAmBvF,KAMlC;IACC,MAAM,EAAEgB,MAAM,EAAE4B,GAAG,EAAEzB,MAAM,EAAEC,KAAK,EAAEyD,QAAQ,EAAE,GAAG7E;IACjD,MAAM,EAAEyB,SAAS,EAAEC,KAAK,EAAE,GAAG7B,wBAAwBuB,gBAAAA,QAASQ;IAC9D,MAAMC,QAAQxC,mBAAmB;QAAE8B;QAAQC;QAAOU,SAASL;IAAU;IACrE,MAAM,CAAC2D,MAAMI,QAAQ,GAAGrG,SAAS;IACjC,IAAI,CAACuC,SAAS,CAACD,WAAW,OAAO;IACjC,qBACE;;0BACE,KAAC5C;gBAAOuB,MAAK;gBAAQyE,UAAUA,YAAY,CAACjC,IAAIG,MAAM;gBAAEgB,SAAS,IAAMyB,QAAQ;0BAC5E;;0BAEH,KAAC1F;gBACCsF,MAAMA;gBACNC,SAAS,IAAMG,QAAQ;gBACvBxE,QAAQA;gBACR4B,KAAKA;gBACLR,OAAOxC,iBAAiBuB,QAAQC;gBAChCS,OAAOA,MAAMA,KAAK;gBAClByD,YAAYzD,MAAMH,KAAK;;;;AAI/B;AACA6D,mBAAmB7E,WAAW,GAAG;AAEjC,eAAeK,kBAAiB"}
@@ -213,7 +213,9 @@ AutoCreateCompaniesCard.displayName = 'AutoCreateCompaniesCard';
213
213
  const ready = scopeReady && roleReady;
214
214
  return /*#__PURE__*/ _jsx(CardDisplay, {
215
215
  header: 'Default sharing for new records',
216
- help: pluginDocsHelp('crmSettings'),
216
+ help: pluginDocsHelp('crmSharing', {
217
+ anchor: '#access-read-only-or-read-and-edit'
218
+ }),
217
219
  contentGutterX: true,
218
220
  contentGutterY: true,
219
221
  children: /*#__PURE__*/ _jsxs(Stack, {