@aglyn/plugins-crm 1.0.0-beta.217 → 1.0.0-beta.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -13
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/components/companies-bulk-bar.js +12 -4
- package/src/lib/components/companies-bulk-bar.js.map +1 -1
- package/src/lib/components/company-detail-page.js +11 -0
- package/src/lib/components/company-detail-page.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +12 -2
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-import-drawer.d.ts +1 -1
- package/src/lib/components/company-picker.js +10 -3
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/contact-detail-page.d.ts +4 -4
- package/src/lib/components/contact-detail-page.js +50 -53
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -0
- package/src/lib/components/contact-list-columns.js +21 -5
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contact-merge-dialog.js +2 -1
- package/src/lib/components/contact-merge-dialog.js.map +1 -1
- package/src/lib/components/contact-timeline-card.js +4 -3
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
- package/src/lib/components/contacts-bulk-bar.js +24 -51
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +9 -3
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-export-all-button.d.ts +1 -1
- package/src/lib/components/crm-export-all-button.js.map +1 -1
- package/src/lib/components/crm-send-email-dialog.js +9 -6
- package/src/lib/components/crm-send-email-dialog.js.map +1 -1
- package/src/lib/components/csv-import-drawer.js +2 -2
- package/src/lib/components/csv-import-drawer.js.map +1 -1
- package/src/lib/components/deal-detail-page.js +18 -2
- package/src/lib/components/deal-detail-page.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +10 -0
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deals-bulk-bar.js +13 -5
- package/src/lib/components/deals-bulk-bar.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.js +6 -0
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-detail-page.js +25 -5
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +6 -0
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +4 -5
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.js +6 -0
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +28 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +36 -7
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/org-lead-surfaces-note.js +6 -2
- package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
- package/src/lib/components/recipes-card.js +9 -4
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/record-sharing-card.d.ts +57 -0
- package/src/lib/components/record-sharing-card.js +244 -0
- package/src/lib/components/record-sharing-card.js.map +1 -0
- package/src/lib/components/settings-section.js +7 -0
- package/src/lib/components/settings-section.js.map +1 -1
- package/src/lib/components/share-records-dialog.d.ts +28 -0
- package/src/lib/components/share-records-dialog.js +184 -0
- package/src/lib/components/share-records-dialog.js.map +1 -0
- package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
- package/src/lib/components/sharing-rule-drawer.js +374 -0
- package/src/lib/components/sharing-rule-drawer.js.map +1 -0
- package/src/lib/components/sharing-rules-card.d.ts +24 -0
- package/src/lib/components/sharing-rules-card.js +287 -0
- package/src/lib/components/sharing-rules-card.js.map +1 -0
- package/src/lib/components/use-crm-api.d.ts +1 -1
- package/src/lib/components/use-crm-api.js.map +1 -1
- package/src/lib/components/use-site-route-context.d.ts +25 -0
- package/src/lib/components/use-site-route-context.js +58 -0
- package/src/lib/components/use-site-route-context.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +31 -0
- package/src/lib/constants/api-routes.js +27 -1
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +37 -0
- package/src/lib/declarations.console-server.js +103 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/declarations.server.d.ts +6 -0
- package/src/lib/declarations.server.js +33 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-contact-remove.d.ts +35 -0
- package/src/lib/hooks/use-contact-remove.js +77 -0
- package/src/lib/hooks/use-contact-remove.js.map +1 -0
- package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
- package/src/lib/hooks/use-crm-sharing.js +109 -0
- package/src/lib/hooks/use-crm-sharing.js.map +1 -0
- package/src/lib/model/companies-csv.d.ts +1 -1
- package/src/lib/model/companies-csv.js +2 -2
- package/src/lib/model/companies-csv.js.map +1 -1
- package/src/lib/model/contact-holder.d.ts +68 -0
- package/src/lib/model/contact-holder.js +129 -0
- package/src/lib/model/contact-holder.js.map +1 -0
- package/src/lib/model/contact-interaction-links.d.ts +46 -0
- package/src/lib/model/contact-interaction-links.js +65 -0
- package/src/lib/model/contact-interaction-links.js.map +1 -0
- package/src/lib/model/contact-merge.d.ts +136 -0
- package/src/lib/model/contact-merge.js +412 -0
- package/src/lib/model/contact-merge.js.map +1 -0
- package/src/lib/model/contact-record.d.ts +7 -1
- package/src/lib/model/contact-record.js +6 -5
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-remove.d.ts +48 -0
- package/src/lib/model/contact-remove.js +22 -0
- package/src/lib/model/contact-remove.js.map +1 -0
- package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
- package/src/lib/model/contacts-bulk-writes.js +2 -82
- package/src/lib/model/contacts-bulk-writes.js.map +1 -1
- package/src/lib/model/contacts-csv.d.ts +2 -2
- package/src/lib/model/contacts-csv.js +2 -2
- package/src/lib/model/contacts-csv.js.map +1 -1
- package/src/lib/model/crm-csv.d.ts +239 -0
- package/src/lib/model/crm-csv.js +467 -0
- package/src/lib/model/crm-csv.js.map +1 -0
- package/src/lib/model/crm-digest.d.ts +185 -0
- package/src/lib/model/crm-digest.js +272 -0
- package/src/lib/model/crm-digest.js.map +1 -0
- package/src/lib/model/crm-import.d.ts +7 -1
- package/src/lib/model/crm-import.js +2 -1
- package/src/lib/model/crm-import.js.map +1 -1
- package/src/lib/model/crm-record-routes.d.ts +5 -0
- package/src/lib/model/crm-record-routes.js +7 -0
- package/src/lib/model/crm-record-routes.js.map +1 -1
- package/src/lib/model/crm-routes.d.ts +6 -5
- package/src/lib/model/crm-routes.js +7 -7
- package/src/lib/model/crm-routes.js.map +1 -1
- package/src/lib/model/crm-search-sources.d.ts +15 -0
- package/src/lib/model/crm-search-sources.js +162 -0
- package/src/lib/model/crm-search-sources.js.map +1 -0
- package/src/lib/model/crm-sharing-api.d.ts +32 -0
- package/src/lib/model/crm-sharing-api.js +59 -0
- package/src/lib/model/crm-sharing-api.js.map +1 -0
- package/src/lib/model/crm-sharing.d.ts +333 -0
- package/src/lib/model/crm-sharing.js +603 -0
- package/src/lib/model/crm-sharing.js.map +1 -0
- package/src/lib/model/deals-csv.d.ts +1 -1
- package/src/lib/model/deals-csv.js +4 -5
- package/src/lib/model/deals-csv.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +1 -1
- package/src/lib/model/leads-csv.d.ts +1 -1
- package/src/lib/model/leads-csv.js +1 -1
- package/src/lib/model/leads-csv.js.map +1 -1
- package/src/lib/model/tasks-csv.d.ts +1 -1
- package/src/lib/model/tasks-csv.js +1 -1
- package/src/lib/model/tasks-csv.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +75 -0
- package/src/lib/plan-entitlements.js +56 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +3 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
- package/src/lib/server/api-v1/contacts-merge.js +120 -0
- package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
- package/src/lib/server/api-v1/contacts.d.ts +18 -0
- package/src/lib/server/api-v1/contacts.js +1076 -0
- package/src/lib/server/api-v1/contacts.js.map +1 -0
- package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
- package/src/lib/server/api-v1/crm-activities.js +269 -0
- package/src/lib/server/api-v1/crm-activities.js.map +1 -0
- package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
- package/src/lib/server/api-v1/crm-companies.js +391 -0
- package/src/lib/server/api-v1/crm-companies.js.map +1 -0
- package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
- package/src/lib/server/api-v1/crm-deals.js +631 -0
- package/src/lib/server/api-v1/crm-deals.js.map +1 -0
- package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
- package/src/lib/server/api-v1/crm-email-templates.js +336 -0
- package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
- package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
- package/src/lib/server/api-v1/crm-leads.js +832 -0
- package/src/lib/server/api-v1/crm-leads.js.map +1 -0
- package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
- package/src/lib/server/api-v1/crm-pipelines.js +146 -0
- package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
- package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
- package/src/lib/server/api-v1/crm-shared.js +436 -0
- package/src/lib/server/api-v1/crm-shared.js.map +1 -0
- package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
- package/src/lib/server/api-v1/crm-tasks.js +398 -0
- package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
- package/src/lib/server/api-v1/openapi.d.ts +29 -0
- package/src/lib/server/api-v1/openapi.js +985 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/api-v1/usage.d.ts +26 -0
- package/src/lib/server/api-v1/usage.js +73 -0
- package/src/lib/server/api-v1/usage.js.map +1 -0
- package/src/lib/server/consent-group-participant.js +1 -1
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
- package/src/lib/server/contact-lifecycle-floor.js +91 -0
- package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
- package/src/lib/server/contact-remove.d.ts +75 -0
- package/src/lib/server/contact-remove.js +244 -0
- package/src/lib/server/contact-remove.js.map +1 -0
- package/src/lib/server/contact-update.js +2 -1
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.d.ts +47 -11
- package/src/lib/server/contacts-import.js +171 -71
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/contacts-merge.d.ts +1 -1
- package/src/lib/server/contacts-merge.js +3 -2
- package/src/lib/server/contacts-merge.js.map +1 -1
- package/src/lib/server/convert-host-lead.d.ts +189 -0
- package/src/lib/server/convert-host-lead.js +518 -0
- package/src/lib/server/convert-host-lead.js.map +1 -0
- package/src/lib/server/crm-sharing.d.ts +108 -0
- package/src/lib/server/crm-sharing.js +790 -0
- package/src/lib/server/crm-sharing.js.map +1 -0
- package/src/lib/server/daily-digest-route.d.ts +107 -0
- package/src/lib/server/daily-digest-route.js +588 -0
- package/src/lib/server/daily-digest-route.js.map +1 -0
- package/src/lib/server/export-route.d.ts +60 -0
- package/src/lib/server/export-route.js +447 -0
- package/src/lib/server/export-route.js.map +1 -0
- package/src/lib/server/inbound-route.d.ts +74 -0
- package/src/lib/server/inbound-route.js +257 -0
- package/src/lib/server/inbound-route.js.map +1 -0
- package/src/lib/server/lead-convert.d.ts +1 -1
- package/src/lib/server/lead-convert.js +2 -2
- package/src/lib/server/lead-convert.js.map +1 -1
- package/src/lib/server/merge-contacts.d.ts +55 -0
- package/src/lib/server/merge-contacts.js +274 -0
- package/src/lib/server/merge-contacts.js.map +1 -0
- package/src/lib/server/pipeline-record-index.d.ts +36 -0
- package/src/lib/server/pipeline-record-index.js +87 -0
- package/src/lib/server/pipeline-record-index.js.map +1 -0
- package/src/lib/server/record-facts.js +2 -1
- package/src/lib/server/record-facts.js.map +1 -1
- package/src/lib/server/task-reminders-route.d.ts +82 -0
- package/src/lib/server/task-reminders-route.js +497 -0
- package/src/lib/server/task-reminders-route.js.map +1 -0
- package/src/lib/server-deal-stage.js +2 -1
- package/src/lib/server-deal-stage.js.map +1 -1
- package/src/lib/server.d.ts +5 -2
- package/src/lib/server.js +47 -2
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
- package/src/lib/testing/stand-in-site-record-routes.js +55 -0
- package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
- package/src/lib/usage-axes.d.ts +35 -0
- package/src/lib/usage-axes.js +68 -0
- package/src/lib/usage-axes.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +0 -24
- package/src/lib/components/use-emails-hub-path.js +0 -59
- package/src/lib/components/use-emails-hub-path.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-campaigns-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 {\n campaignMembershipUnchanged,\n campaignMembershipValue,\n pluginDocsHelp,\n readCampaignIds,\n} from '@aglyn/aglyn'\nimport CampaignPicker, {\n type CampaignPickerOption,\n} from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack } from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { crmClientListFields } from '../model/crm-list-query'\n\n/** The list field a campaign filing moves (AGL-3321). */\nconst LEAD_CAMPAIGN_LIST_FIELDS = ['scopedCampaignIds'] as const\n\n/** The helper line under the picker — the contact card's, word for word. */\nexport const LEAD_CAMPAIGNS_HELPER_TEXT =\n 'Your own filing. It never adds anyone to a send — a campaign mails its lists.'\n\n/**\n * The campaigns a lead is filed under, as NAMES for the record header.\n *\n * Resolved from the site's containers; an id with no live container — a\n * campaign the console deleted, or one still loading — is left out rather\n * than shown, because a document id on a chip tells a reader nothing and\n * looks like a fault. The picker below keeps such an id visible as itself,\n * so the filing is never silently lost; the header only ever names.\n */\nexport function leadCampaignNames(\n lead: Record<string, unknown> | null | undefined,\n options: readonly CampaignPickerOption[],\n): string[] {\n const labels = new Map(options.map((option) => [option.value, option.label]))\n return readCampaignIds(lead)\n .map((id) => labels.get(id))\n .filter((name): name is string => Boolean(name))\n}\n\nexport interface LeadCampaignsCardProps {\n /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Read for the org root alone — the containers\n * themselves are handed down as `options` — so the card serves both\n * levels; the org-level mount answers the same org without a site.\n */\n hostId: string | null\n leadId: string\n lead: Record<string, unknown>\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n /** The site's campaigns, read once by the page for this card and the header. */\n options: readonly CampaignPickerOption[]\n /** The campaign read has answered — false while it settles. */\n optionsReady: boolean\n /**\n * What a saved filing changed, once it has landed: the ids added to and\n * removed from the lead's membership. The page files the activity\n * entries (AGL-3274); the card only knows the document.\n */\n onFiled?: (change: { added: string[]; removed: string[] }) => void\n}\n\n/**\n * WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).\n *\n * The contact's Relationship card has carried this picker since AGL-2596;\n * the lead's page had none, so a lead filed under a campaign by the New\n * lead drawer, the bulk bar or a sequence's enroll showed nothing of it —\n * the one field on the record the reader had no way to see or change.\n *\n * The same control, the same value helper, the same helper line. The save\n * is the lead's own client door — a one-field `update` on\n * `hosts/{hostId}/leads`, as the properties card writes status and notes —\n * guarded by the seed like every draft edited over a cached read, and\n * `campaignMembershipUnchanged` keeps Save quiet while the picker only\n * reorders what is stored. **Save filing** sits in the card header.\n *\n * It is FILING, not audience: the helper says so, in the sentence the\n * contact card uses, because a reader who finds a campaign picker on a\n * person's page assumes the opposite.\n */\nexport function LeadCampaignsCard(props: LeadCampaignsCardProps) {\n const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const stored = readCampaignIds(lead)\n const [selected, setSelected] = useState<string[]>(stored)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n // A newer membership from the server replaces an UNEDITED selection — a\n // sequence's enroll or a bulk add lands while the page is open — and an\n // edited one is the reader's, decided by the guard on save.\n const storedKey = stored.join(',')\n useEffect(() => {\n if (!dirty) setSelected(stored)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storedKey, dirty])\n\n const unchanged = campaignMembershipUnchanged(stored, selected)\n const empty = optionsReady && !options.length\n\n const save = async () => {\n if (unchanged) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setSaving(true)\n const next = campaignMembershipValue(selected)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n campaignIds: next,\n // The Leads list's Campaign filter under a site reads these\n // (AGL-3321), from the lead with its new campaigns.\n ...crmClientListFields('leads', lead, { campaignIds: next }, LEAD_CAMPAIGN_LIST_FIELDS),\n updatedAt: serverTimestamp(),\n })\n },\n )\n setSaving(false)\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The filing could not be saved.', { variant: 'warning' })\n return\n }\n setDirty(false)\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n onFiled?.({\n added: next.filter((id) => !stored.includes(id)),\n removed: stored.filter((id) => !next.includes(id)),\n })\n }\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: empty ? null : (\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || unchanged}\n onClick={() => void save().catch((error: unknown) => {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n setSaving(false)\n })}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <CampaignPicker\n options={options}\n value={selected}\n onChange={(next) => {\n setSelected(next)\n setDirty(true)\n }}\n label=\"Filed under campaigns\"\n helperText={LEAD_CAMPAIGNS_HELPER_TEXT}\n disabled={saving}\n empty={empty}\n emptyText=\"There are no campaigns to file this lead under yet. Create one from Marketing.\"\n />\n </Stack>\n </CardDisplay>\n )\n}\nLeadCampaignsCard.displayName = 'LeadCampaignsCard'\n\nexport default LeadCampaignsCard\n"],"names":["campaignMembershipUnchanged","campaignMembershipValue","pluginDocsHelp","readCampaignIds","CampaignPicker","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Stack","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","crmClientListFields","LEAD_CAMPAIGN_LIST_FIELDS","LEAD_CAMPAIGNS_HELPER_TEXT","leadCampaignNames","lead","options","labels","Map","map","option","value","label","id","get","filter","name","Boolean","LeadCampaignsCard","props","hostId","leadId","leadStatus","fromCache","optionsReady","onFiled","orgId","firestore","enqueueSnackbar","stored","selected","setSelected","dirty","setDirty","saving","setSaving","storedKey","join","unchanged","empty","length","save","variant","persist","next","verdict","subject","unreadable","campaignIds","updatedAt","ok","message","added","includes","removed","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","disabled","onClick","catch","error","console","Error","allowDuplicate","spacing","onChange","helperText","emptyText","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-campaigns-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 {\n campaignMembershipUnchanged,\n campaignMembershipValue,\n pluginDocsHelp,\n readCampaignIds,\n} from '@aglyn/aglyn'\nimport CampaignPicker, {\n type CampaignPickerOption,\n} from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack } from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { crmClientListFields } from '../model/crm-list-query'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\n/** The list field a campaign filing moves (AGL-3321). */\nconst LEAD_CAMPAIGN_LIST_FIELDS = ['scopedCampaignIds'] as const\n\n/** The helper line under the picker — the contact card's, word for word. */\nexport const LEAD_CAMPAIGNS_HELPER_TEXT =\n 'Your own filing. It never adds anyone to a send — a campaign mails its lists.'\n\n/**\n * The campaigns a lead is filed under, as NAMES for the record header.\n *\n * Resolved from the site's containers; an id with no live container — a\n * campaign the console deleted, or one still loading — is left out rather\n * than shown, because a document id on a chip tells a reader nothing and\n * looks like a fault. The picker below keeps such an id visible as itself,\n * so the filing is never silently lost; the header only ever names.\n */\nexport function leadCampaignNames(\n lead: Record<string, unknown> | null | undefined,\n options: readonly CampaignPickerOption[],\n): string[] {\n const labels = new Map(options.map((option) => [option.value, option.label]))\n return readCampaignIds(lead)\n .map((id) => labels.get(id))\n .filter((name): name is string => Boolean(name))\n}\n\nexport interface LeadCampaignsCardProps {\n /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Read for the org root alone — the containers\n * themselves are handed down as `options` — so the card serves both\n * levels; the org-level mount answers the same org without a site.\n */\n hostId: string | null\n leadId: string\n lead: Record<string, unknown>\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n /** The site's campaigns, read once by the page for this card and the header. */\n options: readonly CampaignPickerOption[]\n /** The campaign read has answered — false while it settles. */\n optionsReady: boolean\n /**\n * What a saved filing changed, once it has landed: the ids added to and\n * removed from the lead's membership. The page files the activity\n * entries (AGL-3274); the card only knows the document.\n */\n onFiled?: (change: { added: string[]; removed: string[] }) => void\n}\n\n/**\n * WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).\n *\n * The contact's Relationship card has carried this picker since AGL-2596;\n * the lead's page had none, so a lead filed under a campaign by the New\n * lead drawer, the bulk bar or a sequence's enroll showed nothing of it —\n * the one field on the record the reader had no way to see or change.\n *\n * The same control, the same value helper, the same helper line. The save\n * is the lead's own client door — a one-field `update` on\n * `hosts/{hostId}/leads`, as the properties card writes status and notes —\n * guarded by the seed like every draft edited over a cached read, and\n * `campaignMembershipUnchanged` keeps Save quiet while the picker only\n * reorders what is stored. **Save filing** sits in the card header.\n *\n * It is FILING, not audience: the helper says so, in the sentence the\n * contact card uses, because a reader who finds a campaign picker on a\n * person's page assumes the opposite.\n */\nexport function LeadCampaignsCard(props: LeadCampaignsCardProps) {\n const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const stored = readCampaignIds(lead)\n const [selected, setSelected] = useState<string[]>(stored)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n // A newer membership from the server replaces an UNEDITED selection — a\n // sequence's enroll or a bulk add lands while the page is open — and an\n // edited one is the reader's, decided by the guard on save.\n const storedKey = stored.join(',')\n useEffect(() => {\n if (!dirty) setSelected(stored)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storedKey, dirty])\n\n const unchanged = campaignMembershipUnchanged(stored, selected)\n const empty = optionsReady && !options.length\n\n const save = async () => {\n if (unchanged) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setSaving(true)\n const next = campaignMembershipValue(selected)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n campaignIds: next,\n // The Leads list's Campaign filter under a site reads these\n // (AGL-3321), from the lead with its new campaigns.\n ...crmClientListFields('leads', lead, { campaignIds: next }, LEAD_CAMPAIGN_LIST_FIELDS),\n updatedAt: serverTimestamp(),\n })\n },\n )\n setSaving(false)\n // A rule may share by campaign (AGL-3336).\n if (verdict.ok) followUpSharing('leads', [leadId])\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The filing could not be saved.', { variant: 'warning' })\n return\n }\n setDirty(false)\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n onFiled?.({\n added: next.filter((id) => !stored.includes(id)),\n removed: stored.filter((id) => !next.includes(id)),\n })\n }\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: empty ? null : (\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || unchanged}\n onClick={() => void save().catch((error: unknown) => {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n setSaving(false)\n })}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <CampaignPicker\n options={options}\n value={selected}\n onChange={(next) => {\n setSelected(next)\n setDirty(true)\n }}\n label=\"Filed under campaigns\"\n helperText={LEAD_CAMPAIGNS_HELPER_TEXT}\n disabled={saving}\n empty={empty}\n emptyText=\"There are no campaigns to file this lead under yet. Create one from Marketing.\"\n />\n </Stack>\n </CardDisplay>\n )\n}\nLeadCampaignsCard.displayName = 'LeadCampaignsCard'\n\nexport default LeadCampaignsCard\n"],"names":["campaignMembershipUnchanged","campaignMembershipValue","pluginDocsHelp","readCampaignIds","CampaignPicker","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Stack","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","crmClientListFields","useCrmSharingFollowUp","LEAD_CAMPAIGN_LIST_FIELDS","LEAD_CAMPAIGNS_HELPER_TEXT","leadCampaignNames","lead","options","labels","Map","map","option","value","label","id","get","filter","name","Boolean","LeadCampaignsCard","props","hostId","leadId","leadStatus","fromCache","optionsReady","onFiled","orgId","followUpSharing","firestore","enqueueSnackbar","stored","selected","setSelected","dirty","setDirty","saving","setSaving","storedKey","join","unchanged","empty","length","save","variant","persist","next","verdict","subject","unreadable","campaignIds","updatedAt","ok","message","added","includes","removed","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","disabled","onClick","catch","error","console","Error","allowDuplicate","spacing","onChange","helperText","emptyText","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SACEA,2BAA2B,EAC3BC,uBAAuB,EACvBC,cAAc,EACdC,eAAe,QACV,eAAc;AACrB,OAAOC,oBAEA,wEAAuE;AAC9E,SAASC,WAAW,QAAQ,uBAAsB;AAClD,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAEEC,YAAY,EACZC,kBAAkB,QACb,iCAAgC;AACvC,SAASC,MAAM,EAAEC,KAAK,QAAQ,gBAAe;AAC7C,SAASC,GAAG,EAAEC,eAAe,EAAEC,SAAS,QAAQ,qBAAoB;AACpE,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,WAAW,QAAQ,4BAAwB;AACpD,SAASC,mBAAmB,QAAQ,6BAAyB;AAC7D,SAASC,qBAAqB,QAAQ,8BAA0B;AAEhE,uDAAuD,GACvD,MAAMC,4BAA4B;IAAC;CAAoB;AAEvD,0EAA0E,GAC1E,OAAO,MAAMC,6BACX,gFAA+E;AAEjF;;;;;;;;CAQC,GACD,OAAO,SAASC,kBACdC,IAAgD,EAChDC,OAAwC;IAExC,MAAMC,SAAS,IAAIC,IAAIF,QAAQG,GAAG,CAAC,CAACC,SAAW;YAACA,OAAOC,KAAK;YAAED,OAAOE,KAAK;SAAC;IAC3E,OAAO1B,gBAAgBmB,MACpBI,GAAG,CAAC,CAACI,KAAON,OAAOO,GAAG,CAACD,KACvBE,MAAM,CAAC,CAACC,OAAyBC,QAAQD;AAC9C;AA2BA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASE,kBAAkBC,KAA6B;IAC7D,MAAM,EAAEC,MAAM,EAAEC,MAAM,EAAEhB,IAAI,EAAEiB,UAAU,EAAEC,SAAS,EAAEjB,OAAO,EAAEkB,YAAY,EAAEC,OAAO,EAAE,GAAGN;IACxF,0EAA0E;IAC1E,4DAA4D;IAC5D,MAAM,EAAEO,KAAK,EAAE,GAAG3B,YAAY;QAAEqB;IAAO;IACvC,MAAMO,kBAAkB1B,sBAAsBmB,QAAQM;IACtD,MAAME,YAAYtC;IAClB,MAAM,EAAEuC,eAAe,EAAE,GAAGxC;IAC5B,MAAMyC,SAAS5C,gBAAgBmB;IAC/B,MAAM,CAAC0B,UAAUC,YAAY,GAAGlC,SAAmBgC;IACnD,MAAM,CAACG,OAAOC,SAAS,GAAGpC,SAAS;IACnC,MAAM,CAACqC,QAAQC,UAAU,GAAGtC,SAAS;IACrC,wEAAwE;IACxE,wEAAwE;IACxE,4DAA4D;IAC5D,MAAMuC,YAAYP,OAAOQ,IAAI,CAAC;IAC9BzC,UAAU;QACR,IAAI,CAACoC,OAAOD,YAAYF;IACxB,uDAAuD;IACzD,GAAG;QAACO;QAAWJ;KAAM;IAErB,MAAMM,YAAYxD,4BAA4B+C,QAAQC;IACtD,MAAMS,QAAQhB,gBAAgB,CAAClB,QAAQmC,MAAM;IAE7C,MAAMC,OAAO;QACX,IAAIH,WAAW;QACf,IAAI,CAACb,OAAO;YACVG,gBAAgB,yDAAyD;gBACvEc,SAAS;gBACTC,SAAS;YACX;YACA;QACF;QACAR,UAAU;QACV,MAAMS,OAAO7D,wBAAwB+C;QACrC,MAAMe,UAAU,MAAMvD,mBACpB;YAAEwD,SAAS;YAAQxB;YAAWyB,YAAY1B,eAAe;QAAQ,GACjE;YACE,MAAM1B,UAAUF,IAAIkC,WAAW,QAAQF,OAAO,SAASL,SAAS;gBAC9D4B,aAAaJ;eAGV7C,oBAAoB,SAASK,MAAM;gBAAE4C,aAAaJ;YAAK,GAAG3C;gBAC7DgD,WAAWvD;;QAEf;QAEFyC,UAAU;QACV,2CAA2C;QAC3C,IAAIU,QAAQK,EAAE,EAAExB,gBAAgB,SAAS;YAACN;SAAO;QACjD,IAAI,CAACyB,QAAQK,EAAE,EAAE;gBACCL;YAAhBjB,iBAAgBiB,mBAAAA,QAAQM,OAAO,YAAfN,mBAAmB,kCAAkC;gBAAEH,SAAS;YAAU;YAC1F;QACF;QACAT,SAAS;QACTL,gBAAgB,gBAAgB;YAAEc,SAAS;YAAWC,SAAS;QAAM;QACrEnB,2BAAAA,QAAU;YACR4B,OAAOR,KAAK9B,MAAM,CAAC,CAACF,KAAO,CAACiB,OAAOwB,QAAQ,CAACzC;YAC5C0C,SAASzB,OAAOf,MAAM,CAAC,CAACF,KAAO,CAACgC,KAAKS,QAAQ,CAACzC;QAChD;IACF;IAEA,qBACE,KAACzB;QACCoE,QAAQ;QACRC,MAAMxE,eAAe,YAAY;YAAEyE,QAAQ;QAAgB;QAC3DC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,QAAQtB,QAAQ,qBACd,KAAChD;gBACCuE,MAAK;gBACLpB,SAAQ;gBACRqB,UAAU7B,UAAUI;gBACpB0B,SAAS,IAAM,KAAKvB,OAAOwB,KAAK,CAAC,CAACC;wBAChCC,QAAQD,KAAK,CAACA;wBACdtC,gBACEsC,iBAAiBE,SAASF,MAAMf,OAAO,GAAGe,MAAMf,OAAO,GAAG,yBAC1D;4BAAET,SAAS;4BAAS2B,gBAAgB;wBAAK;wBAE3ClC,UAAU;oBACZ;0BAECD,SAAS,YAAY;;QAG5B;kBAEA,cAAA,KAAC1C;YAAM8E,SAAS;sBACd,cAAA,KAACpF;gBACCmB,SAASA;gBACTK,OAAOoB;gBACPyC,UAAU,CAAC3B;oBACTb,YAAYa;oBACZX,SAAS;gBACX;gBACAtB,OAAM;gBACN6D,YAAYtE;gBACZ6D,UAAU7B;gBACVK,OAAOA;gBACPkC,WAAU;;;;AAKpB;AACAxD,kBAAkByD,WAAW,GAAG;AAEhC,eAAezD,kBAAiB"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
-
import { pluginDocsHelp,
|
|
18
|
+
import { pluginDocsHelp, normalizeContactEmail, readErasureRequestedAtMs } from "@aglyn/aglyn";
|
|
19
19
|
import { useFirestore, useFirestoreDoc } from "@aglyn/tenant-feature-instance";
|
|
20
20
|
import { Stack, Typography } from "@mui/material";
|
|
21
21
|
import { doc } from "firebase/firestore";
|
|
@@ -35,6 +35,9 @@ import { LeadHistoryCard } from "./lead-history-card.js";
|
|
|
35
35
|
import { LeadPropertiesCard } from "./lead-properties-card.js";
|
|
36
36
|
import { LeadUnqualifyDialog } from "./lead-unqualify-dialog.js";
|
|
37
37
|
import { RecordActivityCard } from "./record-activity-card.js";
|
|
38
|
+
import { CrmShareChipView, RecordSharingCard } from "./record-sharing-card.js";
|
|
39
|
+
import { crmShareChipFor } from "../model/crm-sharing.js";
|
|
40
|
+
import { leadPrimaryGroup } from "../model/contact-holder.js";
|
|
38
41
|
/**
|
|
39
42
|
* `/crm/leads/{leadId}` — one lead (AGL-2608).
|
|
40
43
|
*
|
|
@@ -58,6 +61,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
58
61
|
* the same answer `contactPrimaryGroup` gives the contact page, and the
|
|
59
62
|
* same one the Leads list already converts a row as.
|
|
60
63
|
*/ export function LeadDetailPage(props) {
|
|
64
|
+
var _ref;
|
|
61
65
|
const { id, hostId, org, basePath } = props;
|
|
62
66
|
const firestore = useFirestore();
|
|
63
67
|
const routes = crmRoutes(basePath);
|
|
@@ -186,10 +190,17 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
186
190
|
// The campaigns the lead is filed under (AGL-3274), by name
|
|
187
191
|
// beside the status. An id no container answers for draws no
|
|
188
192
|
// chip: the card below keeps it, the header only names.
|
|
189
|
-
extraChips:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
extraChips: [
|
|
194
|
+
// Seen here only through a share (AGL-3336): who shared it.
|
|
195
|
+
/*#__PURE__*/ _jsx(CrmShareChipView, {
|
|
196
|
+
chip: crmShareChipFor(lead, (_ref = viewingGroup == null ? void 0 : viewingGroup.hostIds) != null ? _ref : []),
|
|
197
|
+
org: org
|
|
198
|
+
}, "shared"),
|
|
199
|
+
...leadCampaignNames(lead, campaigns.options).map((name)=>/*#__PURE__*/ _jsx(CrmRecordChip, {
|
|
200
|
+
label: "Campaign",
|
|
201
|
+
value: name
|
|
202
|
+
}, name))
|
|
203
|
+
]
|
|
193
204
|
}),
|
|
194
205
|
/*#__PURE__*/ _jsx(CrmRecordInsightsZone, {
|
|
195
206
|
hostId: siteHostId,
|
|
@@ -228,6 +239,15 @@ import { RecordActivityCard } from "./record-activity-card.js";
|
|
|
228
239
|
leadId: id,
|
|
229
240
|
lead: lead
|
|
230
241
|
}),
|
|
242
|
+
/*#__PURE__*/ _jsx(RecordSharingCard, {
|
|
243
|
+
object: "leads",
|
|
244
|
+
id: id,
|
|
245
|
+
record: lead,
|
|
246
|
+
hostId: hostId != null ? hostId : null,
|
|
247
|
+
orgId: orgId,
|
|
248
|
+
org: org,
|
|
249
|
+
viewingHostIds: viewingGroup == null ? void 0 : viewingGroup.hostIds
|
|
250
|
+
}),
|
|
231
251
|
/*#__PURE__*/ _jsx(RecordActivityCard, {
|
|
232
252
|
hostId: siteHostId,
|
|
233
253
|
org: org,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-detail-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n pluginDocsHelp,\n type CrmLeadFields,\n leadPrimaryGroup,\n normalizeContactEmail,\n readErasureRequestedAtMs,\n} from '@aglyn/aglyn'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport { Stack, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useMemo, useState } from 'react'\nimport { useCampaignFilingLog } from '../hooks/use-campaign-filing-log'\nimport { useCrmCampaigns } from '../hooks/use-crm-campaigns'\nimport { CrmCreateSiteDefault } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useOrgMemberOptions } from '../hooks/use-org-member-options'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { useErasePersonAction } from './erase-person-action'\nimport { LeadCampaignsCard, leadCampaignNames } from './lead-campaigns-card'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport { LeadHistoryCard } from './lead-history-card'\nimport { LeadPropertiesCard } from './lead-properties-card'\nimport { LeadUnqualifyDialog } from './lead-unqualify-dialog'\nimport { RecordActivityCard } from './record-activity-card'\n\ntype LeadDocument = Record<string, unknown> & CrmLeadFields\n\n/**\n * `/crm/leads/{leadId}` — one lead (AGL-2608).\n *\n * The record behind a row of the Leads section: what the person did on the\n * site, who is working them, and the conversion that turns them into a\n * contact, a company and a deal. One document listen and one roster request\n * for the owner picker; the convert dialog's reads open only when it does.\n *\n * ## The two levels, and the site a lead is acted on as (AGL-3278)\n *\n * The org root comes from `useCrmScope`, as it does on every other CRM\n * record page: under a site it is looked up from that site, and at the\n * organization level it is the mount's, known at once. It used to come\n * from `useOrgDataScope({ hostId })` — correct only under a site, because\n * an absent host settles at `orgId: null`, and a null org builds no\n * reference, opens no listener and leaves the page loading forever.\n *\n * Several surfaces still need ONE site: the feed an activity is filed in, the booking door, the consent basis, the\n * conversion. Under a site that is the mounted one. At the organization\n * level it is the lead's own first capturing site — `leadPrimaryGroup` —\n * the same answer `contactPrimaryGroup` gives the contact page, and the\n * same one the Leads list already converts a row as.\n */\nexport function LeadDetailPage(props: CrmDetailPageProps) {\n const { id, hostId, org, basePath } = props\n const firestore = useFirestore()\n const routes = crmRoutes(basePath)\n /*\n * Resolved BEFORE the read that needs it (AGL-3275). A lead is an org row\n * now, so the org has to be known to address one — and `scope` is null\n * while the host-index lookup settles. Handing that null to `doc()` is a\n * thrown `TypeError`, which the page renders as a 500 rather than as a\n * record still loading.\n */\n const { scope, orgId, ready, consentGroup: viewingGroup } = useCrmScope({\n hostId,\n org,\n })\n const {\n data: lead,\n status,\n fromCache,\n } = useFirestoreDoc<LeadDocument>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'leads', id) : null),\n [firestore, scope, id],\n )\n /*\n * The group this lead is READ through: the mounted site's under a site,\n * and the lead's own first capturing site at the organization level\n * (AGL-3278). The consent basis is read against it — a refusal recorded\n * for any sibling in a declared group stands against every one of them,\n * so a group of one would report a person as opted in who had\n * unsubscribed from the brand beside this one.\n */\n const leadGroup = useMemo(\n () => viewingGroup ?? leadPrimaryGroup(lead, org as Record<string, unknown>),\n [viewingGroup, lead, org],\n )\n /** The one site this page acts as, or null for a lead no site captured. */\n const siteHostId = hostId ?? (leadGroup.hostId || null)\n const roster = useOrgMemberOptions(orgId)\n /*\n * The campaigns, read once for the page (AGL-3274): the header names the\n * ones the lead is filed under and the Campaigns card offers them. One\n * listener, not one per surface. Under a site, the campaigns placed on\n * it; at the organization level the lead is the org's record and so is\n * every campaign it may be filed under.\n */\n const campaigns = useCrmCampaigns({ hostId, orgId }, { enabled: true })\n // A saved filing is written on the lead's Activity too (AGL-3274), one\n // entry per campaign added or removed, by the member who saved it.\n const logFiling = useCampaignFilingLog({\n orgId,\n hostId: siteHostId,\n org: org as Record<string, unknown> | undefined,\n })\n const [converting, setConverting] = useState(false)\n const [unqualifying, setUnqualifying] = useState(false)\n // The privacy erasure (AGL-2623), offered from the lead as from the\n // contact: the same request, filed by the lead's address.\n const leadEmail = lead ? normalizeContactEmail(lead['email']) : null\n const erase = useErasePersonAction({\n hostId: siteHostId,\n orgId,\n subject: lead && leadEmail ? { kind: 'lead', id, email: leadEmail } : null,\n requestedAtMs: readErasureRequestedAtMs(lead),\n })\n\n const label = lead ? String(lead['name'] || lead['email'] || id) : undefined\n\n /*\n * A SETTLED lookup with no org is an answer, not a wait (AGL-3278). The\n * spinner below is for a read in flight; standing on it once `ready` has\n * said there is no org to read from is the endless \"Loading…\" this page\n * shipped with.\n */\n const noOrg = ready && !scope\n if (noOrg || status === 'error' || (status === 'success' && !lead)) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {noOrg || status === 'error'\n ? 'This lead could not be read.'\n : 'This lead no longer exists — it may have been removed from the Inbox.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n if (!lead) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n loading\n />\n )\n }\n\n return (\n /* A task or an activity created from this page defaults to the lead's\n own site, not to the site the reader last picked in a list's drawer\n (AGL-2630) — as on every other record page. */\n <CrmCreateSiteDefault hostId={siteHostId}>\n {/* The properties card is the record's lead card: it publishes the\n page heading and the trail, so the history card under it says what\n it holds rather than repeating the name. */}\n <Stack spacing={3}>\n <LeadPropertiesCard\n hostId={siteHostId}\n consentGroup={leadGroup}\n orgId={orgId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n basePath={basePath}\n roster={roster}\n onConvert={() => setConverting(true)}\n onUnqualify={() => setUnqualifying(true)}\n extraMenuItems={erase.menuItems}\n banner={erase.banner}\n erasurePending={erase.pendingSinceMs !== null}\n org={org}\n // The campaigns the lead is filed under (AGL-3274), by name\n // beside the status. An id no container answers for draws no\n // chip: the card below keeps it, the header only names.\n extraChips={leadCampaignNames(lead, campaigns.options).map((name) => (\n <CrmRecordChip key={name} label=\"Campaign\" value={name} />\n ))}\n />\n {/* What an assistant says about where the lead stands (AGL-2917): read on its own site. */}\n <CrmRecordInsightsZone\n hostId={siteHostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"lead\"\n recordId={id}\n name={label ?? ''}\n />\n <LeadCampaignsCard\n hostId={siteHostId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n options={campaigns.options}\n optionsReady={campaigns.ready}\n onFiled={({ added, removed }) => {\n const named = (ids: string[]) =>\n ids.map((campaignId) => ({\n id: campaignId,\n name: campaigns.options.find((option) => option.value === campaignId)?.label ?? '',\n }))\n void logFiling({ leadId: id }, { filed: named(added), removed: named(removed) })\n }}\n />\n <LeadHistoryCard hostId={siteHostId} leadId={id} lead={lead} />\n <RecordActivityCard hostId={siteHostId} org={org} leadId={id} />\n </Stack>\n <LeadConvertDialog\n open={converting}\n onClose={() => setConverting(false)}\n hostId={siteHostId}\n orgId={orgId}\n org={org as Record<string, unknown> | undefined}\n leadId={id}\n lead={lead}\n basePath={basePath}\n roster={roster}\n />\n <LeadUnqualifyDialog\n open={unqualifying}\n onClose={() => setUnqualifying(false)}\n hostId={siteHostId}\n leadId={id}\n leadLabel={label ?? id}\n />\n {erase.dialog}\n </CrmCreateSiteDefault>\n )\n}\nLeadDetailPage.displayName = 'LeadDetailPage'\n\nexport default LeadDetailPage\n"],"names":["pluginDocsHelp","leadPrimaryGroup","normalizeContactEmail","readErasureRequestedAtMs","useFirestore","useFirestoreDoc","Stack","Typography","doc","useMemo","useState","useCampaignFilingLog","useCrmCampaigns","CrmCreateSiteDefault","useCrmScope","useOrgMemberOptions","crmRoutes","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","useErasePersonAction","LeadCampaignsCard","leadCampaignNames","LeadConvertDialog","LeadHistoryCard","LeadPropertiesCard","LeadUnqualifyDialog","RecordActivityCard","LeadDetailPage","props","id","hostId","org","basePath","firestore","routes","scope","orgId","ready","consentGroup","viewingGroup","data","lead","status","fromCache","leadGroup","siteHostId","roster","campaigns","enabled","logFiling","converting","setConverting","unqualifying","setUnqualifying","leadEmail","erase","subject","kind","email","requestedAtMs","label","String","undefined","noOrg","title","help","anchor","backHref","section","backLabel","variant","color","loading","spacing","leadId","leadStatus","onConvert","onUnqualify","extraMenuItems","menuItems","banner","erasurePending","pendingSinceMs","extraChips","options","map","name","value","recordId","optionsReady","onFiled","added","removed","named","ids","campaignId","find","option","filed","open","onClose","leadLabel","dialog","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SACEA,cAAc,EAEdC,gBAAgB,EAChBC,qBAAqB,EACrBC,wBAAwB,QACnB,eAAc;AACrB,SAASC,YAAY,EAAEC,eAAe,QAAQ,iCAAgC;AAC9E,SAASC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACjD,SAASC,GAAG,QAAQ,qBAAoB;AACxC,SAASC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACzC,SAASC,oBAAoB,QAAQ,sCAAkC;AACvE,SAASC,eAAe,QAAQ,gCAA4B;AAC5D,SAASC,oBAAoB,QAAQ,gCAA4B;AACjE,SAASC,WAAW,QAAQ,4BAAwB;AACpD,SAASC,mBAAmB,QAAQ,qCAAiC;AACrE,SAAkCC,SAAS,QAAQ,yBAAqB;AACxE,SAASC,aAAa,EAAEC,eAAe,QAAQ,yBAAqB;AACpE,SAASC,qBAAqB,QAAQ,gCAA4B;AAClE,SAASC,oBAAoB,QAAQ,2BAAuB;AAC5D,SAASC,iBAAiB,EAAEC,iBAAiB,QAAQ,2BAAuB;AAC5E,SAASC,iBAAiB,QAAQ,2BAAuB;AACzD,SAASC,eAAe,QAAQ,yBAAqB;AACrD,SAASC,kBAAkB,QAAQ,4BAAwB;AAC3D,SAASC,mBAAmB,QAAQ,6BAAyB;AAC7D,SAASC,kBAAkB,QAAQ,4BAAwB;AAI3D;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,SAASC,eAAeC,KAAyB;IACtD,MAAM,EAAEC,EAAE,EAAEC,MAAM,EAAEC,GAAG,EAAEC,QAAQ,EAAE,GAAGJ;IACtC,MAAMK,YAAY9B;IAClB,MAAM+B,SAASnB,UAAUiB;IACzB;;;;;;GAMC,GACD,MAAM,EAAEG,KAAK,EAAEC,KAAK,EAAEC,KAAK,EAAEC,cAAcC,YAAY,EAAE,GAAG1B,YAAY;QACtEiB;QACAC;IACF;IACA,MAAM,EACJS,MAAMC,IAAI,EACVC,MAAM,EACNC,SAAS,EACV,GAAGvC,gBACF,IAAO+B,QAAQ5B,IAAI0B,WAAWE,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAE,SAASN,MAAM,MACjE;QAACI;QAAWE;QAAON;KAAG;IAExB;;;;;;;GAOC,GACD,MAAMe,YAAYpC,QAChB,IAAM+B,uBAAAA,eAAgBvC,iBAAiByC,MAAMV,MAC7C;QAACQ;QAAcE;QAAMV;KAAI;IAE3B,yEAAyE,GACzE,MAAMc,aAAaf,iBAAAA,SAAWc,UAAUd,MAAM,IAAI;IAClD,MAAMgB,SAAShC,oBAAoBsB;IACnC;;;;;;GAMC,GACD,MAAMW,YAAYpC,gBAAgB;QAAEmB;QAAQM;IAAM,GAAG;QAAEY,SAAS;IAAK;IACrE,uEAAuE;IACvE,mEAAmE;IACnE,MAAMC,YAAYvC,qBAAqB;QACrC0B;QACAN,QAAQe;QACRd,KAAKA;IACP;IACA,MAAM,CAACmB,YAAYC,cAAc,GAAG1C,SAAS;IAC7C,MAAM,CAAC2C,cAAcC,gBAAgB,GAAG5C,SAAS;IACjD,oEAAoE;IACpE,0DAA0D;IAC1D,MAAM6C,YAAYb,OAAOxC,sBAAsBwC,IAAI,CAAC,QAAQ,IAAI;IAChE,MAAMc,QAAQpC,qBAAqB;QACjCW,QAAQe;QACRT;QACAoB,SAASf,QAAQa,YAAY;YAAEG,MAAM;YAAQ5B;YAAI6B,OAAOJ;QAAU,IAAI;QACtEK,eAAezD,yBAAyBuC;IAC1C;IAEA,MAAMmB,QAAQnB,OAAOoB,OAAOpB,IAAI,CAAC,OAAO,IAAIA,IAAI,CAAC,QAAQ,IAAIZ,MAAMiC;IAEnE;;;;;GAKC,GACD,MAAMC,QAAQ1B,SAAS,CAACF;IACxB,IAAI4B,SAASrB,WAAW,WAAYA,WAAW,aAAa,CAACD,MAAO;QAClE,qBACE,KAACxB;YACCwC,MAAK;YACLO,OAAOF;YACPG,MAAMlE,eAAe,YAAY;gBAAEmE,QAAQ;YAAgB;YAC3DC,UAAUjC,OAAOkC,OAAO,CAAC;YACzBC,WAAU;sBAEV,cAAA,KAAC/D;gBAAWgE,SAAQ;gBAAQC,OAAM;0BAC/BR,SAASrB,WAAW,UACjB,iCACA;;;IAIZ;IACA,IAAI,CAACD,MAAM;QACT,qBACE,KAACxB;YACCwC,MAAK;YACLO,OAAOF;YACPG,MAAMlE,eAAe,YAAY;gBAAEmE,QAAQ;YAAgB;YAC3DC,UAAUjC,OAAOkC,OAAO,CAAC;YACzBC,WAAU;YACVG,OAAO;;IAGb;IAEA,OACE;;mDAE+C,iBAC/C,MAAC5D;QAAqBkB,QAAQe;;0BAI5B,MAACxC;gBAAMoE,SAAS;;kCACd,KAACjD;wBACCM,QAAQe;wBACRP,cAAcM;wBACdR,OAAOA;wBACPsC,QAAQ7C;wBACRY,MAAMA;wBACNkC,YAAYjC;wBACZC,WAAWA;wBACXX,UAAUA;wBACVc,QAAQA;wBACR8B,WAAW,IAAMzB,cAAc;wBAC/B0B,aAAa,IAAMxB,gBAAgB;wBACnCyB,gBAAgBvB,MAAMwB,SAAS;wBAC/BC,QAAQzB,MAAMyB,MAAM;wBACpBC,gBAAgB1B,MAAM2B,cAAc,KAAK;wBACzCnD,KAAKA;wBACL,4DAA4D;wBAC5D,6DAA6D;wBAC7D,wDAAwD;wBACxDoD,YAAY9D,kBAAkBoB,MAAMM,UAAUqC,OAAO,EAAEC,GAAG,CAAC,CAACC,qBAC1D,KAACtE;gCAAyB4C,OAAM;gCAAW2B,OAAOD;+BAA9BA;;kCAIxB,KAACpE;wBACCY,QAAQe;wBACRd,KAAKA;wBACL0B,MAAK;wBACL+B,UAAU3D;wBACVyD,IAAI,EAAE1B,gBAAAA,QAAS;;kCAEjB,KAACxC;wBACCU,QAAQe;wBACR6B,QAAQ7C;wBACRY,MAAMA;wBACNkC,YAAYjC;wBACZC,WAAWA;wBACXyC,SAASrC,UAAUqC,OAAO;wBAC1BK,cAAc1C,UAAUV,KAAK;wBAC7BqD,SAAS,CAAC,EAAEC,KAAK,EAAEC,OAAO,EAAE;4BAC1B,MAAMC,QAAQ,CAACC,MACbA,IAAIT,GAAG,CAAC,CAACU;;wCAEDhD;2CAFiB;wCACvBlB,IAAIkE;wCACJT,IAAI,WAAEvC,0BAAAA,UAAUqC,OAAO,CAACY,IAAI,CAAC,CAACC,SAAWA,OAAOV,KAAK,KAAKQ,gCAApDhD,wBAAiEa,KAAK,mBAAI;oCAClF;;4BACF,KAAKX,UAAU;gCAAEyB,QAAQ7C;4BAAG,GAAG;gCAAEqE,OAAOL,MAAMF;gCAAQC,SAASC,MAAMD;4BAAS;wBAChF;;kCAEF,KAACrE;wBAAgBO,QAAQe;wBAAY6B,QAAQ7C;wBAAIY,MAAMA;;kCACvD,KAACf;wBAAmBI,QAAQe;wBAAYd,KAAKA;wBAAK2C,QAAQ7C;;;;0BAE5D,KAACP;gBACC6E,MAAMjD;gBACNkD,SAAS,IAAMjD,cAAc;gBAC7BrB,QAAQe;gBACRT,OAAOA;gBACPL,KAAKA;gBACL2C,QAAQ7C;gBACRY,MAAMA;gBACNT,UAAUA;gBACVc,QAAQA;;0BAEV,KAACrB;gBACC0E,MAAM/C;gBACNgD,SAAS,IAAM/C,gBAAgB;gBAC/BvB,QAAQe;gBACR6B,QAAQ7C;gBACRwE,SAAS,EAAEzC,gBAAAA,QAAS/B;;YAErB0B,MAAM+C,MAAM;;;AAGnB;AACA3E,eAAe4E,WAAW,GAAG;AAE7B,eAAe5E,eAAc"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-detail-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n pluginDocsHelp,\n type CrmLeadFields,\n normalizeContactEmail,\n readErasureRequestedAtMs,\n} from '@aglyn/aglyn'\nimport { useFirestore, useFirestoreDoc } from '@aglyn/tenant-feature-instance'\nimport { Stack, Typography } from '@mui/material'\nimport { doc } from 'firebase/firestore'\nimport { useMemo, useState } from 'react'\nimport { useCampaignFilingLog } from '../hooks/use-campaign-filing-log'\nimport { useCrmCampaigns } from '../hooks/use-crm-campaigns'\nimport { CrmCreateSiteDefault } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useOrgMemberOptions } from '../hooks/use-org-member-options'\nimport { type CrmDetailPageProps, crmRoutes } from '../model/crm-routes'\nimport { CrmRecordChip, CrmRecordHeader } from './crm-record-header'\nimport { CrmRecordInsightsZone } from './crm-record-insights-zone'\nimport { useErasePersonAction } from './erase-person-action'\nimport { LeadCampaignsCard, leadCampaignNames } from './lead-campaigns-card'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport { LeadHistoryCard } from './lead-history-card'\nimport { LeadPropertiesCard } from './lead-properties-card'\nimport { LeadUnqualifyDialog } from './lead-unqualify-dialog'\nimport { RecordActivityCard } from './record-activity-card'\nimport { CrmShareChipView, RecordSharingCard } from './record-sharing-card'\nimport { crmShareChipFor } from '../model/crm-sharing'\nimport { leadPrimaryGroup } from '../model/contact-holder'\n\ntype LeadDocument = Record<string, unknown> & CrmLeadFields\n\n/**\n * `/crm/leads/{leadId}` — one lead (AGL-2608).\n *\n * The record behind a row of the Leads section: what the person did on the\n * site, who is working them, and the conversion that turns them into a\n * contact, a company and a deal. One document listen and one roster request\n * for the owner picker; the convert dialog's reads open only when it does.\n *\n * ## The two levels, and the site a lead is acted on as (AGL-3278)\n *\n * The org root comes from `useCrmScope`, as it does on every other CRM\n * record page: under a site it is looked up from that site, and at the\n * organization level it is the mount's, known at once. It used to come\n * from `useOrgDataScope({ hostId })` — correct only under a site, because\n * an absent host settles at `orgId: null`, and a null org builds no\n * reference, opens no listener and leaves the page loading forever.\n *\n * Several surfaces still need ONE site: the feed an activity is filed in, the booking door, the consent basis, the\n * conversion. Under a site that is the mounted one. At the organization\n * level it is the lead's own first capturing site — `leadPrimaryGroup` —\n * the same answer `contactPrimaryGroup` gives the contact page, and the\n * same one the Leads list already converts a row as.\n */\nexport function LeadDetailPage(props: CrmDetailPageProps) {\n const { id, hostId, org, basePath } = props\n const firestore = useFirestore()\n const routes = crmRoutes(basePath)\n /*\n * Resolved BEFORE the read that needs it (AGL-3275). A lead is an org row\n * now, so the org has to be known to address one — and `scope` is null\n * while the host-index lookup settles. Handing that null to `doc()` is a\n * thrown `TypeError`, which the page renders as a 500 rather than as a\n * record still loading.\n */\n const { scope, orgId, ready, consentGroup: viewingGroup } = useCrmScope({\n hostId,\n org,\n })\n const {\n data: lead,\n status,\n fromCache,\n } = useFirestoreDoc<LeadDocument>(\n () => (scope ? doc(firestore, scope[0], scope[1], 'leads', id) : null),\n [firestore, scope, id],\n )\n /*\n * The group this lead is READ through: the mounted site's under a site,\n * and the lead's own first capturing site at the organization level\n * (AGL-3278). The consent basis is read against it — a refusal recorded\n * for any sibling in a declared group stands against every one of them,\n * so a group of one would report a person as opted in who had\n * unsubscribed from the brand beside this one.\n */\n const leadGroup = useMemo(\n () => viewingGroup ?? leadPrimaryGroup(lead, org as Record<string, unknown>),\n [viewingGroup, lead, org],\n )\n /** The one site this page acts as, or null for a lead no site captured. */\n const siteHostId = hostId ?? (leadGroup.hostId || null)\n const roster = useOrgMemberOptions(orgId)\n /*\n * The campaigns, read once for the page (AGL-3274): the header names the\n * ones the lead is filed under and the Campaigns card offers them. One\n * listener, not one per surface. Under a site, the campaigns placed on\n * it; at the organization level the lead is the org's record and so is\n * every campaign it may be filed under.\n */\n const campaigns = useCrmCampaigns({ hostId, orgId }, { enabled: true })\n // A saved filing is written on the lead's Activity too (AGL-3274), one\n // entry per campaign added or removed, by the member who saved it.\n const logFiling = useCampaignFilingLog({\n orgId,\n hostId: siteHostId,\n org: org as Record<string, unknown> | undefined,\n })\n const [converting, setConverting] = useState(false)\n const [unqualifying, setUnqualifying] = useState(false)\n // The privacy erasure (AGL-2623), offered from the lead as from the\n // contact: the same request, filed by the lead's address.\n const leadEmail = lead ? normalizeContactEmail(lead['email']) : null\n const erase = useErasePersonAction({\n hostId: siteHostId,\n orgId,\n subject: lead && leadEmail ? { kind: 'lead', id, email: leadEmail } : null,\n requestedAtMs: readErasureRequestedAtMs(lead),\n })\n\n const label = lead ? String(lead['name'] || lead['email'] || id) : undefined\n\n /*\n * A SETTLED lookup with no org is an answer, not a wait (AGL-3278). The\n * spinner below is for a read in flight; standing on it once `ready` has\n * said there is no org to read from is the endless \"Loading…\" this page\n * shipped with.\n */\n const noOrg = ready && !scope\n if (noOrg || status === 'error' || (status === 'success' && !lead)) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {noOrg || status === 'error'\n ? 'This lead could not be read.'\n : 'This lead no longer exists — it may have been removed from the Inbox.'}\n </Typography>\n </CrmRecordHeader>\n )\n }\n if (!lead) {\n return (\n <CrmRecordHeader\n kind=\"Lead\"\n title={undefined}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n backHref={routes.section('leads')}\n backLabel=\"Back to leads\"\n loading\n />\n )\n }\n\n return (\n /* A task or an activity created from this page defaults to the lead's\n own site, not to the site the reader last picked in a list's drawer\n (AGL-2630) — as on every other record page. */\n <CrmCreateSiteDefault hostId={siteHostId}>\n {/* The properties card is the record's lead card: it publishes the\n page heading and the trail, so the history card under it says what\n it holds rather than repeating the name. */}\n <Stack spacing={3}>\n <LeadPropertiesCard\n hostId={siteHostId}\n consentGroup={leadGroup}\n orgId={orgId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n basePath={basePath}\n roster={roster}\n onConvert={() => setConverting(true)}\n onUnqualify={() => setUnqualifying(true)}\n extraMenuItems={erase.menuItems}\n banner={erase.banner}\n erasurePending={erase.pendingSinceMs !== null}\n org={org}\n // The campaigns the lead is filed under (AGL-3274), by name\n // beside the status. An id no container answers for draws no\n // chip: the card below keeps it, the header only names.\n extraChips={[\n // Seen here only through a share (AGL-3336): who shared it.\n <CrmShareChipView\n key=\"shared\"\n chip={crmShareChipFor(lead, viewingGroup?.hostIds ?? [])}\n org={org}\n />,\n ...leadCampaignNames(lead, campaigns.options).map((name) => (\n <CrmRecordChip key={name} label=\"Campaign\" value={name} />\n )),\n ]}\n />\n {/* What an assistant says about where the lead stands (AGL-2917): read on its own site. */}\n <CrmRecordInsightsZone\n hostId={siteHostId}\n org={org as Record<string, unknown> | undefined}\n kind=\"lead\"\n recordId={id}\n name={label ?? ''}\n />\n <LeadCampaignsCard\n hostId={siteHostId}\n leadId={id}\n lead={lead}\n leadStatus={status}\n fromCache={fromCache}\n options={campaigns.options}\n optionsReady={campaigns.ready}\n onFiled={({ added, removed }) => {\n const named = (ids: string[]) =>\n ids.map((campaignId) => ({\n id: campaignId,\n name: campaigns.options.find((option) => option.value === campaignId)?.label ?? '',\n }))\n void logFiling({ leadId: id }, { filed: named(added), removed: named(removed) })\n }}\n />\n <LeadHistoryCard hostId={siteHostId} leadId={id} lead={lead} />\n {/* Where the lead is visible and why, and a manager's share (AGL-3336). */}\n <RecordSharingCard\n object=\"leads\"\n id={id}\n record={lead}\n hostId={hostId ?? null}\n orgId={orgId}\n org={org}\n viewingHostIds={viewingGroup?.hostIds}\n />\n <RecordActivityCard hostId={siteHostId} org={org} leadId={id} />\n </Stack>\n <LeadConvertDialog\n open={converting}\n onClose={() => setConverting(false)}\n hostId={siteHostId}\n orgId={orgId}\n org={org as Record<string, unknown> | undefined}\n leadId={id}\n lead={lead}\n basePath={basePath}\n roster={roster}\n />\n <LeadUnqualifyDialog\n open={unqualifying}\n onClose={() => setUnqualifying(false)}\n hostId={siteHostId}\n leadId={id}\n leadLabel={label ?? id}\n />\n {erase.dialog}\n </CrmCreateSiteDefault>\n )\n}\nLeadDetailPage.displayName = 'LeadDetailPage'\n\nexport default LeadDetailPage\n"],"names":["pluginDocsHelp","normalizeContactEmail","readErasureRequestedAtMs","useFirestore","useFirestoreDoc","Stack","Typography","doc","useMemo","useState","useCampaignFilingLog","useCrmCampaigns","CrmCreateSiteDefault","useCrmScope","useOrgMemberOptions","crmRoutes","CrmRecordChip","CrmRecordHeader","CrmRecordInsightsZone","useErasePersonAction","LeadCampaignsCard","leadCampaignNames","LeadConvertDialog","LeadHistoryCard","LeadPropertiesCard","LeadUnqualifyDialog","RecordActivityCard","CrmShareChipView","RecordSharingCard","crmShareChipFor","leadPrimaryGroup","LeadDetailPage","props","id","hostId","org","basePath","firestore","routes","scope","orgId","ready","consentGroup","viewingGroup","data","lead","status","fromCache","leadGroup","siteHostId","roster","campaigns","enabled","logFiling","converting","setConverting","unqualifying","setUnqualifying","leadEmail","erase","subject","kind","email","requestedAtMs","label","String","undefined","noOrg","title","help","anchor","backHref","section","backLabel","variant","color","loading","spacing","leadId","leadStatus","onConvert","onUnqualify","extraMenuItems","menuItems","banner","erasurePending","pendingSinceMs","extraChips","chip","hostIds","options","map","name","value","recordId","optionsReady","onFiled","added","removed","named","ids","campaignId","find","option","filed","object","record","viewingHostIds","open","onClose","leadLabel","dialog","displayName"],"mappings":"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"}
|
|
@@ -39,6 +39,7 @@ import { LeadOwnerSelect } from "./lead-owner-select.js";
|
|
|
39
39
|
import { LeadSourceSelect } from "./lead-source-select.js";
|
|
40
40
|
import { LeadStatusChip } from "./lead-status-chip.js";
|
|
41
41
|
import { crmClientListFields, CRM_CLIENT_SEARCH_FIELDS } from "../model/crm-list-query.js";
|
|
42
|
+
import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
|
|
42
43
|
/** The Leads list's fields a profile save rewrites; the verdict key is the server's. */ const LEAD_CLIENT_LIST_FIELDS = [
|
|
43
44
|
...CRM_CLIENT_SEARCH_FIELDS,
|
|
44
45
|
'leadSourceKey'
|
|
@@ -113,6 +114,8 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
113
114
|
const firestore = useFirestore();
|
|
114
115
|
const { enqueueSnackbar } = useSnackbar();
|
|
115
116
|
const routes = crmRoutes(basePath);
|
|
117
|
+
// A client-direct write owes the org's sharing rules a re-evaluation (AGL-3336).
|
|
118
|
+
const followUpSharing = useCrmSharingFollowUp(hostId, orgId);
|
|
116
119
|
/*
|
|
117
120
|
* Built at USE, not at render (AGL-3275). A lead is an org row, so the org
|
|
118
121
|
* has to be known to address one — and `orgId` is null while the lookup
|
|
@@ -202,6 +205,9 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
202
205
|
await updateDoc(ref, _extends({}, fields, {
|
|
203
206
|
updatedAt: serverTimestamp()
|
|
204
207
|
}));
|
|
208
|
+
followUpSharing('leads', [
|
|
209
|
+
leadId
|
|
210
|
+
]);
|
|
205
211
|
enqueueSnackbar(done, {
|
|
206
212
|
variant: 'success',
|
|
207
213
|
persist: false
|
|
@@ -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 CrmLeadStatus,\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 MenuItem,\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 {\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 addressDraftFrom,\n ContactAddressFields,\n type AddressDraft,\n} from './contact-address-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 { LeadSourceSelect } from './lead-source-select'\nimport { LeadStatusChip } from './lead-status-chip'\nimport { crmClientListFields, CRM_CLIENT_SEARCH_FIELDS } from '../model/crm-list-query'\n\n/** The Leads list's fields a profile save rewrites; the verdict key is the server's. */\nconst LEAD_CLIENT_LIST_FIELDS = [...CRM_CLIENT_SEARCH_FIELDS, 'leadSourceKey'] as const\n\nconst NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX\nconst TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX\n\n/** The profile as the form holds it: every field a string, the address a draft. */\ninterface ProfileDraft {\n company: string\n jobTitle: string\n phone: string\n website: string\n leadSource: string\n tags: string\n address: AddressDraft\n}\n\n/** The stored profile as a draft the fields can edit. */\nfunction profileDraftFrom(lead: Record<string, unknown> & CrmLeadFields): ProfileDraft {\n return {\n company: String(lead.company ?? ''),\n jobTitle: String(lead.jobTitle ?? ''),\n phone: String(lead.phone ?? lead['phone'] ?? ''),\n website: String(lead.website ?? ''),\n leadSource: String(lead.leadSource ?? ''),\n tags: (lead.tags ?? []).join(', '),\n address: addressDraftFrom(lead.address ?? null),\n }\n}\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 onUnqualify: () => 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 /*\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\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) — company, title, phone, website,\n * address, tags, lead source — edited in the Details card with the notes\n * and the custom fields, under one Save, the way the contact's Properties\n * card saves. Seeded from the document and guarded on save: a draft\n * edited over a cached read must not overwrite a newer profile with an\n * older one.\n */\n const [profile, setProfile] = useState<ProfileDraft>(() => profileDraftFrom(lead))\n const [profileDirty, setProfileDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n const [profileErrors, setProfileErrors] = useState<Record<string, string>>({})\n useEffect(() => {\n if (!profileDirty) setProfile(profileDraftFrom(lead))\n // The draft follows the document until it is edited; the fields are\n // read one by one so a change to any of them reseeds.\n }, [\n profileDirty,\n lead.company,\n lead.jobTitle,\n lead.phone,\n lead.website,\n lead.leadSource,\n lead.tags,\n lead.address,\n ])\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 const storedCustom = useMemo(() => lead.custom ?? {}, [lead.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n const editProfile = <K extends keyof ProfileDraft>(key: K, value: ProfileDraft[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 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(profileDraftFrom(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 { patch, errors } = Aglyn.normalizeCrmLeadProfile({\n company: profile.company,\n jobTitle: profile.jobTitle,\n phone: profile.phone,\n website: profile.website,\n leadSource: profile.leadSource,\n tags: profile.tags,\n address: profile.address,\n })\n setProfileErrors(errors)\n if (Object.keys(errors).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 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('leads', lead, profileFields, LEAD_CLIENT_LIST_FIELDS),\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}\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} />\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 and\n booking doors write no phone, so a row for every lead would be a\n permanent blank. A form that captures one fills this.\n */}\n {leadPhone ? (\n <Fact label=\"Phone\">\n <CrmPhoneLink phone={leadPhone} />\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} />\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={status === 'unqualified' ? 'unqualified' : status}\n onChange={(event) => {\n const next = String(event.target.value) as CrmLeadStatus\n if (next === 'unqualified') {\n onUnqualify()\n return\n }\n // Reopening drops the reason with the closed state: a lead\n // being worked again is not \"unqualified because …\".\n void write(\n {\n status: next,\n ...(status === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n },\n 'Status updated',\n )\n }}\n >\n <MenuItem value=\"new\">{Aglyn.CRM_LEAD_STATUS_LABELS.new}</MenuItem>\n <MenuItem value=\"working\">{Aglyn.CRM_LEAD_STATUS_LABELS.working}</MenuItem>\n <MenuItem value=\"unqualified\">\n {`${Aglyn.CRM_LEAD_STATUS_LABELS.unqualified}…`}\n </MenuItem>\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 <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"Company\"\n value={profile.company}\n onChange={(event) => editProfile('company', event.target.value)}\n disabled={converted}\n slotProps={{ htmlInput: { maxLength: TEXT_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={profile.jobTitle}\n onChange={(event) => editProfile('jobTitle', event.target.value)}\n disabled={converted}\n slotProps={{ htmlInput: { maxLength: TEXT_MAX } }}\n fullWidth\n />\n </Stack>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={profile.phone}\n onChange={(event) => editProfile('phone', event.target.value)}\n disabled={converted}\n error={Boolean(profileErrors['phone'])}\n helperText={profileErrors['phone'] || 'With the country code, like +1 512 555 0107'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Website\"\n value={profile.website}\n onChange={(event) => editProfile('website', event.target.value)}\n disabled={converted}\n error={Boolean(profileErrors['website'])}\n helperText={profileErrors['website'] || 'Like acme.com'}\n fullWidth\n />\n </Stack>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n {/* The org's own values (AGL-3298); the stored one stays shown when the list no longer offers it. */}\n <LeadSourceSelect\n picklist={leadSources.picklist}\n value={profile.leadSource}\n stored={String(lead.leadSource ?? '')}\n onChange={(label) => editProfile('leadSource', label)}\n disabled={converted}\n />\n <TextField\n size=\"small\"\n label=\"Tags\"\n value={profile.tags}\n onChange={(event) => editProfile('tags', event.target.value)}\n disabled={converted}\n helperText=\"Comma-separated\"\n fullWidth\n />\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Address'}\n </Typography>\n <ContactAddressFields\n value={profile.address}\n onChange={(next) => editProfile('address', next)}\n disabled={converted}\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","MenuItem","Select","Stack","TextField","Tooltip","Typography","deleteField","doc","serverTimestamp","updateDoc","useEffect","useId","useMemo","useState","useContactFieldDefinitions","useLeadSourcePicklist","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","crmRoutes","addressDraftFrom","ContactAddressFields","CrmCallButton","CrmPhoneLink","CrmEmailStateChip","CrmEmailGatewayChip","CrmRecordChip","CrmRecordHeader","CrmSendEmailButton","LeadOwnerSelect","LeadSourceSelect","LeadStatusChip","crmClientListFields","CRM_CLIENT_SEARCH_FIELDS","LEAD_CLIENT_LIST_FIELDS","NOTES_MAX","CRM_LEAD_NOTES_MAX","TEXT_MAX","CRM_LEAD_TEXT_MAX","profileDraftFrom","lead","company","String","jobTitle","phone","website","leadSource","tags","join","address","profileWrite","patch","write","key","value","Object","entries","undefined","CONVERT_PENDING_ERASURE_REASON","Fact","props","spacing","variant","color","label","component","children","LeadPropertiesCard","hostId","consentGroup","orgId","leadId","leadStatus","fromCache","basePath","roster","onConvert","onUnqualify","extraMenuItems","banner","extraChips","erasurePending","org","firestore","enqueueSnackbar","routes","refFor","status","crmLeadStatus","converted","Boolean","convertedContactId","open","isCrmLeadOpen","emailState","readEmailState","leadPhone","trim","notes","setNotes","statusLabelId","notesDirty","setNotesDirty","profile","setProfile","profileDirty","setProfileDirty","saving","setSaving","profileErrors","setProfileErrors","fields","leadSources","storedCustom","custom","setCustom","editProfile","current","done","ref","persist","updatedAt","error","Error","message","customChanged","length","dirty","discard","saveDetails","update","errors","normalizeCrmLeadProfile","keys","missing","active","profileFields","assign","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","dealId","deal","span","disabled","onClick","link","email","name","menuItems","icon","path","destructive","chips","state","ownerUid","labelFor","direction","xs","md","sx","alignItems","convertedAtMs","toLocaleString","minWidth","id","labelId","onChange","event","next","target","unqualifiedReason","CRM_LEAD_STATUS_LABELS","new","working","unqualified","fullWidth","uid","severity","header","contentGutterX","contentGutterY","HeaderProps","action","slotProps","htmlInput","maxLength","helperText","picklist","stored","map","definition","$id","multiline","minRows","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 CrmLeadStatus,\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 MenuItem,\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 {\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 addressDraftFrom,\n ContactAddressFields,\n type AddressDraft,\n} from './contact-address-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 { LeadSourceSelect } from './lead-source-select'\nimport { LeadStatusChip } from './lead-status-chip'\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 = [...CRM_CLIENT_SEARCH_FIELDS, 'leadSourceKey'] as const\n\nconst NOTES_MAX = Aglyn.CRM_LEAD_NOTES_MAX\nconst TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX\n\n/** The profile as the form holds it: every field a string, the address a draft. */\ninterface ProfileDraft {\n company: string\n jobTitle: string\n phone: string\n website: string\n leadSource: string\n tags: string\n address: AddressDraft\n}\n\n/** The stored profile as a draft the fields can edit. */\nfunction profileDraftFrom(lead: Record<string, unknown> & CrmLeadFields): ProfileDraft {\n return {\n company: String(lead.company ?? ''),\n jobTitle: String(lead.jobTitle ?? ''),\n phone: String(lead.phone ?? lead['phone'] ?? ''),\n website: String(lead.website ?? ''),\n leadSource: String(lead.leadSource ?? ''),\n tags: (lead.tags ?? []).join(', '),\n address: addressDraftFrom(lead.address ?? null),\n }\n}\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 onUnqualify: () => 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\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) — company, title, phone, website,\n * address, tags, lead source — edited in the Details card with the notes\n * and the custom fields, under one Save, the way the contact's Properties\n * card saves. Seeded from the document and guarded on save: a draft\n * edited over a cached read must not overwrite a newer profile with an\n * older one.\n */\n const [profile, setProfile] = useState<ProfileDraft>(() => profileDraftFrom(lead))\n const [profileDirty, setProfileDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n const [profileErrors, setProfileErrors] = useState<Record<string, string>>({})\n useEffect(() => {\n if (!profileDirty) setProfile(profileDraftFrom(lead))\n // The draft follows the document until it is edited; the fields are\n // read one by one so a change to any of them reseeds.\n }, [\n profileDirty,\n lead.company,\n lead.jobTitle,\n lead.phone,\n lead.website,\n lead.leadSource,\n lead.tags,\n lead.address,\n ])\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 const storedCustom = useMemo(() => lead.custom ?? {}, [lead.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n const editProfile = <K extends keyof ProfileDraft>(key: K, value: ProfileDraft[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(profileDraftFrom(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 { patch, errors } = Aglyn.normalizeCrmLeadProfile({\n company: profile.company,\n jobTitle: profile.jobTitle,\n phone: profile.phone,\n website: profile.website,\n leadSource: profile.leadSource,\n tags: profile.tags,\n address: profile.address,\n })\n setProfileErrors(errors)\n if (Object.keys(errors).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 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('leads', lead, profileFields, LEAD_CLIENT_LIST_FIELDS),\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}\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} />\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 and\n booking doors write no phone, so a row for every lead would be a\n permanent blank. A form that captures one fills this.\n */}\n {leadPhone ? (\n <Fact label=\"Phone\">\n <CrmPhoneLink phone={leadPhone} />\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} />\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={status === 'unqualified' ? 'unqualified' : status}\n onChange={(event) => {\n const next = String(event.target.value) as CrmLeadStatus\n if (next === 'unqualified') {\n onUnqualify()\n return\n }\n // Reopening drops the reason with the closed state: a lead\n // being worked again is not \"unqualified because …\".\n void write(\n {\n status: next,\n ...(status === 'unqualified' ? { unqualifiedReason: deleteField() } : {}),\n },\n 'Status updated',\n )\n }}\n >\n <MenuItem value=\"new\">{Aglyn.CRM_LEAD_STATUS_LABELS.new}</MenuItem>\n <MenuItem value=\"working\">{Aglyn.CRM_LEAD_STATUS_LABELS.working}</MenuItem>\n <MenuItem value=\"unqualified\">\n {`${Aglyn.CRM_LEAD_STATUS_LABELS.unqualified}…`}\n </MenuItem>\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 <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"Company\"\n value={profile.company}\n onChange={(event) => editProfile('company', event.target.value)}\n disabled={converted}\n slotProps={{ htmlInput: { maxLength: TEXT_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={profile.jobTitle}\n onChange={(event) => editProfile('jobTitle', event.target.value)}\n disabled={converted}\n slotProps={{ htmlInput: { maxLength: TEXT_MAX } }}\n fullWidth\n />\n </Stack>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={profile.phone}\n onChange={(event) => editProfile('phone', event.target.value)}\n disabled={converted}\n error={Boolean(profileErrors['phone'])}\n helperText={profileErrors['phone'] || 'With the country code, like +1 512 555 0107'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Website\"\n value={profile.website}\n onChange={(event) => editProfile('website', event.target.value)}\n disabled={converted}\n error={Boolean(profileErrors['website'])}\n helperText={profileErrors['website'] || 'Like acme.com'}\n fullWidth\n />\n </Stack>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>\n {/* The org's own values (AGL-3298); the stored one stays shown when the list no longer offers it. */}\n <LeadSourceSelect\n picklist={leadSources.picklist}\n value={profile.leadSource}\n stored={String(lead.leadSource ?? '')}\n onChange={(label) => editProfile('leadSource', label)}\n disabled={converted}\n />\n <TextField\n size=\"small\"\n label=\"Tags\"\n value={profile.tags}\n onChange={(event) => editProfile('tags', event.target.value)}\n disabled={converted}\n helperText=\"Comma-separated\"\n fullWidth\n />\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Address'}\n </Typography>\n <ContactAddressFields\n value={profile.address}\n onChange={(next) => editProfile('address', next)}\n disabled={converted}\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","MenuItem","Select","Stack","TextField","Tooltip","Typography","deleteField","doc","serverTimestamp","updateDoc","useEffect","useId","useMemo","useState","useContactFieldDefinitions","useLeadSourcePicklist","crmCustomDraftChanges","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","crmRoutes","addressDraftFrom","ContactAddressFields","CrmCallButton","CrmPhoneLink","CrmEmailStateChip","CrmEmailGatewayChip","CrmRecordChip","CrmRecordHeader","CrmSendEmailButton","LeadOwnerSelect","LeadSourceSelect","LeadStatusChip","crmClientListFields","CRM_CLIENT_SEARCH_FIELDS","useCrmSharingFollowUp","LEAD_CLIENT_LIST_FIELDS","NOTES_MAX","CRM_LEAD_NOTES_MAX","TEXT_MAX","CRM_LEAD_TEXT_MAX","profileDraftFrom","lead","company","String","jobTitle","phone","website","leadSource","tags","join","address","profileWrite","patch","write","key","value","Object","entries","undefined","CONVERT_PENDING_ERASURE_REASON","Fact","props","spacing","variant","color","label","component","children","LeadPropertiesCard","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","trim","notes","setNotes","statusLabelId","notesDirty","setNotesDirty","profile","setProfile","profileDirty","setProfileDirty","saving","setSaving","profileErrors","setProfileErrors","fields","leadSources","storedCustom","custom","setCustom","editProfile","current","done","ref","persist","updatedAt","error","Error","message","customChanged","length","dirty","discard","saveDetails","update","errors","normalizeCrmLeadProfile","keys","missing","active","profileFields","assign","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","dealId","deal","span","disabled","onClick","link","email","name","menuItems","icon","path","destructive","chips","state","ownerUid","labelFor","direction","xs","md","sx","alignItems","convertedAtMs","toLocaleString","minWidth","id","labelId","onChange","event","next","target","unqualifiedReason","CRM_LEAD_STATUS_LABELS","new","working","unqualified","fullWidth","uid","severity","header","contentGutterX","contentGutterY","HeaderProps","action","slotProps","htmlInput","maxLength","helperText","picklist","stored","map","definition","$id","multiline","minRows","displayName"],"mappings":"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"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
-
import {
|
|
18
|
+
import { pluginRecordHref } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
|
|
19
19
|
import { AppLink } from "@aglyn/shared-ui-jsx";
|
|
20
20
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
21
21
|
import { useConsoleHostRoute, useFirestore, useFirestoreCollection, usePluginApiPost } from "@aglyn/tenant-feature-instance";
|
|
@@ -227,11 +227,10 @@ UnroutedLeadSurfaces.displayName = 'UnroutedLeadSurfaces';
|
|
|
227
227
|
const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId);
|
|
228
228
|
const { routed, unrouted, truncated, status } = useLeadSurfaceForms(hostId);
|
|
229
229
|
const { turningOn, turnOn } = useTurnOnLeadRouting();
|
|
230
|
-
const formHref = useCallback((formId)=>orgSlug && host ?
|
|
230
|
+
const formHref = useCallback((formId)=>orgSlug && host ? pluginRecordHref('form', {
|
|
231
231
|
orgSlug,
|
|
232
|
-
host
|
|
233
|
-
|
|
234
|
-
}) : null, [
|
|
232
|
+
host
|
|
233
|
+
}, formId) : null, [
|
|
235
234
|
orgSlug,
|
|
236
235
|
host
|
|
237
236
|
]);
|