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

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 (105) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/activity-timeline.js +1 -0
  3. package/src/lib/components/activity-timeline.js.map +1 -1
  4. package/src/lib/components/assignment-rule-drawer.js +1 -0
  5. package/src/lib/components/assignment-rule-drawer.js.map +1 -1
  6. package/src/lib/components/companies-section.js +43 -8
  7. package/src/lib/components/companies-section.js.map +1 -1
  8. package/src/lib/components/company-edit-drawer.js +3 -1
  9. package/src/lib/components/company-edit-drawer.js.map +1 -1
  10. package/src/lib/components/contact-associations-card.js +1 -0
  11. package/src/lib/components/contact-associations-card.js.map +1 -1
  12. package/src/lib/components/contact-detail-page.js +6 -1
  13. package/src/lib/components/contact-detail-page.js.map +1 -1
  14. package/src/lib/components/contact-duplicates-card.js +1 -0
  15. package/src/lib/components/contact-duplicates-card.js.map +1 -1
  16. package/src/lib/components/contact-field-drawer.js +1 -0
  17. package/src/lib/components/contact-field-drawer.js.map +1 -1
  18. package/src/lib/components/contact-known-by-card.js +1 -0
  19. package/src/lib/components/contact-known-by-card.js.map +1 -1
  20. package/src/lib/components/contact-timeline-card.js +1 -0
  21. package/src/lib/components/contact-timeline-card.js.map +1 -1
  22. package/src/lib/components/contacts-section.js +37 -8
  23. package/src/lib/components/contacts-section.js.map +1 -1
  24. package/src/lib/components/crm-email-check.d.ts +14 -0
  25. package/src/lib/components/crm-email-check.js +38 -1
  26. package/src/lib/components/crm-email-check.js.map +1 -1
  27. package/src/lib/components/crm-glance-card.js +1 -0
  28. package/src/lib/components/crm-glance-card.js.map +1 -1
  29. package/src/lib/components/crm-next-activity-column.d.ts +5 -4
  30. package/src/lib/components/crm-next-activity-column.js +5 -4
  31. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  32. package/src/lib/components/crm-record-header.js +2 -0
  33. package/src/lib/components/crm-record-header.js.map +1 -1
  34. package/src/lib/components/deal-edit-drawer.js +3 -1
  35. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  36. package/src/lib/components/deals-section.js +51 -6
  37. package/src/lib/components/deals-section.js.map +1 -1
  38. package/src/lib/components/fields-section.js +4 -0
  39. package/src/lib/components/fields-section.js.map +1 -1
  40. package/src/lib/components/lead-history-card.js +1 -0
  41. package/src/lib/components/lead-history-card.js.map +1 -1
  42. package/src/lib/components/lead-properties-card.js +9 -3
  43. package/src/lib/components/lead-properties-card.js.map +1 -1
  44. package/src/lib/components/lead-surfaces-note.js +1 -0
  45. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  46. package/src/lib/components/leads-section.js +61 -11
  47. package/src/lib/components/leads-section.js.map +1 -1
  48. package/src/lib/components/linked-deals-card.js +1 -0
  49. package/src/lib/components/linked-deals-card.js.map +1 -1
  50. package/src/lib/components/picklist-values-card.js +1 -0
  51. package/src/lib/components/picklist-values-card.js.map +1 -1
  52. package/src/lib/components/recipes-card.js +1 -0
  53. package/src/lib/components/recipes-card.js.map +1 -1
  54. package/src/lib/components/reports/activity-card.js +1 -0
  55. package/src/lib/components/reports/activity-card.js.map +1 -1
  56. package/src/lib/components/reports/closed-deals-card.js +1 -0
  57. package/src/lib/components/reports/closed-deals-card.js.map +1 -1
  58. package/src/lib/components/reports/contacts-trend-card.js +1 -0
  59. package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
  60. package/src/lib/components/reports/lead-funnel-card.js +1 -0
  61. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  62. package/src/lib/components/reports/lead-source-card.js +1 -0
  63. package/src/lib/components/reports/lead-source-card.js.map +1 -1
  64. package/src/lib/components/reports/pipeline-card.js +1 -0
  65. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  66. package/src/lib/components/reports/source-conversion-card.js +1 -0
  67. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  68. package/src/lib/components/reports/tasks-card.js +1 -0
  69. package/src/lib/components/reports/tasks-card.js.map +1 -1
  70. package/src/lib/components/reports-section.js +2 -0
  71. package/src/lib/components/reports-section.js.map +1 -1
  72. package/src/lib/components/tasks-calendar.js +1 -0
  73. package/src/lib/components/tasks-calendar.js.map +1 -1
  74. package/src/lib/components/tasks-section.js +61 -9
  75. package/src/lib/components/tasks-section.js.map +1 -1
  76. package/src/lib/constants/company-filters.js +40 -2
  77. package/src/lib/constants/company-filters.js.map +1 -1
  78. package/src/lib/constants/contact-filters.d.ts +10 -1
  79. package/src/lib/constants/contact-filters.js +62 -2
  80. package/src/lib/constants/contact-filters.js.map +1 -1
  81. package/src/lib/constants/deal-filters.js +78 -0
  82. package/src/lib/constants/deal-filters.js.map +1 -1
  83. package/src/lib/hooks/use-crm-column-sort.d.ts +67 -0
  84. package/src/lib/hooks/use-crm-column-sort.js +156 -0
  85. package/src/lib/hooks/use-crm-column-sort.js.map +1 -0
  86. package/src/lib/hooks/use-crm-tasks.d.ts +6 -1
  87. package/src/lib/hooks/use-crm-tasks.js +10 -8
  88. package/src/lib/hooks/use-crm-tasks.js.map +1 -1
  89. package/src/lib/model/crm-list-query.d.ts +6 -0
  90. package/src/lib/model/crm-list-query.js +13 -0
  91. package/src/lib/model/crm-list-query.js.map +1 -1
  92. package/src/lib/model/lead-filters.d.ts +19 -2
  93. package/src/lib/model/lead-filters.js +112 -2
  94. package/src/lib/model/lead-filters.js.map +1 -1
  95. package/src/lib/model/native-vocabulary.d.ts +41 -0
  96. package/src/lib/model/native-vocabulary.js +59 -0
  97. package/src/lib/model/native-vocabulary.js.map +1 -0
  98. package/src/lib/model/task-views.d.ts +15 -2
  99. package/src/lib/model/task-views.js +50 -4
  100. package/src/lib/model/task-views.js.map +1 -1
  101. package/src/lib/server/email-check.d.ts +4 -1
  102. package/src/lib/server/email-check.js +18 -6
  103. package/src/lib/server/email-check.js.map +1 -1
  104. package/src/lib/transfer/tasks.js +2 -1
  105. package/src/lib/transfer/tasks.js.map +1 -1
@@ -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 { 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 { crmClientListFields, CRM_CLIENT_SEARCH_FIELDS } 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 '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 <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","CrmEmailGatewayChip","CrmRecordChip","CrmRecordHeader","CrmSendEmailButton","LeadOwnerSelect","LeadStatusChip","leadStatusChoices","leadStatusMenuItems","crmClientListFields","CRM_CLIENT_SEARCH_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\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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"}
@@ -157,6 +157,7 @@ LeadSurfaceFormList.displayName = 'LeadSurfaceFormList';
157
157
  */ export function UnroutedLeadSurfaces(props) {
158
158
  const { hostId, forms, formHref, turningOn, onTurnOn } = props;
159
159
  return /*#__PURE__*/ _jsxs(Stack, {
160
+ useFlexGap: true,
160
161
  direction: "row",
161
162
  spacing: 1,
162
163
  sx: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-surfaces-note.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { pluginRecordHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n usePluginApiPost,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack, Tooltip, Typography } from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n updateDoc,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n LEAD_SURFACE_FORMS_WINDOW,\n type LeadSurfaceForm,\n leadSurfaceForms,\n} from '../model/lead-surfaces'\n\nexport interface LeadSurfacesNoteProps {\n hostId: string\n}\n\n/** The always-on surfaces, named once wherever the note is drawn. */\nexport const LEAD_SURFACES_INTRO =\n 'Leads are created by bookings, forms with lead routing on, imports and New lead'\n/**\n * The rule that keeps one person one record (AGL-3232): a lead is the\n * whole record until it converts, and a person the workspace already holds\n * as a contact is not filed as a lead again.\n */\nexport const LEAD_SURFACES_CONTACTS_TOO =\n 'A lead is its own record until it converts into a contact. Someone who is already a contact is not filed as a lead again — their submission lands on the contact.'\n\n/** One site's forms, sorted into the two answers the note gives. */\nexport interface LeadSurfaceFormsResult {\n /** `routing.lead` is on: every submission with an address files a lead. */\n routed: LeadSurfaceForm[]\n /** The rest, each with its verdict on whether the switch may be offered. */\n unrouted: LeadSurfaceForm[]\n /** The site has more forms than the window; the lists are not the whole answer. */\n truncated: boolean\n status: ReturnType<typeof useFirestoreCollection>['status']\n}\n\n/**\n * The site's forms, ordered by id and bounded at one past the window — the\n * same read the Inbox's form picker makes, ordered by `__name__` because a\n * form saved without a display name would otherwise fall out of an ordered\n * query and vanish from a list about routing. Host-scoped by path, so any\n * member of the site may read it and no scope predicate is needed.\n *\n * One listener per call, so a note that groups several sites mounts one\n * reader per site it shows and nothing for a site it has not opened.\n */\nexport function useLeadSurfaceForms(hostId: string): LeadSurfaceFormsResult {\n const firestore = useFirestore()\n const { data: formDocs, status } = useFirestoreCollection<\n Record<string, unknown> & { $id: string }\n >(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'forms'),\n orderBy('__name__'),\n limit(LEAD_SURFACE_FORMS_WINDOW + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n return useMemo(() => {\n const forms = leadSurfaceForms(formDocs.slice(0, LEAD_SURFACE_FORMS_WINDOW))\n return {\n routed: forms.filter((form) => form.routed),\n unrouted: forms.filter((form) => !form.routed),\n truncated: formDocs.length > LEAD_SURFACE_FORMS_WINDOW,\n status,\n }\n }, [formDocs, status])\n}\n\n/** The form a switch is mid-flight on — under which site, since a form id is only unique within one. */\nexport interface LeadRoutingTarget {\n hostId: string\n formId: string\n}\n\n/**\n * The switch: one dotted `updateDoc` on `routing.lead`, so a dataset binding\n * beside it is untouched, under the same host-content write the form's page\n * makes. One at a time, whichever site the form is on — two switches in\n * flight would be two toasts racing for the same reader.\n *\n * `siteName` rides into the toast when the note shows more than one site,\n * because \"Contact now files a lead\" is an incomplete sentence to a reader\n * who can see three forms called Contact.\n */\nexport function useTurnOnLeadRouting(): {\n turningOn: LeadRoutingTarget | null\n turnOn: (target: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => Promise<void>\n} {\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const postPluginApi = usePluginApiPost()\n const [turningOn, setTurningOn] = useState<LeadRoutingTarget | null>(null)\n const turnOn = useCallback(\n async ({\n hostId,\n form,\n siteName,\n }: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => {\n if (turningOn) return\n setTurningOn({ hostId, formId: form.$id })\n const where = siteName ? ` on ${siteName}` : ''\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', form.$id), {\n 'routing.lead': true,\n updatedAt: serverTimestamp(),\n })\n // A lead-routing form holds `0` leads until its first (AGL-3330); the\n // counters are the forms plugin's to write, so it recounts them —\n // by address, since this plugin may not import that one.\n void postPluginApi('/api/forms/stats', { hostId, formIds: [form.$id] })\n enqueueSnackbar(\n `\"${form.displayName}\"${where} now files a lead from every submission ` +\n 'that carries an email address.',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar(`Lead routing could not be turned on${where}.`, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setTurningOn(null)\n }\n },\n [turningOn, firestore, enqueueSnackbar, postPluginApi],\n )\n return { turningOn, turnOn }\n}\n\n/** A form by name, linked to its own page when the console can address it. */\nexport function LeadSurfaceFormName(props: {\n form: LeadSurfaceForm\n href: string | null\n}) {\n const { form, href } = props\n return href ? (\n <AppLink href={href}>{form.displayName}</AppLink>\n ) : (\n <span>{form.displayName}</span>\n )\n}\nLeadSurfaceFormName.displayName = 'LeadSurfaceFormName'\n\n/** The routed forms as one clause — \"A, B, and possibly more\" — for a sentence to end. */\nexport function LeadSurfaceFormList(props: {\n forms: LeadSurfaceForm[]\n truncated: boolean\n formHref: (formId: string) => string | null\n}) {\n const { forms, truncated, formHref } = props\n return (\n <>\n {forms.map((form, index) => (\n <span key={form.$id}>\n {index ? ', ' : ''}\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </span>\n ))}\n {truncated ? ', and possibly more' : ''}\n </>\n )\n}\nLeadSurfaceFormList.displayName = 'LeadSurfaceFormList'\n\n/**\n * \"Not routing leads:\" — every form that does not route, with the switch\n * beside the ones that could and the reason beside the ones that cannot.\n *\n * Offered only where `leadSurfaceForms` says the publish check would honor\n * it — a form with no email field, or one that records no consent, is\n * refused with the reason as the tooltip, because a switch that flips and\n * then fails at publish is the exact confusion this note exists to remove.\n */\nexport function UnroutedLeadSurfaces(props: {\n hostId: string\n forms: LeadSurfaceForm[]\n formHref: (formId: string) => string | null\n turningOn: LeadRoutingTarget | null\n onTurnOn: (form: LeadSurfaceForm) => void\n}) {\n const { hostId, forms, formHref, turningOn, onTurnOn } = props\n return (\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Not routing leads:'}\n </Typography>\n {forms.map((form) => {\n const pending =\n turningOn?.hostId === hostId && turningOn.formId === form.$id\n return (\n <Stack\n key={form.$id}\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\" component=\"span\">\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </Typography>\n {/*\n Disabled with the reason as its tooltip rather than absent:\n an absent control and an inapplicable one look alike, and\n only one of them says what to do about it.\n */}\n <Tooltip title={form.blocker ?? ''}>\n <span>\n <Button\n size=\"small\"\n variant=\"text\"\n disabled={!form.canRoute || turningOn !== null}\n onClick={() => onTurnOn(form)}\n >\n {pending ? 'Turning on…' : 'Turn on lead routing'}\n </Button>\n </span>\n </Tooltip>\n </Stack>\n )\n })}\n </Stack>\n )\n}\nUnroutedLeadSurfaces.displayName = 'UnroutedLeadSurfaces'\n\n/**\n * WHAT CREATES A LEAD ON THIS SITE (AGL-2612).\n *\n * Every capture lands in Contacts; a LEAD is filed only by a lead surface —\n * a member sign-up, a booking, or a form whose author declared\n * `routing.lead`. The first two are always on and the third is a switch on\n * each form's own page, which left the Leads list unable to say why one\n * form's people were in it and another's were not. This says so, by name,\n * and offers the switch here for the forms that could carry it.\n *\n * The reader, the switch and the two rows are shared with the\n * organization-level note (`OrgLeadSurfacesNote`), which draws them once\n * per site; this is the one-site composition. The form links resolve the\n * site's console route from the host index, because under a site that is\n * the only address the note is handed.\n */\nexport function LeadSurfacesNote(props: LeadSurfacesNoteProps) {\n const { hostId } = props\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const { routed, unrouted, truncated, status } = useLeadSurfaceForms(hostId)\n const { turningOn, turnOn } = useTurnOnLeadRouting()\n\n const formHref = useCallback(\n (formId: string) =>\n orgSlug && host\n ? pluginRecordHref('form', { orgSlug, host }, formId)\n : null,\n [orgSlug, host],\n )\n\n return (\n <Stack spacing={0.5}>\n <Typography variant=\"body2\" color=\"text.secondary\" component=\"div\">\n {LEAD_SURFACES_INTRO}\n {status === 'loading' ? (\n '…'\n ) : routed.length ? (\n <>\n {': '}\n <LeadSurfaceFormList forms={routed} truncated={truncated} formHref={formHref} />\n {'.'}\n </>\n ) : (\n ' — no form routes leads yet.'\n )}\n {` ${LEAD_SURFACES_CONTACTS_TOO}`}\n </Typography>\n {unrouted.length ? (\n <UnroutedLeadSurfaces\n hostId={hostId}\n forms={unrouted}\n formHref={formHref}\n turningOn={turningOn}\n onTurnOn={(form) => void turnOn({ hostId, form })}\n />\n ) : null}\n </Stack>\n )\n}\nLeadSurfacesNote.displayName = 'LeadSurfacesNote'\n\nexport default LeadSurfacesNote\n"],"names":["pluginRecordHref","AppLink","useSnackbar","useConsoleHostRoute","useFirestore","useFirestoreCollection","usePluginApiPost","Button","Stack","Tooltip","Typography","collection","doc","limit","orderBy","query","serverTimestamp","updateDoc","useCallback","useMemo","useState","LEAD_SURFACE_FORMS_WINDOW","leadSurfaceForms","LEAD_SURFACES_INTRO","LEAD_SURFACES_CONTACTS_TOO","useLeadSurfaceForms","hostId","firestore","data","formDocs","status","idField","forms","slice","routed","filter","form","unrouted","truncated","length","useTurnOnLeadRouting","enqueueSnackbar","postPluginApi","turningOn","setTurningOn","turnOn","siteName","formId","$id","where","updatedAt","formIds","displayName","variant","persist","error","console","allowDuplicate","LeadSurfaceFormName","props","href","span","LeadSurfaceFormList","formHref","map","index","UnroutedLeadSurfaces","onTurnOn","direction","spacing","sx","alignItems","flexWrap","rowGap","color","pending","component","title","blocker","size","disabled","canRoute","onClick","LeadSurfacesNote","orgSlug","subdomain","host"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-surfaces-note.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { pluginRecordHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n usePluginApiPost,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack, Tooltip, Typography } from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n updateDoc,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n LEAD_SURFACE_FORMS_WINDOW,\n type LeadSurfaceForm,\n leadSurfaceForms,\n} from '../model/lead-surfaces'\n\nexport interface LeadSurfacesNoteProps {\n hostId: string\n}\n\n/** The always-on surfaces, named once wherever the note is drawn. */\nexport const LEAD_SURFACES_INTRO =\n 'Leads are created by bookings, forms with lead routing on, imports and New lead'\n/**\n * The rule that keeps one person one record (AGL-3232): a lead is the\n * whole record until it converts, and a person the workspace already holds\n * as a contact is not filed as a lead again.\n */\nexport const LEAD_SURFACES_CONTACTS_TOO =\n 'A lead is its own record until it converts into a contact. Someone who is already a contact is not filed as a lead again — their submission lands on the contact.'\n\n/** One site's forms, sorted into the two answers the note gives. */\nexport interface LeadSurfaceFormsResult {\n /** `routing.lead` is on: every submission with an address files a lead. */\n routed: LeadSurfaceForm[]\n /** The rest, each with its verdict on whether the switch may be offered. */\n unrouted: LeadSurfaceForm[]\n /** The site has more forms than the window; the lists are not the whole answer. */\n truncated: boolean\n status: ReturnType<typeof useFirestoreCollection>['status']\n}\n\n/**\n * The site's forms, ordered by id and bounded at one past the window — the\n * same read the Inbox's form picker makes, ordered by `__name__` because a\n * form saved without a display name would otherwise fall out of an ordered\n * query and vanish from a list about routing. Host-scoped by path, so any\n * member of the site may read it and no scope predicate is needed.\n *\n * One listener per call, so a note that groups several sites mounts one\n * reader per site it shows and nothing for a site it has not opened.\n */\nexport function useLeadSurfaceForms(hostId: string): LeadSurfaceFormsResult {\n const firestore = useFirestore()\n const { data: formDocs, status } = useFirestoreCollection<\n Record<string, unknown> & { $id: string }\n >(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'forms'),\n orderBy('__name__'),\n limit(LEAD_SURFACE_FORMS_WINDOW + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n return useMemo(() => {\n const forms = leadSurfaceForms(formDocs.slice(0, LEAD_SURFACE_FORMS_WINDOW))\n return {\n routed: forms.filter((form) => form.routed),\n unrouted: forms.filter((form) => !form.routed),\n truncated: formDocs.length > LEAD_SURFACE_FORMS_WINDOW,\n status,\n }\n }, [formDocs, status])\n}\n\n/** The form a switch is mid-flight on — under which site, since a form id is only unique within one. */\nexport interface LeadRoutingTarget {\n hostId: string\n formId: string\n}\n\n/**\n * The switch: one dotted `updateDoc` on `routing.lead`, so a dataset binding\n * beside it is untouched, under the same host-content write the form's page\n * makes. One at a time, whichever site the form is on — two switches in\n * flight would be two toasts racing for the same reader.\n *\n * `siteName` rides into the toast when the note shows more than one site,\n * because \"Contact now files a lead\" is an incomplete sentence to a reader\n * who can see three forms called Contact.\n */\nexport function useTurnOnLeadRouting(): {\n turningOn: LeadRoutingTarget | null\n turnOn: (target: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => Promise<void>\n} {\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const postPluginApi = usePluginApiPost()\n const [turningOn, setTurningOn] = useState<LeadRoutingTarget | null>(null)\n const turnOn = useCallback(\n async ({\n hostId,\n form,\n siteName,\n }: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => {\n if (turningOn) return\n setTurningOn({ hostId, formId: form.$id })\n const where = siteName ? ` on ${siteName}` : ''\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', form.$id), {\n 'routing.lead': true,\n updatedAt: serverTimestamp(),\n })\n // A lead-routing form holds `0` leads until its first (AGL-3330); the\n // counters are the forms plugin's to write, so it recounts them —\n // by address, since this plugin may not import that one.\n void postPluginApi('/api/forms/stats', { hostId, formIds: [form.$id] })\n enqueueSnackbar(\n `\"${form.displayName}\"${where} now files a lead from every submission ` +\n 'that carries an email address.',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar(`Lead routing could not be turned on${where}.`, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setTurningOn(null)\n }\n },\n [turningOn, firestore, enqueueSnackbar, postPluginApi],\n )\n return { turningOn, turnOn }\n}\n\n/** A form by name, linked to its own page when the console can address it. */\nexport function LeadSurfaceFormName(props: {\n form: LeadSurfaceForm\n href: string | null\n}) {\n const { form, href } = props\n return href ? (\n <AppLink href={href}>{form.displayName}</AppLink>\n ) : (\n <span>{form.displayName}</span>\n )\n}\nLeadSurfaceFormName.displayName = 'LeadSurfaceFormName'\n\n/** The routed forms as one clause — \"A, B, and possibly more\" — for a sentence to end. */\nexport function LeadSurfaceFormList(props: {\n forms: LeadSurfaceForm[]\n truncated: boolean\n formHref: (formId: string) => string | null\n}) {\n const { forms, truncated, formHref } = props\n return (\n <>\n {forms.map((form, index) => (\n <span key={form.$id}>\n {index ? ', ' : ''}\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </span>\n ))}\n {truncated ? ', and possibly more' : ''}\n </>\n )\n}\nLeadSurfaceFormList.displayName = 'LeadSurfaceFormList'\n\n/**\n * \"Not routing leads:\" — every form that does not route, with the switch\n * beside the ones that could and the reason beside the ones that cannot.\n *\n * Offered only where `leadSurfaceForms` says the publish check would honor\n * it — a form with no email field, or one that records no consent, is\n * refused with the reason as the tooltip, because a switch that flips and\n * then fails at publish is the exact confusion this note exists to remove.\n */\nexport function UnroutedLeadSurfaces(props: {\n hostId: string\n forms: LeadSurfaceForm[]\n formHref: (formId: string) => string | null\n turningOn: LeadRoutingTarget | null\n onTurnOn: (form: LeadSurfaceForm) => void\n}) {\n const { hostId, forms, formHref, turningOn, onTurnOn } = props\n return (\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Not routing leads:'}\n </Typography>\n {forms.map((form) => {\n const pending =\n turningOn?.hostId === hostId && turningOn.formId === form.$id\n return (\n <Stack\n key={form.$id}\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\" component=\"span\">\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </Typography>\n {/*\n Disabled with the reason as its tooltip rather than absent:\n an absent control and an inapplicable one look alike, and\n only one of them says what to do about it.\n */}\n <Tooltip title={form.blocker ?? ''}>\n <span>\n <Button\n size=\"small\"\n variant=\"text\"\n disabled={!form.canRoute || turningOn !== null}\n onClick={() => onTurnOn(form)}\n >\n {pending ? 'Turning on…' : 'Turn on lead routing'}\n </Button>\n </span>\n </Tooltip>\n </Stack>\n )\n })}\n </Stack>\n )\n}\nUnroutedLeadSurfaces.displayName = 'UnroutedLeadSurfaces'\n\n/**\n * WHAT CREATES A LEAD ON THIS SITE (AGL-2612).\n *\n * Every capture lands in Contacts; a LEAD is filed only by a lead surface —\n * a member sign-up, a booking, or a form whose author declared\n * `routing.lead`. The first two are always on and the third is a switch on\n * each form's own page, which left the Leads list unable to say why one\n * form's people were in it and another's were not. This says so, by name,\n * and offers the switch here for the forms that could carry it.\n *\n * The reader, the switch and the two rows are shared with the\n * organization-level note (`OrgLeadSurfacesNote`), which draws them once\n * per site; this is the one-site composition. The form links resolve the\n * site's console route from the host index, because under a site that is\n * the only address the note is handed.\n */\nexport function LeadSurfacesNote(props: LeadSurfacesNoteProps) {\n const { hostId } = props\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const { routed, unrouted, truncated, status } = useLeadSurfaceForms(hostId)\n const { turningOn, turnOn } = useTurnOnLeadRouting()\n\n const formHref = useCallback(\n (formId: string) =>\n orgSlug && host\n ? pluginRecordHref('form', { orgSlug, host }, formId)\n : null,\n [orgSlug, host],\n )\n\n return (\n <Stack spacing={0.5}>\n <Typography variant=\"body2\" color=\"text.secondary\" component=\"div\">\n {LEAD_SURFACES_INTRO}\n {status === 'loading' ? (\n '…'\n ) : routed.length ? (\n <>\n {': '}\n <LeadSurfaceFormList forms={routed} truncated={truncated} formHref={formHref} />\n {'.'}\n </>\n ) : (\n ' — no form routes leads yet.'\n )}\n {` ${LEAD_SURFACES_CONTACTS_TOO}`}\n </Typography>\n {unrouted.length ? (\n <UnroutedLeadSurfaces\n hostId={hostId}\n forms={unrouted}\n formHref={formHref}\n turningOn={turningOn}\n onTurnOn={(form) => void turnOn({ hostId, form })}\n />\n ) : null}\n </Stack>\n )\n}\nLeadSurfacesNote.displayName = 'LeadSurfacesNote'\n\nexport default LeadSurfacesNote\n"],"names":["pluginRecordHref","AppLink","useSnackbar","useConsoleHostRoute","useFirestore","useFirestoreCollection","usePluginApiPost","Button","Stack","Tooltip","Typography","collection","doc","limit","orderBy","query","serverTimestamp","updateDoc","useCallback","useMemo","useState","LEAD_SURFACE_FORMS_WINDOW","leadSurfaceForms","LEAD_SURFACES_INTRO","LEAD_SURFACES_CONTACTS_TOO","useLeadSurfaceForms","hostId","firestore","data","formDocs","status","idField","forms","slice","routed","filter","form","unrouted","truncated","length","useTurnOnLeadRouting","enqueueSnackbar","postPluginApi","turningOn","setTurningOn","turnOn","siteName","formId","$id","where","updatedAt","formIds","displayName","variant","persist","error","console","allowDuplicate","LeadSurfaceFormName","props","href","span","LeadSurfaceFormList","formHref","map","index","UnroutedLeadSurfaces","onTurnOn","useFlexGap","direction","spacing","sx","alignItems","flexWrap","rowGap","color","pending","component","title","blocker","size","disabled","canRoute","onClick","LeadSurfacesNote","orgSlug","subdomain","host"],"mappings":"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"}
@@ -32,6 +32,7 @@ import { customFieldColumns } from "./contact-custom-columns.js";
32
32
  import { useCrmViewGrid } from "../hooks/use-crm-view-grid.js";
33
33
  import { CRM_LIST_SLOTS, CrmColumnOrderProvider } from "./crm-column-menu.js";
34
34
  import { useCrmFoldsScope, useCrmListQuery } from "../hooks/use-crm-list-query.js";
35
+ import { crmViewQuerySort, useCrmColumnSort } from "../hooks/use-crm-column-sort.js";
35
36
  import CrmViewsControl from "./crm-views-control.js";
36
37
  import { CrmListActions, CrmListToolbar } from "./crm-list-toolbar.js";
37
38
  import RowActionsMenu from "@aglyn/shared-ui-jsx/components/row-actions-menu.component";
@@ -44,7 +45,7 @@ import { useRouter } from "next/navigation";
44
45
  import { useCallback, useEffect, useMemo, useState } from "react";
45
46
  import { leadPrimaryGroup } from "../model/contact-holder.js";
46
47
  import { crmRoutes } from "../model/crm-routes.js";
47
- import { LEAD_EMAIL_FILTER_OPTIONS, LEAD_FILTER_CODECS, LEAD_LIST_DECLARATION, LEAD_LIST_FILTER_FIELDS, LEAD_LIST_FILTER_HEADERS, LEAD_PICKLIST_FILTERS, LEAD_SOURCE_DIRECTION_FILTER_OPTIONS, LEAD_SOURCE_FILTER_NONE, leadClausesForGrid, leadStatusFilterOptions, LEAD_PREFIX_SEARCH, leadClauseImpliesScope, leadClausesToStore, leadQueryClause } from "../model/lead-filters.js";
48
+ import { LEAD_EMAIL_FILTER_OPTIONS, LEAD_FILTER_CODECS, LEAD_LIST_DECLARATION, LEAD_LIST_FILTER_FIELDS, LEAD_LIST_SORTS, LEAD_LIST_FILTER_HEADERS, LEAD_PICKLIST_FILTERS, LEAD_SOURCE_DIRECTION_FILTER_OPTIONS, LEAD_SOURCE_FILTER_NONE, leadClausesForGrid, leadStatusFilterOptions, LEAD_PREFIX_SEARCH, leadClauseImpliesScope, leadClausesToStore, leadQueryClause, leadStatusBase } from "../model/lead-filters.js";
48
49
  import { crmAskClauses, crmQueryRefusals } from "../model/crm-list-query.js";
49
50
  import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
50
51
  import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
@@ -263,14 +264,31 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
263
264
  foldsScope
264
265
  ]);
265
266
  const searchKey = gridFilter.searchWords.join(' ');
267
+ /*
268
+ * The Status the list always asks is its BASE (AGL-3680; see
269
+ * `leadStatusBase`), so a header order — served only with no filter on —
270
+ * is served on the Open leads the list opens with. Every other clause
271
+ * stays a filter.
272
+ */ const statusBase = useMemo(()=>leadStatusBase(asked.clauses.find((clause)=>clause.field === 'status')), [
273
+ asked.clauses
274
+ ]);
275
+ const filterClauses = useMemo(()=>asked.clauses.filter((clause)=>clause.field !== 'status'), [
276
+ asked.clauses
277
+ ]);
278
+ // The header order the view asks (AGL-3680), read before the query it orders.
279
+ const leadSort = useMemo(()=>crmViewQuerySort(LEAD_LIST_SORTS, views.state.sort, LEAD_LIST_SORTS[0]), [
280
+ views.state.sort
281
+ ]);
266
282
  const paged = useCrmListQuery({
267
283
  scope: dataRoot,
268
284
  collection: 'leads',
269
285
  visibleTo: scopeTokens,
270
286
  foldsScope,
271
287
  declaration: LEAD_LIST_DECLARATION,
272
- clauses: asked.clauses,
288
+ clauses: filterClauses,
273
289
  search: gridFilter.searchWords,
290
+ base: statusBase,
291
+ sort: leadSort.sort,
274
292
  impliesScope: leadClauseImpliesScope,
275
293
  prefixSearch: LEAD_PREFIX_SEARCH
276
294
  });
@@ -303,11 +321,13 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
303
321
  ]);
304
322
  // The list's filter, when one narrows it, for the export to read the same
305
323
  // records the list does (AGL-3528).
306
- const exportFilter = useMemo(()=>paged.plan.served.length || paged.plan.searched ? {
324
+ // The Status rides the plan's base (AGL-3680), so it narrows the export too.
325
+ const exportFilter = useMemo(()=>paged.plan.served.length || paged.plan.searched || statusBase.length ? {
307
326
  label: 'what the list shows',
308
327
  plan: paged.plan
309
328
  } : null, [
310
- paged.plan
329
+ paged.plan,
330
+ statusBase
311
331
  ]);
312
332
  const [assigning, setAssigning] = useState(null);
313
333
  const [unqualifying, setUnqualifying] = useState(null);
@@ -729,6 +749,34 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
729
749
  filterOptions
730
750
  ]);
731
751
  const grid = useCrmViewGrid(views, filterColumns, LEAD_HIDDEN_COLUMNS);
752
+ /*
753
+ * EVERY HEADER SORTS (AGL-3680): the stored ones on the query
754
+ * (`LEAD_LIST_SORTS`), the rest over the page, saying so — Status by its
755
+ * meaning's place, and the picklist columns in the order the org keeps
756
+ * their values, as a picklist sorts.
757
+ */ const pageSorts = useMemo(()=>{
758
+ const rank = (set, label)=>set && String(label != null ? label : '').trim() ? Aglyn.crmPicklistRank(set, label) : null;
759
+ return _extends({
760
+ status: (row)=>Aglyn.CRM_LEAD_STATUSES.indexOf(Aglyn.crmLeadStatus(row)),
761
+ leadSource: (row)=>rank(leadSourceList.picklist, row.leadSource)
762
+ }, Object.fromEntries(LEAD_PICKLIST_FILTERS.map((entry)=>[
763
+ entry.column,
764
+ (row)=>rank(leadPicklists.lists[entry.picklistId], row[entry.column])
765
+ ])));
766
+ }, [
767
+ leadSourceList.picklist,
768
+ leadPicklists.lists
769
+ ]);
770
+ const columnSort = useCrmColumnSort({
771
+ views,
772
+ sorts: LEAD_LIST_SORTS,
773
+ defaultSort: LEAD_LIST_SORTS[0],
774
+ asked: leadSort,
775
+ orderBy: paged.plan.orderBy,
776
+ rows,
777
+ columns: grid.columns,
778
+ pageSorts
779
+ });
732
780
  return /*#__PURE__*/ _jsxs(_Fragment, {
733
781
  children: [
734
782
  /*#__PURE__*/ _jsx(CardDisplay, {
@@ -787,7 +835,10 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
787
835
  }),
788
836
  /*#__PURE__*/ _jsx(ListQueryNotices, {
789
837
  refused: refused,
790
- notices: paged.plan.notices
838
+ notices: [
839
+ ...paged.plan.notices,
840
+ ...columnSort.notices
841
+ ]
791
842
  }),
792
843
  /*#__PURE__*/ _jsx(LeadsBulkBar, {
793
844
  rows: rows,
@@ -801,7 +852,8 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
801
852
  /*#__PURE__*/ _jsx(CrmColumnOrderProvider, {
802
853
  value: grid.columnOrder,
803
854
  children: /*#__PURE__*/ _jsx(ListTable, {
804
- rows: rows,
855
+ rows: columnSort.rows,
856
+ columnSort: columnSort,
805
857
  columns: grid.columns,
806
858
  slots: CRM_LIST_SLOTS,
807
859
  selectable: {
@@ -810,13 +862,11 @@ import OrgLeadSurfacesNote from "./org-lead-surfaces-note.js";
810
862
  },
811
863
  loading: status === 'loading',
812
864
  onOpen: (_id, row)=>router.push(routes.lead(row.leadId)),
813
- // Columns are the view's, controlled (AGL-2617). The query
814
- // orders the list — newest seen first — so the grid sorts
815
- // nothing itself: a sort over one page would reorder that page.
865
+ // Columns are the view's, controlled (AGL-2617). The headers
866
+ // sort through `columnSort` (AGL-3680): a stored column
867
+ // re-asks the query, any other sorts the page and says so.
816
868
  columnVisibilityModel: grid.columnVisibilityModel,
817
869
  onColumnVisibilityModelChange: grid.onColumnVisibilityModelChange,
818
- sortingMode: "server",
819
- disableColumnSorting: true,
820
870
  // The panel and the search are the grid's; the query answers
821
871
  // both (AGL-3321).
822
872
  filterMode: "server",