@aglyn/plugins-crm 1.0.0-beta.169 → 1.0.0-beta.171

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-crm",
3
- "version": "1.0.0-beta.169",
3
+ "version": "1.0.0-beta.171",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,18 +25,18 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.169",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.169",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.169",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.169",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.169",
33
- "@aglyn/shared-ui-next": "1.0.0-beta.169",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.169",
35
- "@aglyn/shared-util-email": "1.0.0-beta.169",
36
- "@aglyn/shared-util-http": "1.0.0-beta.169",
37
- "@aglyn/tenant-data-admin": "1.0.0-beta.169",
38
- "@aglyn/tenant-feature-instance": "1.0.0-beta.169",
39
- "@aglyn/tenant-runtime": "1.0.0-beta.169",
28
+ "@aglyn/aglyn": "1.0.0-beta.171",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.171",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.171",
31
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.171",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.171",
33
+ "@aglyn/shared-ui-next": "1.0.0-beta.171",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.171",
35
+ "@aglyn/shared-util-email": "1.0.0-beta.171",
36
+ "@aglyn/shared-util-http": "1.0.0-beta.171",
37
+ "@aglyn/tenant-data-admin": "1.0.0-beta.171",
38
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.171",
39
+ "@aglyn/tenant-runtime": "1.0.0-beta.171",
40
40
  "@dnd-kit/core": "^6.3.1",
41
41
  "@dnd-kit/utilities": "^3.2.2",
42
42
  "@swc/helpers": "0.5.23",
@@ -13,7 +13,13 @@ export declare const LEAD_CAMPAIGNS_HELPER_TEXT = "Your own filing. It never add
13
13
  */
14
14
  export declare function leadCampaignNames(lead: Record<string, unknown> | null | undefined, options: readonly CampaignPickerOption[]): string[];
15
15
  export interface LeadCampaignsCardProps {
16
- hostId: string;
16
+ /**
17
+ * The lead's own site, or `null` at the organization level for a lead no
18
+ * site captured (AGL-3278). Read for the org root alone — the containers
19
+ * themselves are handed down as `options` — so the card serves both
20
+ * levels; the org-level mount answers the same org without a site.
21
+ */
22
+ hostId: string | null;
17
23
  leadId: string;
18
24
  lead: Record<string, unknown>;
19
25
  leadStatus: FirestoreDocStatus;
@@ -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'\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 hostId: string\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.\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 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 >\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=\"This site has no campaigns yet. Create one from Marketing to file leads under it.\"\n />\n {empty ? null : (\n <Stack direction=\"row\">\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 </Stack>\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","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","spacing","onChange","helperText","disabled","emptyText","direction","size","onClick","catch","error","console","Error","allowDuplicate","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'\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.\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 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 >\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=\"This site has no campaigns yet. Create one from Marketing to file leads under it.\"\n />\n {empty ? null : (\n <Stack direction=\"row\">\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 </Stack>\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","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","spacing","onChange","helperText","disabled","emptyText","direction","size","onClick","catch","error","console","Error","allowDuplicate","displayName"],"mappings":"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"}
@@ -3,7 +3,14 @@ import type { OrgMemberOptions } from '../hooks/use-org-member-options';
3
3
  export interface LeadConvertDialogProps {
4
4
  open: boolean;
5
5
  onClose: () => void;
6
- hostId: string;
6
+ /**
7
+ * The site the conversion is FILED AS (AGL-3278): the mounted one, or
8
+ * the lead's own first capturing site at the organization level. A
9
+ * conversion has to name one — the contact it makes is captured by a
10
+ * site, and its consent is that site's group's — so with none the
11
+ * dialog refuses rather than posting an empty host.
12
+ */
13
+ hostId: string | null;
7
14
  orgId: string | null;
8
15
  org: Record<string, unknown> | null | undefined;
9
16
  leadId: string;
@@ -169,13 +169,15 @@ import { LeadOwnerSelect } from "./lead-owner-select.js";
169
169
  ]);
170
170
  const amountCents = dollarsToCents(dealAmount);
171
171
  const amountInvalid = amountCents === undefined;
172
- const submittable = !busy && (companyMode !== 'new' || companyName.trim().length > 0) && (companyMode !== 'existing' || Boolean(companyId)) && (!dealOn || dealTitle.trim().length > 0 && !amountInvalid);
172
+ const submittable = !busy && // A conversion names the site it is filed as; there is none for a lead
173
+ // no site captured (AGL-3278).
174
+ Boolean(hostId) && (companyMode !== 'new' || companyName.trim().length > 0) && (companyMode !== 'existing' || Boolean(companyId)) && (!dealOn || dealTitle.trim().length > 0 && !amountInvalid);
173
175
  const submit = async ()=>{
174
176
  if (!submittable) return;
175
177
  setBusy(true);
176
178
  setError(null);
177
179
  const body = _extends({
178
- hostId,
180
+ hostId: hostId != null ? hostId : '',
179
181
  leadId
180
182
  }, ownerUid ? {
181
183
  ownerUid
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-convert-dialog.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 { CrmDealStage, CrmLeadFields } from '@aglyn/aglyn'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Checkbox,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Divider,\n FormControl,\n FormControlLabel,\n InputLabel,\n MenuItem,\n Radio,\n RadioGroup,\n Select,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n documentId,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useId, useMemo, useRef, useState } from 'react'\nimport {\n dollarsToCents,\n suggestCompanyForLead,\n} from '../model/lead-company-suggestion'\nimport { crmRoutes } from '../model/crm-routes'\nimport type { LeadConvertRequest, LeadConvertResponse } from '../server/lead-convert'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { LeadOwnerSelect } from './lead-owner-select'\n\n/**\n * How many companies the picker loads. Ordered by name, so a bigger org sees\n * its first two hundred alphabetically and the domain match still finds the\n * right one when it is among them; past that the converter creates and the\n * route dedupes by domain server-side, which is the backstop this ceiling\n * relies on.\n */\nconst COMPANY_OPTIONS_CEILING = 200\n\ntype CompanyMode = 'none' | 'existing' | 'new'\n\nexport interface LeadConvertDialogProps {\n open: boolean\n onClose: () => void\n hostId: string\n orgId: string | null\n org: Record<string, unknown> | null | undefined\n leadId: string\n lead: Record<string, unknown> & CrmLeadFields\n basePath: string\n roster: OrgMemberOptions\n}\n\n/**\n * Convert a lead into a contact, and optionally a company and a deal\n * (AGL-2608).\n *\n * The dialog collects the decisions and posts ONE request to\n * `crm/lead-convert`; it writes nothing itself, because the contact can only\n * come from the server's dedupe door and the rest of the conversion has to\n * follow it in order. What it does own is the suggestion: the company the\n * lead's email domain implies, matched against the companies this reader can\n * see, so the common case is a glance and a click.\n *\n * Both reads open only while the dialog is — a converter who never opens it\n * pays for neither the company list nor the pipeline.\n */\nexport function LeadConvertDialog(props: LeadConvertDialogProps) {\n const { open, onClose, hostId, orgId, org, leadId, lead, basePath, roster } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n\n /*\n * The scope this reader may see — the same tokens every CRM listener\n * filters on, and the rules refuse a list without them. The Leads list\n * mounts the dialog closed at the organization level with no site at all\n * (the row it converts brings one), and a consent group must name a site,\n * so until then the scope is the organization's token alone — nothing reads\n * it while the dialog is closed.\n */\n const visibleToTokens = useMemo(() => {\n const group = hostId ? Aglyn.consentGroupForHost(org ?? {}, hostId) : null\n return [\n Aglyn.ORG_SCOPE_TOKEN,\n ...(group?.hostIds ?? []).map((id) => Aglyn.hostScopeToken(id)),\n ].slice(0, Aglyn.MAX_SCOPE_HOSTS)\n }, [org, hostId])\n const tokensKey = visibleToTokens.join(',')\n\n const { data: companies, status: companiesStatus } = useFirestoreCollection<\n Record<string, unknown> & { $id: string; name?: string; domain?: string }\n >(\n () =>\n open && orgId\n ? query(\n collection(firestore, 'orgs', orgId, Aglyn.CRM_COLLECTIONS.companies),\n where('visibleTo', 'array-contains-any', visibleToTokens),\n orderBy('nameLower'),\n limit(COMPANY_OPTIONS_CEILING),\n )\n : null,\n [firestore, orgId, open, tokensKey],\n { idField: '$id' },\n )\n const { data: pipelines } = useFirestoreCollection<\n Record<string, unknown> & { $id: string; isDefault?: boolean; stages?: CrmDealStage[] }\n >(\n () =>\n open && orgId\n ? query(\n collection(firestore, 'orgs', orgId, Aglyn.CRM_COLLECTIONS.pipelines),\n where('visibleTo', 'array-contains-any', visibleToTokens),\n orderBy(documentId()),\n limit(20),\n )\n : null,\n [firestore, orgId, open, tokensKey],\n { idField: '$id' },\n )\n /*\n * The pipeline the deal will open in — the route's own choice, mirrored so\n * the stage picker offers the stages the deal will really have: the flagged\n * default, else the first by id, else the stages the route will seed.\n */\n const pipeline = pipelines.find((entry) => entry.isDefault) ?? pipelines[0]\n const stages = useMemo(\n () =>\n [...(pipeline?.stages ?? Aglyn.DEFAULT_DEAL_STAGES)]\n .filter((stage) => stage.kind === 'open')\n .sort((a, b) => a.order - b.order),\n [pipeline],\n )\n\n const email = String(lead['email'] ?? '')\n const leadLabel = String(lead['name'] || email || leadId)\n\n const [ownerUid, setOwnerUid] = useState('')\n const [companyMode, setCompanyMode] = useState<CompanyMode>('none')\n const [companyId, setCompanyId] = useState('')\n const [companyName, setCompanyName] = useState('')\n const [companyDomain, setCompanyDomain] = useState('')\n const [dealOn, setDealOn] = useState(false)\n const [dealTitle, setDealTitle] = useState('')\n const [dealAmount, setDealAmount] = useState('')\n const [dealCurrency, setDealCurrency] = useState('usd')\n const [dealStageId, setDealStageId] = useState('')\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n // The labels' ids, so the two comboboxes are named \"Company\" and \"Stage\"\n // rather than after whatever they show — see `LeadOwnerSelect`.\n const companyLabelId = useId()\n const stageLabelId = useId()\n\n /*\n * Reset on every open, and seed the company step ONCE per open when the\n * company list has answered — not on every snapshot, or a converter who\n * changed their mind would have the suggestion put back under them.\n */\n const seededFor = useRef<string | null>(null)\n useEffect(() => {\n if (!open) {\n seededFor.current = null\n return\n }\n setOwnerUid(String(lead.ownerUid ?? user?.uid ?? ''))\n setDealOn(false)\n setDealTitle(leadLabel)\n setDealAmount('')\n setDealCurrency('usd')\n setDealStageId('')\n setError(null)\n setCompanyMode('none')\n setCompanyId('')\n setCompanyName('')\n setCompanyDomain('')\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, leadId])\n useEffect(() => {\n if (!open || companiesStatus !== 'success') return\n if (seededFor.current === leadId) return\n seededFor.current = leadId\n // The lead's own company text first (AGL-3233), then the domain.\n const suggestion = suggestCompanyForLead(email, companies, lead.company)\n setCompanyMode(suggestion.mode)\n if (suggestion.mode === 'existing') setCompanyId(suggestion.companyId)\n if (suggestion.mode === 'new') {\n setCompanyName(suggestion.name)\n setCompanyDomain(suggestion.domain)\n }\n }, [open, companiesStatus, companies, email, leadId, lead.company])\n\n const amountCents = dollarsToCents(dealAmount)\n const amountInvalid = amountCents === undefined\n const submittable =\n !busy &&\n (companyMode !== 'new' || companyName.trim().length > 0) &&\n (companyMode !== 'existing' || Boolean(companyId)) &&\n (!dealOn || (dealTitle.trim().length > 0 && !amountInvalid))\n\n const submit = async () => {\n if (!submittable) return\n setBusy(true)\n setError(null)\n const body: LeadConvertRequest = {\n hostId,\n leadId,\n ...(ownerUid ? { ownerUid } : {}),\n ...(companyMode === 'existing' ? { companyId } : {}),\n ...(companyMode === 'new'\n ? {\n createCompany: {\n name: companyName.trim(),\n ...(companyDomain.trim() ? { domain: companyDomain.trim() } : {}),\n },\n }\n : {}),\n ...(dealOn\n ? {\n deal: {\n title: dealTitle.trim(),\n ...(typeof amountCents === 'number' ? { amountCents } : {}),\n currency: dealCurrency.trim().toLowerCase() || 'usd',\n ...(dealStageId ? { stageId: dealStageId } : {}),\n },\n }\n : {}),\n }\n try {\n const response = await authorizedFetch(user, '/api/crm/lead-convert', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n const answer = (await response.json().catch(() => ({}))) as\n | LeadConvertResponse\n | { error?: string }\n if (!response.ok || !('ok' in answer)) {\n setError(\n String((answer as { error?: string }).error ?? 'The lead could not be converted.'),\n )\n return\n }\n enqueueSnackbar(\n answer.alreadyConverted\n ? 'This lead was already converted — opening the contact'\n : 'Lead converted',\n { variant: 'success', persist: false },\n )\n onClose()\n router.push(crmRoutes(basePath).contact(answer.contactId))\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : 'The lead could not be converted.')\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} maxWidth=\"sm\" fullWidth>\n <DialogTitle>{`Convert ${leadLabel}`}</DialogTitle>\n <DialogContent>\n <Stack spacing={3} sx={{ pt: 1 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Contact'}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${email} becomes a contact at the Sales qualified stage — or ` +\n 'joins the one this address already has.'}\n </Typography>\n <LeadOwnerSelect value={ownerUid} onChange={setOwnerUid} roster={roster} />\n </Stack>\n <Divider />\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Company'}</Typography>\n <RadioGroup\n value={companyMode}\n onChange={(event) => setCompanyMode(event.target.value as CompanyMode)}\n >\n <FormControlLabel value=\"none\" control={<Radio size=\"small\" />} label=\"No company\" />\n <FormControlLabel\n value=\"existing\"\n control={<Radio size=\"small\" />}\n label=\"Link an existing company\"\n disabled={companies.length === 0}\n />\n <FormControlLabel\n value=\"new\"\n control={<Radio size=\"small\" />}\n label=\"Create a company\"\n />\n </RadioGroup>\n {companyMode === 'existing' ? (\n <FormControl size=\"small\" fullWidth>\n <InputLabel id={companyLabelId}>{'Company'}</InputLabel>\n <Select\n labelId={companyLabelId}\n label=\"Company\"\n value={companyId}\n onChange={(event) => setCompanyId(String(event.target.value))}\n >\n {companies.map((company) => (\n <MenuItem key={company.$id} value={company.$id}>\n {String(company.name ?? company.$id)}\n {company.domain ? (\n <Typography\n component=\"span\"\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ ml: 1 }}\n >\n {String(company.domain)}\n </Typography>\n ) : null}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n ) : null}\n {companyMode === 'new' ? (\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n <TextField\n size=\"small\"\n label=\"Company name\"\n value={companyName}\n onChange={(event) => setCompanyName(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 120 } }}\n />\n <TextField\n size=\"small\"\n label=\"Domain\"\n value={companyDomain}\n onChange={(event) => setCompanyDomain(event.target.value)}\n helperText=\"Used to match future contacts to this company\"\n fullWidth\n />\n </Stack>\n ) : null}\n </Stack>\n <Divider />\n <Stack spacing={1}>\n <FormControlLabel\n control={\n <Checkbox\n checked={dealOn}\n onChange={(event) => setDealOn(event.target.checked)}\n />\n }\n label={<Typography variant=\"subtitle2\">{'Open a deal'}</Typography>}\n />\n {dealOn ? (\n <Stack spacing={1}>\n <TextField\n size=\"small\"\n label=\"Deal title\"\n value={dealTitle}\n onChange={(event) => setDealTitle(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n <TextField\n size=\"small\"\n label=\"Amount\"\n value={dealAmount}\n onChange={(event) => setDealAmount(event.target.value)}\n error={amountInvalid}\n helperText={amountInvalid ? 'Enter an amount like 1200 or 1,200.50' : ' '}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Currency\"\n value={dealCurrency}\n onChange={(event) => setDealCurrency(event.target.value)}\n helperText=\" \"\n sx={{ width: { sm: 140 } }}\n slotProps={{ htmlInput: { maxLength: 3 } }}\n />\n </Stack>\n <FormControl size=\"small\" fullWidth>\n {/* `shrink`, because `displayEmpty` below draws the \"First\n stage\" placeholder where an empty value would leave the\n field blank — and a label that has not shrunk sits on\n top of it. */}\n <InputLabel id={stageLabelId} shrink>\n {'Stage'}\n </InputLabel>\n <Select\n labelId={stageLabelId}\n label=\"Stage\"\n notched\n value={dealStageId}\n onChange={(event) => setDealStageId(String(event.target.value))}\n displayEmpty\n >\n <MenuItem value=\"\">\n <em>{stages[0] ? `First stage (${stages[0].name})` : 'First stage'}</em>\n </MenuItem>\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n {!pipeline ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'This workspace has no pipeline yet — a Sales pipeline ' +\n 'with the default stages is created with the deal.'}\n </Typography>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" onClick={() => void submit()} disabled={!submittable}>\n {busy ? 'Converting…' : 'Convert'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nLeadConvertDialog.displayName = 'LeadConvertDialog'\n\nexport default LeadConvertDialog\n"],"names":["Aglyn","authorizedFetch","useSnackbar","useFirestore","useFirestoreCollection","useUser","Alert","Button","Checkbox","Dialog","DialogActions","DialogContent","DialogTitle","Divider","FormControl","FormControlLabel","InputLabel","MenuItem","Radio","RadioGroup","Select","Stack","TextField","Typography","collection","documentId","limit","orderBy","query","where","useRouter","useEffect","useId","useMemo","useRef","useState","dollarsToCents","suggestCompanyForLead","crmRoutes","LeadOwnerSelect","COMPANY_OPTIONS_CEILING","LeadConvertDialog","props","pipelines","lead","open","onClose","hostId","orgId","org","leadId","basePath","roster","firestore","router","data","user","enqueueSnackbar","visibleToTokens","group","consentGroupForHost","ORG_SCOPE_TOKEN","hostIds","map","id","hostScopeToken","slice","MAX_SCOPE_HOSTS","tokensKey","join","companies","status","companiesStatus","CRM_COLLECTIONS","idField","pipeline","find","entry","isDefault","stages","DEFAULT_DEAL_STAGES","filter","stage","kind","sort","a","b","order","email","String","leadLabel","ownerUid","setOwnerUid","companyMode","setCompanyMode","companyId","setCompanyId","companyName","setCompanyName","companyDomain","setCompanyDomain","dealOn","setDealOn","dealTitle","setDealTitle","dealAmount","setDealAmount","dealCurrency","setDealCurrency","dealStageId","setDealStageId","busy","setBusy","error","setError","companyLabelId","stageLabelId","seededFor","current","uid","suggestion","company","mode","name","domain","amountCents","amountInvalid","undefined","submittable","trim","length","Boolean","submit","body","createCompany","deal","title","currency","toLowerCase","stageId","response","method","headers","JSON","stringify","answer","json","catch","ok","alreadyConverted","variant","persist","push","contact","contactId","cause","Error","message","maxWidth","fullWidth","spacing","sx","pt","color","value","onChange","event","target","control","size","label","disabled","labelId","$id","component","ml","direction","xs","sm","slotProps","htmlInput","maxLength","helperText","checked","width","shrink","notched","displayEmpty","em","severity","onClick","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-convert-dialog.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 { CrmDealStage, CrmLeadFields } from '@aglyn/aglyn'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Checkbox,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Divider,\n FormControl,\n FormControlLabel,\n InputLabel,\n MenuItem,\n Radio,\n RadioGroup,\n Select,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n documentId,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useEffect, useId, useMemo, useRef, useState } from 'react'\nimport {\n dollarsToCents,\n suggestCompanyForLead,\n} from '../model/lead-company-suggestion'\nimport { crmRoutes } from '../model/crm-routes'\nimport type { LeadConvertRequest, LeadConvertResponse } from '../server/lead-convert'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport { LeadOwnerSelect } from './lead-owner-select'\n\n/**\n * How many companies the picker loads. Ordered by name, so a bigger org sees\n * its first two hundred alphabetically and the domain match still finds the\n * right one when it is among them; past that the converter creates and the\n * route dedupes by domain server-side, which is the backstop this ceiling\n * relies on.\n */\nconst COMPANY_OPTIONS_CEILING = 200\n\ntype CompanyMode = 'none' | 'existing' | 'new'\n\nexport interface LeadConvertDialogProps {\n open: boolean\n onClose: () => void\n /**\n * The site the conversion is FILED AS (AGL-3278): the mounted one, or\n * the lead's own first capturing site at the organization level. A\n * conversion has to name one — the contact it makes is captured by a\n * site, and its consent is that site's group's — so with none the\n * dialog refuses rather than posting an empty host.\n */\n hostId: string | null\n orgId: string | null\n org: Record<string, unknown> | null | undefined\n leadId: string\n lead: Record<string, unknown> & CrmLeadFields\n basePath: string\n roster: OrgMemberOptions\n}\n\n/**\n * Convert a lead into a contact, and optionally a company and a deal\n * (AGL-2608).\n *\n * The dialog collects the decisions and posts ONE request to\n * `crm/lead-convert`; it writes nothing itself, because the contact can only\n * come from the server's dedupe door and the rest of the conversion has to\n * follow it in order. What it does own is the suggestion: the company the\n * lead's email domain implies, matched against the companies this reader can\n * see, so the common case is a glance and a click.\n *\n * Both reads open only while the dialog is — a converter who never opens it\n * pays for neither the company list nor the pipeline.\n */\nexport function LeadConvertDialog(props: LeadConvertDialogProps) {\n const { open, onClose, hostId, orgId, org, leadId, lead, basePath, roster } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n\n /*\n * The scope this reader may see — the same tokens every CRM listener\n * filters on, and the rules refuse a list without them. The Leads list\n * mounts the dialog closed at the organization level with no site at all\n * (the row it converts brings one), and a consent group must name a site,\n * so until then the scope is the organization's token alone — nothing reads\n * it while the dialog is closed.\n */\n const visibleToTokens = useMemo(() => {\n const group = hostId ? Aglyn.consentGroupForHost(org ?? {}, hostId) : null\n return [\n Aglyn.ORG_SCOPE_TOKEN,\n ...(group?.hostIds ?? []).map((id) => Aglyn.hostScopeToken(id)),\n ].slice(0, Aglyn.MAX_SCOPE_HOSTS)\n }, [org, hostId])\n const tokensKey = visibleToTokens.join(',')\n\n const { data: companies, status: companiesStatus } = useFirestoreCollection<\n Record<string, unknown> & { $id: string; name?: string; domain?: string }\n >(\n () =>\n open && orgId\n ? query(\n collection(firestore, 'orgs', orgId, Aglyn.CRM_COLLECTIONS.companies),\n where('visibleTo', 'array-contains-any', visibleToTokens),\n orderBy('nameLower'),\n limit(COMPANY_OPTIONS_CEILING),\n )\n : null,\n [firestore, orgId, open, tokensKey],\n { idField: '$id' },\n )\n const { data: pipelines } = useFirestoreCollection<\n Record<string, unknown> & { $id: string; isDefault?: boolean; stages?: CrmDealStage[] }\n >(\n () =>\n open && orgId\n ? query(\n collection(firestore, 'orgs', orgId, Aglyn.CRM_COLLECTIONS.pipelines),\n where('visibleTo', 'array-contains-any', visibleToTokens),\n orderBy(documentId()),\n limit(20),\n )\n : null,\n [firestore, orgId, open, tokensKey],\n { idField: '$id' },\n )\n /*\n * The pipeline the deal will open in — the route's own choice, mirrored so\n * the stage picker offers the stages the deal will really have: the flagged\n * default, else the first by id, else the stages the route will seed.\n */\n const pipeline = pipelines.find((entry) => entry.isDefault) ?? pipelines[0]\n const stages = useMemo(\n () =>\n [...(pipeline?.stages ?? Aglyn.DEFAULT_DEAL_STAGES)]\n .filter((stage) => stage.kind === 'open')\n .sort((a, b) => a.order - b.order),\n [pipeline],\n )\n\n const email = String(lead['email'] ?? '')\n const leadLabel = String(lead['name'] || email || leadId)\n\n const [ownerUid, setOwnerUid] = useState('')\n const [companyMode, setCompanyMode] = useState<CompanyMode>('none')\n const [companyId, setCompanyId] = useState('')\n const [companyName, setCompanyName] = useState('')\n const [companyDomain, setCompanyDomain] = useState('')\n const [dealOn, setDealOn] = useState(false)\n const [dealTitle, setDealTitle] = useState('')\n const [dealAmount, setDealAmount] = useState('')\n const [dealCurrency, setDealCurrency] = useState('usd')\n const [dealStageId, setDealStageId] = useState('')\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n // The labels' ids, so the two comboboxes are named \"Company\" and \"Stage\"\n // rather than after whatever they show — see `LeadOwnerSelect`.\n const companyLabelId = useId()\n const stageLabelId = useId()\n\n /*\n * Reset on every open, and seed the company step ONCE per open when the\n * company list has answered — not on every snapshot, or a converter who\n * changed their mind would have the suggestion put back under them.\n */\n const seededFor = useRef<string | null>(null)\n useEffect(() => {\n if (!open) {\n seededFor.current = null\n return\n }\n setOwnerUid(String(lead.ownerUid ?? user?.uid ?? ''))\n setDealOn(false)\n setDealTitle(leadLabel)\n setDealAmount('')\n setDealCurrency('usd')\n setDealStageId('')\n setError(null)\n setCompanyMode('none')\n setCompanyId('')\n setCompanyName('')\n setCompanyDomain('')\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, leadId])\n useEffect(() => {\n if (!open || companiesStatus !== 'success') return\n if (seededFor.current === leadId) return\n seededFor.current = leadId\n // The lead's own company text first (AGL-3233), then the domain.\n const suggestion = suggestCompanyForLead(email, companies, lead.company)\n setCompanyMode(suggestion.mode)\n if (suggestion.mode === 'existing') setCompanyId(suggestion.companyId)\n if (suggestion.mode === 'new') {\n setCompanyName(suggestion.name)\n setCompanyDomain(suggestion.domain)\n }\n }, [open, companiesStatus, companies, email, leadId, lead.company])\n\n const amountCents = dollarsToCents(dealAmount)\n const amountInvalid = amountCents === undefined\n const submittable =\n !busy &&\n // A conversion names the site it is filed as; there is none for a lead\n // no site captured (AGL-3278).\n Boolean(hostId) &&\n (companyMode !== 'new' || companyName.trim().length > 0) &&\n (companyMode !== 'existing' || Boolean(companyId)) &&\n (!dealOn || (dealTitle.trim().length > 0 && !amountInvalid))\n\n const submit = async () => {\n if (!submittable) return\n setBusy(true)\n setError(null)\n const body: LeadConvertRequest = {\n hostId: hostId ?? '',\n leadId,\n ...(ownerUid ? { ownerUid } : {}),\n ...(companyMode === 'existing' ? { companyId } : {}),\n ...(companyMode === 'new'\n ? {\n createCompany: {\n name: companyName.trim(),\n ...(companyDomain.trim() ? { domain: companyDomain.trim() } : {}),\n },\n }\n : {}),\n ...(dealOn\n ? {\n deal: {\n title: dealTitle.trim(),\n ...(typeof amountCents === 'number' ? { amountCents } : {}),\n currency: dealCurrency.trim().toLowerCase() || 'usd',\n ...(dealStageId ? { stageId: dealStageId } : {}),\n },\n }\n : {}),\n }\n try {\n const response = await authorizedFetch(user, '/api/crm/lead-convert', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n const answer = (await response.json().catch(() => ({}))) as\n | LeadConvertResponse\n | { error?: string }\n if (!response.ok || !('ok' in answer)) {\n setError(\n String((answer as { error?: string }).error ?? 'The lead could not be converted.'),\n )\n return\n }\n enqueueSnackbar(\n answer.alreadyConverted\n ? 'This lead was already converted — opening the contact'\n : 'Lead converted',\n { variant: 'success', persist: false },\n )\n onClose()\n router.push(crmRoutes(basePath).contact(answer.contactId))\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : 'The lead could not be converted.')\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} maxWidth=\"sm\" fullWidth>\n <DialogTitle>{`Convert ${leadLabel}`}</DialogTitle>\n <DialogContent>\n <Stack spacing={3} sx={{ pt: 1 }}>\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Contact'}</Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${email} becomes a contact at the Sales qualified stage — or ` +\n 'joins the one this address already has.'}\n </Typography>\n <LeadOwnerSelect value={ownerUid} onChange={setOwnerUid} roster={roster} />\n </Stack>\n <Divider />\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Company'}</Typography>\n <RadioGroup\n value={companyMode}\n onChange={(event) => setCompanyMode(event.target.value as CompanyMode)}\n >\n <FormControlLabel value=\"none\" control={<Radio size=\"small\" />} label=\"No company\" />\n <FormControlLabel\n value=\"existing\"\n control={<Radio size=\"small\" />}\n label=\"Link an existing company\"\n disabled={companies.length === 0}\n />\n <FormControlLabel\n value=\"new\"\n control={<Radio size=\"small\" />}\n label=\"Create a company\"\n />\n </RadioGroup>\n {companyMode === 'existing' ? (\n <FormControl size=\"small\" fullWidth>\n <InputLabel id={companyLabelId}>{'Company'}</InputLabel>\n <Select\n labelId={companyLabelId}\n label=\"Company\"\n value={companyId}\n onChange={(event) => setCompanyId(String(event.target.value))}\n >\n {companies.map((company) => (\n <MenuItem key={company.$id} value={company.$id}>\n {String(company.name ?? company.$id)}\n {company.domain ? (\n <Typography\n component=\"span\"\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ ml: 1 }}\n >\n {String(company.domain)}\n </Typography>\n ) : null}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n ) : null}\n {companyMode === 'new' ? (\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n <TextField\n size=\"small\"\n label=\"Company name\"\n value={companyName}\n onChange={(event) => setCompanyName(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 120 } }}\n />\n <TextField\n size=\"small\"\n label=\"Domain\"\n value={companyDomain}\n onChange={(event) => setCompanyDomain(event.target.value)}\n helperText=\"Used to match future contacts to this company\"\n fullWidth\n />\n </Stack>\n ) : null}\n </Stack>\n <Divider />\n <Stack spacing={1}>\n <FormControlLabel\n control={\n <Checkbox\n checked={dealOn}\n onChange={(event) => setDealOn(event.target.checked)}\n />\n }\n label={<Typography variant=\"subtitle2\">{'Open a deal'}</Typography>}\n />\n {dealOn ? (\n <Stack spacing={1}>\n <TextField\n size=\"small\"\n label=\"Deal title\"\n value={dealTitle}\n onChange={(event) => setDealTitle(event.target.value)}\n fullWidth\n slotProps={{ htmlInput: { maxLength: 200 } }}\n />\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n <TextField\n size=\"small\"\n label=\"Amount\"\n value={dealAmount}\n onChange={(event) => setDealAmount(event.target.value)}\n error={amountInvalid}\n helperText={amountInvalid ? 'Enter an amount like 1200 or 1,200.50' : ' '}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Currency\"\n value={dealCurrency}\n onChange={(event) => setDealCurrency(event.target.value)}\n helperText=\" \"\n sx={{ width: { sm: 140 } }}\n slotProps={{ htmlInput: { maxLength: 3 } }}\n />\n </Stack>\n <FormControl size=\"small\" fullWidth>\n {/* `shrink`, because `displayEmpty` below draws the \"First\n stage\" placeholder where an empty value would leave the\n field blank — and a label that has not shrunk sits on\n top of it. */}\n <InputLabel id={stageLabelId} shrink>\n {'Stage'}\n </InputLabel>\n <Select\n labelId={stageLabelId}\n label=\"Stage\"\n notched\n value={dealStageId}\n onChange={(event) => setDealStageId(String(event.target.value))}\n displayEmpty\n >\n <MenuItem value=\"\">\n <em>{stages[0] ? `First stage (${stages[0].name})` : 'First stage'}</em>\n </MenuItem>\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n {!pipeline ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'This workspace has no pipeline yet — a Sales pipeline ' +\n 'with the default stages is created with the deal.'}\n </Typography>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n {error ? <Alert severity=\"error\">{error}</Alert> : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" onClick={() => void submit()} disabled={!submittable}>\n {busy ? 'Converting…' : 'Convert'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nLeadConvertDialog.displayName = 'LeadConvertDialog'\n\nexport default LeadConvertDialog\n"],"names":["Aglyn","authorizedFetch","useSnackbar","useFirestore","useFirestoreCollection","useUser","Alert","Button","Checkbox","Dialog","DialogActions","DialogContent","DialogTitle","Divider","FormControl","FormControlLabel","InputLabel","MenuItem","Radio","RadioGroup","Select","Stack","TextField","Typography","collection","documentId","limit","orderBy","query","where","useRouter","useEffect","useId","useMemo","useRef","useState","dollarsToCents","suggestCompanyForLead","crmRoutes","LeadOwnerSelect","COMPANY_OPTIONS_CEILING","LeadConvertDialog","props","pipelines","lead","open","onClose","hostId","orgId","org","leadId","basePath","roster","firestore","router","data","user","enqueueSnackbar","visibleToTokens","group","consentGroupForHost","ORG_SCOPE_TOKEN","hostIds","map","id","hostScopeToken","slice","MAX_SCOPE_HOSTS","tokensKey","join","companies","status","companiesStatus","CRM_COLLECTIONS","idField","pipeline","find","entry","isDefault","stages","DEFAULT_DEAL_STAGES","filter","stage","kind","sort","a","b","order","email","String","leadLabel","ownerUid","setOwnerUid","companyMode","setCompanyMode","companyId","setCompanyId","companyName","setCompanyName","companyDomain","setCompanyDomain","dealOn","setDealOn","dealTitle","setDealTitle","dealAmount","setDealAmount","dealCurrency","setDealCurrency","dealStageId","setDealStageId","busy","setBusy","error","setError","companyLabelId","stageLabelId","seededFor","current","uid","suggestion","company","mode","name","domain","amountCents","amountInvalid","undefined","submittable","Boolean","trim","length","submit","body","createCompany","deal","title","currency","toLowerCase","stageId","response","method","headers","JSON","stringify","answer","json","catch","ok","alreadyConverted","variant","persist","push","contact","contactId","cause","Error","message","maxWidth","fullWidth","spacing","sx","pt","color","value","onChange","event","target","control","size","label","disabled","labelId","$id","component","ml","direction","xs","sm","slotProps","htmlInput","maxLength","helperText","checked","width","shrink","notched","displayEmpty","em","severity","onClick","displayName"],"mappings":"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"}
@@ -4,10 +4,24 @@ import { type CrmDetailPageProps } from '../model/crm-routes';
4
4
  *
5
5
  * The record behind a row of the Leads section: what the person did on the
6
6
  * site, who is working them, and the conversion that turns them into a
7
- * contact, a company and a deal. One document listen `hosts/{hostId}/leads`
8
- * is host-scoped by path, so there is no `visibleTo` to filter and one
9
- * roster request for the owner picker; the convert dialog's reads open only
10
- * when it does.
7
+ * contact, a company and a deal. One document listen and one roster request
8
+ * for the owner picker; the convert dialog's reads open only when it does.
9
+ *
10
+ * ## The two levels, and the site a lead is acted on as (AGL-3278)
11
+ *
12
+ * The org root comes from `useCrmScope`, as it does on every other CRM
13
+ * record page: under a site it is looked up from that site, and at the
14
+ * organization level it is the mount's, known at once. It used to come
15
+ * from `useOrgDataScope({ hostId })` — correct only under a site, because
16
+ * an absent host settles at `orgId: null`, and a null org builds no
17
+ * reference, opens no listener and leaves the page loading forever.
18
+ *
19
+ * Several surfaces still need ONE site: the campaigns to offer, the feed
20
+ * an activity is filed in, the booking door, the consent basis, the
21
+ * conversion. Under a site that is the mounted one. At the organization
22
+ * level it is the lead's own first capturing site — `leadPrimaryGroup` —
23
+ * the same answer `contactPrimaryGroup` gives the contact page, and the
24
+ * same one the Leads list already converts a row as.
11
25
  */
12
26
  export declare function LeadDetailPage(props: CrmDetailPageProps): import("react").JSX.Element;
13
27
  export declare namespace LeadDetailPage {
@@ -14,13 +14,15 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
- import { pluginDocsHelp, normalizeContactEmail, readErasureRequestedAtMs } from "@aglyn/aglyn";
19
- import { useFirestore, useFirestoreDoc, useHostCampaigns, useOrgDataScope } from "@aglyn/tenant-feature-instance";
17
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
+ import { pluginDocsHelp, leadPrimaryGroup, normalizeContactEmail, readErasureRequestedAtMs } from "@aglyn/aglyn";
19
+ import { useFirestore, useFirestoreDoc, useHostCampaigns } from "@aglyn/tenant-feature-instance";
20
20
  import { Stack, Typography } from "@mui/material";
21
21
  import { doc } from "firebase/firestore";
22
- import { useState } from "react";
22
+ import { useMemo, useState } from "react";
23
23
  import { useCampaignFilingLog } from "../hooks/use-campaign-filing-log.js";
24
+ import { CrmCreateSiteDefault } from "../hooks/use-crm-org-mount.js";
25
+ import { useCrmScope } from "../hooks/use-crm-scope.js";
24
26
  import { useOrgMemberOptions } from "../hooks/use-org-member-options.js";
25
27
  import { crmRoutes } from "../model/crm-routes.js";
26
28
  import { CrmRecordChip, CrmRecordHeader } from "./crm-record-header.js";
@@ -37,42 +39,70 @@ import { RecordActivityCard } from "./record-activity-card.js";
37
39
  *
38
40
  * The record behind a row of the Leads section: what the person did on the
39
41
  * site, who is working them, and the conversion that turns them into a
40
- * contact, a company and a deal. One document listen `hosts/{hostId}/leads`
41
- * is host-scoped by path, so there is no `visibleTo` to filter and one
42
- * roster request for the owner picker; the convert dialog's reads open only
43
- * when it does.
42
+ * contact, a company and a deal. One document listen and one roster request
43
+ * for the owner picker; the convert dialog's reads open only when it does.
44
+ *
45
+ * ## The two levels, and the site a lead is acted on as (AGL-3278)
46
+ *
47
+ * The org root comes from `useCrmScope`, as it does on every other CRM
48
+ * record page: under a site it is looked up from that site, and at the
49
+ * organization level it is the mount's, known at once. It used to come
50
+ * from `useOrgDataScope({ hostId })` — correct only under a site, because
51
+ * an absent host settles at `orgId: null`, and a null org builds no
52
+ * reference, opens no listener and leaves the page loading forever.
53
+ *
54
+ * Several surfaces still need ONE site: the campaigns to offer, the feed
55
+ * an activity is filed in, the booking door, the consent basis, the
56
+ * conversion. Under a site that is the mounted one. At the organization
57
+ * level it is the lead's own first capturing site — `leadPrimaryGroup` —
58
+ * the same answer `contactPrimaryGroup` gives the contact page, and the
59
+ * same one the Leads list already converts a row as.
44
60
  */ export function LeadDetailPage(props) {
45
61
  const { id, hostId, org, basePath } = props;
46
62
  const firestore = useFirestore();
47
63
  const routes = crmRoutes(basePath);
48
64
  /*
49
65
  * Resolved BEFORE the read that needs it (AGL-3275). A lead is an org row
50
- * now, so the org has to be known to address one — and `orgId` is null
66
+ * now, so the org has to be known to address one — and `scope` is null
51
67
  * while the host-index lookup settles. Handing that null to `doc()` is a
52
68
  * thrown `TypeError`, which the page renders as a 500 rather than as a
53
69
  * record still loading.
54
- */ const { orgId } = useOrgDataScope({
55
- hostId
70
+ */ const { scope, orgId, ready, consentGroup: viewingGroup } = useCrmScope({
71
+ hostId,
72
+ org
56
73
  });
57
- const { data: lead, status, fromCache } = useFirestoreDoc(()=>orgId ? doc(firestore, 'orgs', orgId, 'leads', id) : null, [
74
+ const { data: lead, status, fromCache } = useFirestoreDoc(()=>scope ? doc(firestore, scope[0], scope[1], 'leads', id) : null, [
58
75
  firestore,
59
- orgId,
76
+ scope,
60
77
  id
61
78
  ]);
79
+ /*
80
+ * The group this lead is READ through: the mounted site's under a site,
81
+ * and the lead's own first capturing site at the organization level
82
+ * (AGL-3278). The consent basis is read against it — a refusal recorded
83
+ * for any sibling in a declared group stands against every one of them,
84
+ * so a group of one would report a person as opted in who had
85
+ * unsubscribed from the brand beside this one.
86
+ */ const leadGroup = useMemo(()=>viewingGroup != null ? viewingGroup : leadPrimaryGroup(lead, org), [
87
+ viewingGroup,
88
+ lead,
89
+ org
90
+ ]);
91
+ /** The one site this page acts as, or null for a lead no site captured. */ const siteHostId = hostId != null ? hostId : leadGroup.hostId || null;
62
92
  const roster = useOrgMemberOptions(orgId);
63
93
  /*
64
94
  * The site's campaigns, read once for the page (AGL-3274): the header
65
95
  * names the ones the lead is filed under and the Campaigns card offers
66
- * them. One listener, not one per surface — a lead belongs to one site,
67
- * so the containers are that site's.
68
- */ const campaigns = useHostCampaigns(hostId, {
96
+ * them. One listener, not one per surface — the containers are the
97
+ * capturing site's.
98
+ */ const campaigns = useHostCampaigns(siteHostId != null ? siteHostId : undefined, {
69
99
  enabled: true
70
100
  });
71
101
  // A saved filing is written on the lead's Activity too (AGL-3274), one
72
102
  // entry per campaign added or removed, by the member who saved it.
73
103
  const logFiling = useCampaignFilingLog({
74
104
  orgId,
75
- hostId,
105
+ hostId: siteHostId,
76
106
  org: org
77
107
  });
78
108
  const [converting, setConverting] = useState(false);
@@ -81,7 +111,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
81
111
  // contact: the same request, filed by the lead's address.
82
112
  const leadEmail = lead ? normalizeContactEmail(lead['email']) : null;
83
113
  const erase = useErasePersonAction({
84
- hostId,
114
+ hostId: siteHostId,
85
115
  orgId,
86
116
  subject: lead && leadEmail ? {
87
117
  kind: 'lead',
@@ -91,7 +121,13 @@ import { RecordActivityCard } from "./record-activity-card.js";
91
121
  requestedAtMs: readErasureRequestedAtMs(lead)
92
122
  });
93
123
  const label = lead ? String(lead['name'] || lead['email'] || id) : undefined;
94
- if (status === 'error' || status === 'success' && !lead) {
124
+ /*
125
+ * A SETTLED lookup with no org is an answer, not a wait (AGL-3278). The
126
+ * spinner below is for a read in flight; standing on it once `ready` has
127
+ * said there is no org to read from is the endless "Loading…" this page
128
+ * shipped with.
129
+ */ const noOrg = ready && !scope;
130
+ if (noOrg || status === 'error' || status === 'success' && !lead) {
95
131
  return /*#__PURE__*/ _jsx(CrmRecordHeader, {
96
132
  kind: "Lead",
97
133
  title: undefined,
@@ -103,7 +139,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
103
139
  children: /*#__PURE__*/ _jsx(Typography, {
104
140
  variant: "body2",
105
141
  color: "text.secondary",
106
- children: status === 'error' ? 'This lead could not be read.' : 'This lead no longer exists — it may have been removed from the Inbox.'
142
+ children: noOrg || status === 'error' ? 'This lead could not be read.' : 'This lead no longer exists — it may have been removed from the Inbox.'
107
143
  })
108
144
  });
109
145
  }
@@ -119,13 +155,17 @@ import { RecordActivityCard } from "./record-activity-card.js";
119
155
  loading: true
120
156
  });
121
157
  }
122
- return /*#__PURE__*/ _jsxs(_Fragment, {
158
+ return(/* A task or an activity created from this page defaults to the lead's
159
+ own site, not to the site the reader last picked in a list's drawer
160
+ (AGL-2630) — as on every other record page. */ /*#__PURE__*/ _jsxs(CrmCreateSiteDefault, {
161
+ hostId: siteHostId,
123
162
  children: [
124
163
  /*#__PURE__*/ _jsxs(Stack, {
125
164
  spacing: 3,
126
165
  children: [
127
166
  /*#__PURE__*/ _jsx(LeadPropertiesCard, {
128
- hostId: hostId,
167
+ hostId: siteHostId,
168
+ consentGroup: leadGroup,
129
169
  orgId: orgId,
130
170
  leadId: id,
131
171
  lead: lead,
@@ -148,14 +188,14 @@ import { RecordActivityCard } from "./record-activity-card.js";
148
188
  }, name))
149
189
  }),
150
190
  /*#__PURE__*/ _jsx(CrmRecordInsightsZone, {
151
- hostId: hostId,
191
+ hostId: siteHostId,
152
192
  org: org,
153
193
  kind: "lead",
154
194
  recordId: id,
155
195
  name: label != null ? label : ''
156
196
  }),
157
197
  /*#__PURE__*/ _jsx(LeadCampaignsCard, {
158
- hostId: hostId,
198
+ hostId: siteHostId,
159
199
  leadId: id,
160
200
  lead: lead,
161
201
  leadStatus: status,
@@ -180,12 +220,12 @@ import { RecordActivityCard } from "./record-activity-card.js";
180
220
  }
181
221
  }),
182
222
  /*#__PURE__*/ _jsx(LeadHistoryCard, {
183
- hostId: hostId,
223
+ hostId: siteHostId,
184
224
  leadId: id,
185
225
  lead: lead
186
226
  }),
187
227
  /*#__PURE__*/ _jsx(RecordActivityCard, {
188
- hostId: hostId,
228
+ hostId: siteHostId,
189
229
  org: org,
190
230
  leadId: id
191
231
  })
@@ -194,7 +234,7 @@ import { RecordActivityCard } from "./record-activity-card.js";
194
234
  /*#__PURE__*/ _jsx(LeadConvertDialog, {
195
235
  open: converting,
196
236
  onClose: ()=>setConverting(false),
197
- hostId: hostId,
237
+ hostId: siteHostId,
198
238
  orgId: orgId,
199
239
  org: org,
200
240
  leadId: id,
@@ -205,13 +245,13 @@ import { RecordActivityCard } from "./record-activity-card.js";
205
245
  /*#__PURE__*/ _jsx(LeadUnqualifyDialog, {
206
246
  open: unqualifying,
207
247
  onClose: ()=>setUnqualifying(false),
208
- hostId: hostId,
248
+ hostId: siteHostId,
209
249
  leadId: id,
210
250
  leadLabel: label != null ? label : id
211
251
  }),
212
252
  erase.dialog
213
253
  ]
214
- });
254
+ }));
215
255
  }
216
256
  LeadDetailPage.displayName = 'LeadDetailPage';
217
257
  export default LeadDetailPage;