@aglyn/plugins-crm 1.0.0-beta.230 → 1.0.0-beta.232
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/lib/components/activity-timeline.js +1 -0
- package/src/lib/components/activity-timeline.js.map +1 -1
- package/src/lib/components/assignment-rule-drawer.js +1 -0
- package/src/lib/components/assignment-rule-drawer.js.map +1 -1
- package/src/lib/components/contact-associations-card.js +1 -0
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-duplicates-card.js +1 -0
- package/src/lib/components/contact-duplicates-card.js.map +1 -1
- package/src/lib/components/contact-field-drawer.js +1 -0
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/components/contact-known-by-card.js +1 -0
- package/src/lib/components/contact-known-by-card.js.map +1 -1
- package/src/lib/components/contact-timeline-card.js +1 -0
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/crm-glance-card.js +1 -0
- package/src/lib/components/crm-glance-card.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.d.ts +1 -13
- package/src/lib/components/crm-next-activity-column.js +1 -19
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/crm-record-header.js +2 -0
- package/src/lib/components/crm-record-header.js.map +1 -1
- package/src/lib/components/deals-section.js +1 -0
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.js +4 -0
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-history-card.js +1 -0
- package/src/lib/components/lead-history-card.js.map +1 -1
- package/src/lib/components/lead-status-options.d.ts +2 -14
- package/src/lib/components/lead-status-options.js +1 -13
- package/src/lib/components/lead-status-options.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +1 -0
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.d.ts +1 -2
- package/src/lib/components/lead-unqualify-dialog.js +2 -1
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/linked-deals-card.js +1 -0
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/picklist-values-card.js +1 -0
- package/src/lib/components/picklist-values-card.js.map +1 -1
- package/src/lib/components/recipes-card.js +1 -0
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/reports/activity-card.js +1 -0
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/components/reports/closed-deals-card.js +1 -0
- package/src/lib/components/reports/closed-deals-card.js.map +1 -1
- package/src/lib/components/reports/contacts-trend-card.js +1 -0
- package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +1 -0
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/lead-source-card.js +1 -0
- package/src/lib/components/reports/lead-source-card.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +1 -0
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +1 -0
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/reports/tasks-card.js +1 -0
- package/src/lib/components/reports/tasks-card.js.map +1 -1
- package/src/lib/components/reports-section.js +2 -0
- package/src/lib/components/reports-section.js.map +1 -1
- package/src/lib/components/settings-section.d.ts +18 -0
- package/src/lib/components/settings-section.js +97 -1
- package/src/lib/components/settings-section.js.map +1 -1
- package/src/lib/components/tasks-calendar.js +1 -0
- package/src/lib/components/tasks-calendar.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +2 -2
- package/src/lib/constants/company-filters.js +1 -1
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +3 -3
- package/src/lib/constants/contact-filters.js +2 -1
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +3 -3
- package/src/lib/constants/deal-filters.js +1 -1
- package/src/lib/constants/deal-filters.js.map +1 -1
- package/src/lib/constants/field-list-query.d.ts +1 -1
- package/src/lib/constants/field-list-query.js +1 -1
- package/src/lib/constants/field-list-query.js.map +1 -1
- package/src/lib/declarations.server.js +9 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/contacts-list-seed.d.ts +1 -1
- package/src/lib/model/contacts-list-seed.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +4 -4
- package/src/lib/model/crm-list-query.js +2 -2
- package/src/lib/model/crm-list-query.js.map +1 -1
- package/src/lib/model/crm-next-activity.d.ts +13 -0
- package/src/lib/model/crm-next-activity.js +19 -0
- package/src/lib/model/crm-next-activity.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +6 -5
- package/src/lib/model/lead-filters.js +3 -2
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/lead-status-choices.d.ts +32 -0
- package/src/lib/model/lead-status-choices.js +31 -0
- package/src/lib/model/lead-status-choices.js.map +1 -0
- package/src/lib/model/task-views.d.ts +2 -2
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/person-records.d.ts +27 -0
- package/src/lib/server/person-records.js +127 -4
- package/src/lib/server/person-records.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-history-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { CAPTURED_BY_HOST_FIELD, CONTACT_SOURCE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\n// The component path and NOT the marketing barrel, for the reason the Inbox\n// and the contacts list give: the barrel is the tenant loader's entry point\n// for the plugin's SITE half, and a console card named there ships to every\n// published page.\nimport {\n CrmRecordAttributionZone,\n useHasCrmRecordAttribution,\n} from './crm-attribution-zone'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Chip, Stack, Typography } from '@mui/material'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\n\n/**\n * The surfaces `addHostLead` names — `signup`, `booking`, `form:{formId}`,\n * `import` — as they read on screen. A form's id is not a name, so it is\n * shown as the kind with the id beside it rather than as an opaque token.\n * The bare kind `form` is the lifecycle backfill's spelling for a person\n * whose timeline kept no form id (AGL-2631), and reads as the kind, the\n * way the contact's own source chip does.\n */\nexport function leadSourceLabel(source: string): string {\n if (source === 'signup') return 'Sign-up'\n if (source === 'booking') return 'Booking'\n if (source === 'import') return CONTACT_SOURCE_LABELS.import\n if (source === 'form') return CONTACT_SOURCE_LABELS.form\n // A lead entered by hand or over the REST API (AGL-3231).\n if (source === 'manual') return 'Added by hand'\n if (source === 'api') return 'API'\n if (source.startsWith('form:')) return `Form ${source.slice('form:'.length)}`\n return source\n}\n\n/** Every surface that produced a capture — the array, or the older single field. */\nexport function leadSources(lead: Record<string, unknown>): string[] {\n const sources = lead['sources']\n if (Array.isArray(sources) && sources.length) {\n return sources.map((source) => String(source))\n }\n return typeof lead['source'] === 'string' && lead['source'] ? [lead['source']] : []\n}\n\n/** Epoch millis or a Firestore timestamp, as a local date-time, or a dash. */\nexport function leadTimeLabel(value: unknown): string {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return new Date(value).toLocaleString()\n }\n const asDate = (value as { toDate?: () => Date } | null | undefined)?.toDate?.()\n return asDate ? asDate.toLocaleString() : '—'\n}\n\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\nexport interface LeadHistoryCardProps {\n /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Named on \"Captured on\" when the document\n * carries no capture list of its own, and the one site the attribution\n * zone is asked about — with none the zone is not drawn, as on the\n * contact's Associations card.\n */\n hostId: string | null\n leadId: string\n lead: Record<string, unknown>\n}\n\n/**\n * What the capture door recorded about this person (AGL-2608): every\n * surface that produced a capture, how many times, and when the first and\n * the latest happened — the fields `addHostLead` keeps on the one document\n * per person — plus the campaign the first capture is credited to.\n *\n * Read-only by construction. Nothing here is the team's to edit: it is what\n * the visitor did, and the CRM's working state lives on the card above it.\n */\nexport function LeadHistoryCard(props: LeadHistoryCardProps) {\n const { hostId, leadId, lead } = props\n // The caption introduces what another plugin draws; with none loaded it\n // would introduce nothing.\n const hasAttribution = useHasCrmRecordAttribution()\n // Sites read by NAME at the organization level, as they do in the Leads\n // list's \"Known by\" column; under a site there is no mount and an id is\n // all there is to print.\n const mount = useCrmOrgMount()\n const sources = leadSources(lead)\n const capturedBy = lead[CAPTURED_BY_HOST_FIELD]\n const count = Number(lead['submissionCount'] ?? 0) || (sources.length ? 1 : 0)\n return (\n <CardDisplay\n header={'Captured history'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={3}>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={3}>\n <Fact label=\"First seen\">\n {leadTimeLabel(lead['firstSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Last seen\">\n {leadTimeLabel(lead['lastSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Captures\">{count ? String(count) : '—'}</Fact>\n <Fact label=\"Captured on\">\n {Array.isArray(capturedBy) && capturedBy.length\n ? capturedBy\n .map((id) => mount?.siteName(String(id)) ?? String(id))\n .join(', ')\n : (hostId ?? '—')}\n </Fact>\n </Stack>\n <Fact label=\"Sources\">\n {sources.length ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip key={source} size=\"small\" variant=\"outlined\" label={leadSourceLabel(source)} />\n ))}\n </Stack>\n ) : (\n '—'\n )}\n </Fact>\n {hasAttribution && hostId ? (\n <Stack spacing={1}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Where this lead came from'}\n </Typography>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"lead\"\n recordId={leadId}\n />\n {/* Attribution is which campaign brought the person; the\n filing on the Campaigns card is the team's own (AGL-3274),\n and a reader who sees one campaign here and another there\n is owed the difference. */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Filing is separate — see Filed under campaigns.'}\n </Typography>\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadHistoryCard.displayName = 'LeadHistoryCard'\n\nexport default LeadHistoryCard\n"],"names":["CAPTURED_BY_HOST_FIELD","CONTACT_SOURCE_LABELS","pluginDocsHelp","CrmRecordAttributionZone","useHasCrmRecordAttribution","CardDisplay","Chip","Stack","Typography","useCrmOrgMount","leadSourceLabel","source","import","form","startsWith","slice","length","leadSources","lead","sources","Array","isArray","map","String","leadTimeLabel","value","Number","isFinite","Date","toLocaleString","asDate","toDate","Fact","props","spacing","variant","color","label","component","children","LeadHistoryCard","hostId","leadId","hasAttribution","mount","capturedBy","count","header","help","anchor","contentGutterX","contentGutterY","direction","xs","md","id","siteName","join","sx","flexWrap","rowGap","size","recordKind","recordId","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-history-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { CAPTURED_BY_HOST_FIELD, CONTACT_SOURCE_LABELS, pluginDocsHelp } from '@aglyn/aglyn'\n// The component path and NOT the marketing barrel, for the reason the Inbox\n// and the contacts list give: the barrel is the tenant loader's entry point\n// for the plugin's SITE half, and a console card named there ships to every\n// published page.\nimport {\n CrmRecordAttributionZone,\n useHasCrmRecordAttribution,\n} from './crm-attribution-zone'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Chip, Stack, Typography } from '@mui/material'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\n\n/**\n * The surfaces `addHostLead` names — `signup`, `booking`, `form:{formId}`,\n * `import` — as they read on screen. A form's id is not a name, so it is\n * shown as the kind with the id beside it rather than as an opaque token.\n * The bare kind `form` is the lifecycle backfill's spelling for a person\n * whose timeline kept no form id (AGL-2631), and reads as the kind, the\n * way the contact's own source chip does.\n */\nexport function leadSourceLabel(source: string): string {\n if (source === 'signup') return 'Sign-up'\n if (source === 'booking') return 'Booking'\n if (source === 'import') return CONTACT_SOURCE_LABELS.import\n if (source === 'form') return CONTACT_SOURCE_LABELS.form\n // A lead entered by hand or over the REST API (AGL-3231).\n if (source === 'manual') return 'Added by hand'\n if (source === 'api') return 'API'\n if (source.startsWith('form:')) return `Form ${source.slice('form:'.length)}`\n return source\n}\n\n/** Every surface that produced a capture — the array, or the older single field. */\nexport function leadSources(lead: Record<string, unknown>): string[] {\n const sources = lead['sources']\n if (Array.isArray(sources) && sources.length) {\n return sources.map((source) => String(source))\n }\n return typeof lead['source'] === 'string' && lead['source'] ? [lead['source']] : []\n}\n\n/** Epoch millis or a Firestore timestamp, as a local date-time, or a dash. */\nexport function leadTimeLabel(value: unknown): string {\n if (typeof value === 'number' && Number.isFinite(value)) {\n return new Date(value).toLocaleString()\n }\n const asDate = (value as { toDate?: () => Date } | null | undefined)?.toDate?.()\n return asDate ? asDate.toLocaleString() : '—'\n}\n\nfunction Fact(props: { label: string; children: React.ReactNode }) {\n return (\n <Stack spacing={0.25}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\">\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\nexport interface LeadHistoryCardProps {\n /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Named on \"Captured on\" when the document\n * carries no capture list of its own, and the one site the attribution\n * zone is asked about — with none the zone is not drawn, as on the\n * contact's Associations card.\n */\n hostId: string | null\n leadId: string\n lead: Record<string, unknown>\n}\n\n/**\n * What the capture door recorded about this person (AGL-2608): every\n * surface that produced a capture, how many times, and when the first and\n * the latest happened — the fields `addHostLead` keeps on the one document\n * per person — plus the campaign the first capture is credited to.\n *\n * Read-only by construction. Nothing here is the team's to edit: it is what\n * the visitor did, and the CRM's working state lives on the card above it.\n */\nexport function LeadHistoryCard(props: LeadHistoryCardProps) {\n const { hostId, leadId, lead } = props\n // The caption introduces what another plugin draws; with none loaded it\n // would introduce nothing.\n const hasAttribution = useHasCrmRecordAttribution()\n // Sites read by NAME at the organization level, as they do in the Leads\n // list's \"Known by\" column; under a site there is no mount and an id is\n // all there is to print.\n const mount = useCrmOrgMount()\n const sources = leadSources(lead)\n const capturedBy = lead[CAPTURED_BY_HOST_FIELD]\n const count = Number(lead['submissionCount'] ?? 0) || (sources.length ? 1 : 0)\n return (\n <CardDisplay\n header={'Captured history'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={3}>\n <Stack direction={{ xs: 'column', md: 'row' }} spacing={3}>\n <Fact label=\"First seen\">\n {leadTimeLabel(lead['firstSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Last seen\">\n {leadTimeLabel(lead['lastSeenAtMs'] ?? lead['createdAt'])}\n </Fact>\n <Fact label=\"Captures\">{count ? String(count) : '—'}</Fact>\n <Fact label=\"Captured on\">\n {Array.isArray(capturedBy) && capturedBy.length\n ? capturedBy\n .map((id) => mount?.siteName(String(id)) ?? String(id))\n .join(', ')\n : (hostId ?? '—')}\n </Fact>\n </Stack>\n <Fact label=\"Sources\">\n {sources.length ? (\n <Stack useFlexGap direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', rowGap: 0.5 }}>\n {sources.map((source) => (\n <Chip key={source} size=\"small\" variant=\"outlined\" label={leadSourceLabel(source)} />\n ))}\n </Stack>\n ) : (\n '—'\n )}\n </Fact>\n {hasAttribution && hostId ? (\n <Stack spacing={1}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Where this lead came from'}\n </Typography>\n <CrmRecordAttributionZone\n hostId={hostId}\n recordKind=\"lead\"\n recordId={leadId}\n />\n {/* Attribution is which campaign brought the person; the\n filing on the Campaigns card is the team's own (AGL-3274),\n and a reader who sees one campaign here and another there\n is owed the difference. */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Filing is separate — see Filed under campaigns.'}\n </Typography>\n </Stack>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nLeadHistoryCard.displayName = 'LeadHistoryCard'\n\nexport default LeadHistoryCard\n"],"names":["CAPTURED_BY_HOST_FIELD","CONTACT_SOURCE_LABELS","pluginDocsHelp","CrmRecordAttributionZone","useHasCrmRecordAttribution","CardDisplay","Chip","Stack","Typography","useCrmOrgMount","leadSourceLabel","source","import","form","startsWith","slice","length","leadSources","lead","sources","Array","isArray","map","String","leadTimeLabel","value","Number","isFinite","Date","toLocaleString","asDate","toDate","Fact","props","spacing","variant","color","label","component","children","LeadHistoryCard","hostId","leadId","hasAttribution","mount","capturedBy","count","header","help","anchor","contentGutterX","contentGutterY","direction","xs","md","id","siteName","join","useFlexGap","sx","flexWrap","rowGap","size","recordKind","recordId","displayName"],"mappings":"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"}
|
|
@@ -1,17 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
* The meanings a person sets by hand from a lead's status select: every one
|
|
4
|
-
* but Qualified, which only a conversion sets (AGL-2608, AGL-3512).
|
|
5
|
-
*/
|
|
6
|
-
export declare const LEAD_STATUS_HAND_SET: readonly CrmLeadStatus[];
|
|
7
|
-
/** One choice of a lead status select: the org's label, and the meaning it sets. */
|
|
8
|
-
export interface LeadStatusChoice {
|
|
9
|
-
label: string;
|
|
10
|
-
status: CrmLeadStatus;
|
|
11
|
-
inactive: boolean;
|
|
12
|
-
}
|
|
13
|
-
/** What a lead status select offers a lead: the org's active values of the hand-set meanings, and its own. */
|
|
14
|
-
export declare function leadStatusChoices(picklist: CrmPicklist, lead: Pick<CrmLeadFields, 'status' | 'statusLabel'>): LeadStatusChoice[];
|
|
1
|
+
import type { LeadStatusChoice } from '../model/lead-status-choices';
|
|
2
|
+
export { LEAD_STATUS_HAND_SET, type LeadStatusChoice, leadStatusChoices, } from '../model/lead-status-choices';
|
|
15
3
|
/**
|
|
16
4
|
* The select's items, by label (AGL-3512). An Unqualified value ends in "…"
|
|
17
5
|
* because it opens the dialog that asks why; the lead's own value, when the
|
|
@@ -15,20 +15,8 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
-
import { crmLeadStatusOptions } from "@aglyn/aglyn";
|
|
19
18
|
import { ListItemText, MenuItem } from "@mui/material";
|
|
20
|
-
|
|
21
|
-
* The meanings a person sets by hand from a lead's status select: every one
|
|
22
|
-
* but Qualified, which only a conversion sets (AGL-2608, AGL-3512).
|
|
23
|
-
*/ export const LEAD_STATUS_HAND_SET = [
|
|
24
|
-
'new',
|
|
25
|
-
'nurturing',
|
|
26
|
-
'working',
|
|
27
|
-
'unqualified'
|
|
28
|
-
];
|
|
29
|
-
/** What a lead status select offers a lead: the org's active values of the hand-set meanings, and its own. */ export function leadStatusChoices(picklist, lead) {
|
|
30
|
-
return crmLeadStatusOptions(picklist, LEAD_STATUS_HAND_SET, lead);
|
|
31
|
-
}
|
|
19
|
+
export { LEAD_STATUS_HAND_SET, leadStatusChoices } from "../model/lead-status-choices.js";
|
|
32
20
|
/**
|
|
33
21
|
* The select's items, by label (AGL-3512). An Unqualified value ends in "…"
|
|
34
22
|
* because it opens the dialog that asks why; the lead's own value, when the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-status-options.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 {
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-status-options.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 { ListItemText, MenuItem } from '@mui/material'\nimport type { LeadStatusChoice } from '../model/lead-status-choices'\n\nexport {\n LEAD_STATUS_HAND_SET,\n type LeadStatusChoice,\n leadStatusChoices,\n} from '../model/lead-status-choices'\n\n/**\n * The select's items, by label (AGL-3512). An Unqualified value ends in \"…\"\n * because it opens the dialog that asks why; the lead's own value, when the\n * list no longer offers it, is shown and not offered.\n */\nexport function leadStatusMenuItems(choices: readonly LeadStatusChoice[]) {\n return choices.map((choice) => (\n <MenuItem key={choice.label} value={choice.label} disabled={choice.inactive}>\n <ListItemText\n primary={choice.status === 'unqualified' ? `${choice.label}…` : choice.label}\n slotProps={choice.inactive ? { primary: { color: 'text.secondary' } } : undefined}\n />\n </MenuItem>\n ))\n}\n"],"names":["ListItemText","MenuItem","LEAD_STATUS_HAND_SET","leadStatusChoices","leadStatusMenuItems","choices","map","choice","value","label","disabled","inactive","primary","status","slotProps","color","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,YAAY,EAAEC,QAAQ,QAAQ,gBAAe;AAGtD,SACEC,oBAAoB,EAEpBC,iBAAiB,QACZ,kCAA8B;AAErC;;;;CAIC,GACD,OAAO,SAASC,oBAAoBC,OAAoC;IACtE,OAAOA,QAAQC,GAAG,CAAC,CAACC,uBAClB,KAACN;YAA4BO,OAAOD,OAAOE,KAAK;YAAEC,UAAUH,OAAOI,QAAQ;sBACzE,cAAA,KAACX;gBACCY,SAASL,OAAOM,MAAM,KAAK,gBAAgB,GAAGN,OAAOE,KAAK,CAAC,CAAC,CAAC,GAAGF,OAAOE,KAAK;gBAC5EK,WAAWP,OAAOI,QAAQ,GAAG;oBAAEC,SAAS;wBAAEG,OAAO;oBAAiB;gBAAE,IAAIC;;WAH7DT,OAAOE,KAAK;AAO/B"}
|
|
@@ -157,6 +157,7 @@ LeadSurfaceFormList.displayName = 'LeadSurfaceFormList';
|
|
|
157
157
|
*/ export function UnroutedLeadSurfaces(props) {
|
|
158
158
|
const { hostId, forms, formHref, turningOn, onTurnOn } = props;
|
|
159
159
|
return /*#__PURE__*/ _jsxs(Stack, {
|
|
160
|
+
useFlexGap: true,
|
|
160
161
|
direction: "row",
|
|
161
162
|
spacing: 1,
|
|
162
163
|
sx: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-surfaces-note.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { pluginRecordHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n usePluginApiPost,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack, Tooltip, Typography } from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n updateDoc,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n LEAD_SURFACE_FORMS_WINDOW,\n type LeadSurfaceForm,\n leadSurfaceForms,\n} from '../model/lead-surfaces'\n\nexport interface LeadSurfacesNoteProps {\n hostId: string\n}\n\n/** The always-on surfaces, named once wherever the note is drawn. */\nexport const LEAD_SURFACES_INTRO =\n 'Leads are created by bookings, forms with lead routing on, imports and New lead'\n/**\n * The rule that keeps one person one record (AGL-3232): a lead is the\n * whole record until it converts, and a person the workspace already holds\n * as a contact is not filed as a lead again.\n */\nexport const LEAD_SURFACES_CONTACTS_TOO =\n 'A lead is its own record until it converts into a contact. Someone who is already a contact is not filed as a lead again — their submission lands on the contact.'\n\n/** One site's forms, sorted into the two answers the note gives. */\nexport interface LeadSurfaceFormsResult {\n /** `routing.lead` is on: every submission with an address files a lead. */\n routed: LeadSurfaceForm[]\n /** The rest, each with its verdict on whether the switch may be offered. */\n unrouted: LeadSurfaceForm[]\n /** The site has more forms than the window; the lists are not the whole answer. */\n truncated: boolean\n status: ReturnType<typeof useFirestoreCollection>['status']\n}\n\n/**\n * The site's forms, ordered by id and bounded at one past the window — the\n * same read the Inbox's form picker makes, ordered by `__name__` because a\n * form saved without a display name would otherwise fall out of an ordered\n * query and vanish from a list about routing. Host-scoped by path, so any\n * member of the site may read it and no scope predicate is needed.\n *\n * One listener per call, so a note that groups several sites mounts one\n * reader per site it shows and nothing for a site it has not opened.\n */\nexport function useLeadSurfaceForms(hostId: string): LeadSurfaceFormsResult {\n const firestore = useFirestore()\n const { data: formDocs, status } = useFirestoreCollection<\n Record<string, unknown> & { $id: string }\n >(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'forms'),\n orderBy('__name__'),\n limit(LEAD_SURFACE_FORMS_WINDOW + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n return useMemo(() => {\n const forms = leadSurfaceForms(formDocs.slice(0, LEAD_SURFACE_FORMS_WINDOW))\n return {\n routed: forms.filter((form) => form.routed),\n unrouted: forms.filter((form) => !form.routed),\n truncated: formDocs.length > LEAD_SURFACE_FORMS_WINDOW,\n status,\n }\n }, [formDocs, status])\n}\n\n/** The form a switch is mid-flight on — under which site, since a form id is only unique within one. */\nexport interface LeadRoutingTarget {\n hostId: string\n formId: string\n}\n\n/**\n * The switch: one dotted `updateDoc` on `routing.lead`, so a dataset binding\n * beside it is untouched, under the same host-content write the form's page\n * makes. One at a time, whichever site the form is on — two switches in\n * flight would be two toasts racing for the same reader.\n *\n * `siteName` rides into the toast when the note shows more than one site,\n * because \"Contact now files a lead\" is an incomplete sentence to a reader\n * who can see three forms called Contact.\n */\nexport function useTurnOnLeadRouting(): {\n turningOn: LeadRoutingTarget | null\n turnOn: (target: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => Promise<void>\n} {\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const postPluginApi = usePluginApiPost()\n const [turningOn, setTurningOn] = useState<LeadRoutingTarget | null>(null)\n const turnOn = useCallback(\n async ({\n hostId,\n form,\n siteName,\n }: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => {\n if (turningOn) return\n setTurningOn({ hostId, formId: form.$id })\n const where = siteName ? ` on ${siteName}` : ''\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', form.$id), {\n 'routing.lead': true,\n updatedAt: serverTimestamp(),\n })\n // A lead-routing form holds `0` leads until its first (AGL-3330); the\n // counters are the forms plugin's to write, so it recounts them —\n // by address, since this plugin may not import that one.\n void postPluginApi('/api/forms/stats', { hostId, formIds: [form.$id] })\n enqueueSnackbar(\n `\"${form.displayName}\"${where} now files a lead from every submission ` +\n 'that carries an email address.',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar(`Lead routing could not be turned on${where}.`, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setTurningOn(null)\n }\n },\n [turningOn, firestore, enqueueSnackbar, postPluginApi],\n )\n return { turningOn, turnOn }\n}\n\n/** A form by name, linked to its own page when the console can address it. */\nexport function LeadSurfaceFormName(props: {\n form: LeadSurfaceForm\n href: string | null\n}) {\n const { form, href } = props\n return href ? (\n <AppLink href={href}>{form.displayName}</AppLink>\n ) : (\n <span>{form.displayName}</span>\n )\n}\nLeadSurfaceFormName.displayName = 'LeadSurfaceFormName'\n\n/** The routed forms as one clause — \"A, B, and possibly more\" — for a sentence to end. */\nexport function LeadSurfaceFormList(props: {\n forms: LeadSurfaceForm[]\n truncated: boolean\n formHref: (formId: string) => string | null\n}) {\n const { forms, truncated, formHref } = props\n return (\n <>\n {forms.map((form, index) => (\n <span key={form.$id}>\n {index ? ', ' : ''}\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </span>\n ))}\n {truncated ? ', and possibly more' : ''}\n </>\n )\n}\nLeadSurfaceFormList.displayName = 'LeadSurfaceFormList'\n\n/**\n * \"Not routing leads:\" — every form that does not route, with the switch\n * beside the ones that could and the reason beside the ones that cannot.\n *\n * Offered only where `leadSurfaceForms` says the publish check would honor\n * it — a form with no email field, or one that records no consent, is\n * refused with the reason as the tooltip, because a switch that flips and\n * then fails at publish is the exact confusion this note exists to remove.\n */\nexport function UnroutedLeadSurfaces(props: {\n hostId: string\n forms: LeadSurfaceForm[]\n formHref: (formId: string) => string | null\n turningOn: LeadRoutingTarget | null\n onTurnOn: (form: LeadSurfaceForm) => void\n}) {\n const { hostId, forms, formHref, turningOn, onTurnOn } = props\n return (\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Not routing leads:'}\n </Typography>\n {forms.map((form) => {\n const pending =\n turningOn?.hostId === hostId && turningOn.formId === form.$id\n return (\n <Stack\n key={form.$id}\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\" component=\"span\">\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </Typography>\n {/*\n Disabled with the reason as its tooltip rather than absent:\n an absent control and an inapplicable one look alike, and\n only one of them says what to do about it.\n */}\n <Tooltip title={form.blocker ?? ''}>\n <span>\n <Button\n size=\"small\"\n variant=\"text\"\n disabled={!form.canRoute || turningOn !== null}\n onClick={() => onTurnOn(form)}\n >\n {pending ? 'Turning on…' : 'Turn on lead routing'}\n </Button>\n </span>\n </Tooltip>\n </Stack>\n )\n })}\n </Stack>\n )\n}\nUnroutedLeadSurfaces.displayName = 'UnroutedLeadSurfaces'\n\n/**\n * WHAT CREATES A LEAD ON THIS SITE (AGL-2612).\n *\n * Every capture lands in Contacts; a LEAD is filed only by a lead surface —\n * a member sign-up, a booking, or a form whose author declared\n * `routing.lead`. The first two are always on and the third is a switch on\n * each form's own page, which left the Leads list unable to say why one\n * form's people were in it and another's were not. This says so, by name,\n * and offers the switch here for the forms that could carry it.\n *\n * The reader, the switch and the two rows are shared with the\n * organization-level note (`OrgLeadSurfacesNote`), which draws them once\n * per site; this is the one-site composition. The form links resolve the\n * site's console route from the host index, because under a site that is\n * the only address the note is handed.\n */\nexport function LeadSurfacesNote(props: LeadSurfacesNoteProps) {\n const { hostId } = props\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const { routed, unrouted, truncated, status } = useLeadSurfaceForms(hostId)\n const { turningOn, turnOn } = useTurnOnLeadRouting()\n\n const formHref = useCallback(\n (formId: string) =>\n orgSlug && host\n ? pluginRecordHref('form', { orgSlug, host }, formId)\n : null,\n [orgSlug, host],\n )\n\n return (\n <Stack spacing={0.5}>\n <Typography variant=\"body2\" color=\"text.secondary\" component=\"div\">\n {LEAD_SURFACES_INTRO}\n {status === 'loading' ? (\n '…'\n ) : routed.length ? (\n <>\n {': '}\n <LeadSurfaceFormList forms={routed} truncated={truncated} formHref={formHref} />\n {'.'}\n </>\n ) : (\n ' — no form routes leads yet.'\n )}\n {` ${LEAD_SURFACES_CONTACTS_TOO}`}\n </Typography>\n {unrouted.length ? (\n <UnroutedLeadSurfaces\n hostId={hostId}\n forms={unrouted}\n formHref={formHref}\n turningOn={turningOn}\n onTurnOn={(form) => void turnOn({ hostId, form })}\n />\n ) : null}\n </Stack>\n )\n}\nLeadSurfacesNote.displayName = 'LeadSurfacesNote'\n\nexport default LeadSurfacesNote\n"],"names":["pluginRecordHref","AppLink","useSnackbar","useConsoleHostRoute","useFirestore","useFirestoreCollection","usePluginApiPost","Button","Stack","Tooltip","Typography","collection","doc","limit","orderBy","query","serverTimestamp","updateDoc","useCallback","useMemo","useState","LEAD_SURFACE_FORMS_WINDOW","leadSurfaceForms","LEAD_SURFACES_INTRO","LEAD_SURFACES_CONTACTS_TOO","useLeadSurfaceForms","hostId","firestore","data","formDocs","status","idField","forms","slice","routed","filter","form","unrouted","truncated","length","useTurnOnLeadRouting","enqueueSnackbar","postPluginApi","turningOn","setTurningOn","turnOn","siteName","formId","$id","where","updatedAt","formIds","displayName","variant","persist","error","console","allowDuplicate","LeadSurfaceFormName","props","href","span","LeadSurfaceFormList","formHref","map","index","UnroutedLeadSurfaces","onTurnOn","direction","spacing","sx","alignItems","flexWrap","rowGap","color","pending","component","title","blocker","size","disabled","canRoute","onClick","LeadSurfacesNote","orgSlug","subdomain","host"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-surfaces-note.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport { pluginRecordHref } from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { AppLink } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useConsoleHostRoute,\n useFirestore,\n useFirestoreCollection,\n usePluginApiPost,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack, Tooltip, Typography } from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n updateDoc,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n LEAD_SURFACE_FORMS_WINDOW,\n type LeadSurfaceForm,\n leadSurfaceForms,\n} from '../model/lead-surfaces'\n\nexport interface LeadSurfacesNoteProps {\n hostId: string\n}\n\n/** The always-on surfaces, named once wherever the note is drawn. */\nexport const LEAD_SURFACES_INTRO =\n 'Leads are created by bookings, forms with lead routing on, imports and New lead'\n/**\n * The rule that keeps one person one record (AGL-3232): a lead is the\n * whole record until it converts, and a person the workspace already holds\n * as a contact is not filed as a lead again.\n */\nexport const LEAD_SURFACES_CONTACTS_TOO =\n 'A lead is its own record until it converts into a contact. Someone who is already a contact is not filed as a lead again — their submission lands on the contact.'\n\n/** One site's forms, sorted into the two answers the note gives. */\nexport interface LeadSurfaceFormsResult {\n /** `routing.lead` is on: every submission with an address files a lead. */\n routed: LeadSurfaceForm[]\n /** The rest, each with its verdict on whether the switch may be offered. */\n unrouted: LeadSurfaceForm[]\n /** The site has more forms than the window; the lists are not the whole answer. */\n truncated: boolean\n status: ReturnType<typeof useFirestoreCollection>['status']\n}\n\n/**\n * The site's forms, ordered by id and bounded at one past the window — the\n * same read the Inbox's form picker makes, ordered by `__name__` because a\n * form saved without a display name would otherwise fall out of an ordered\n * query and vanish from a list about routing. Host-scoped by path, so any\n * member of the site may read it and no scope predicate is needed.\n *\n * One listener per call, so a note that groups several sites mounts one\n * reader per site it shows and nothing for a site it has not opened.\n */\nexport function useLeadSurfaceForms(hostId: string): LeadSurfaceFormsResult {\n const firestore = useFirestore()\n const { data: formDocs, status } = useFirestoreCollection<\n Record<string, unknown> & { $id: string }\n >(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'forms'),\n orderBy('__name__'),\n limit(LEAD_SURFACE_FORMS_WINDOW + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n return useMemo(() => {\n const forms = leadSurfaceForms(formDocs.slice(0, LEAD_SURFACE_FORMS_WINDOW))\n return {\n routed: forms.filter((form) => form.routed),\n unrouted: forms.filter((form) => !form.routed),\n truncated: formDocs.length > LEAD_SURFACE_FORMS_WINDOW,\n status,\n }\n }, [formDocs, status])\n}\n\n/** The form a switch is mid-flight on — under which site, since a form id is only unique within one. */\nexport interface LeadRoutingTarget {\n hostId: string\n formId: string\n}\n\n/**\n * The switch: one dotted `updateDoc` on `routing.lead`, so a dataset binding\n * beside it is untouched, under the same host-content write the form's page\n * makes. One at a time, whichever site the form is on — two switches in\n * flight would be two toasts racing for the same reader.\n *\n * `siteName` rides into the toast when the note shows more than one site,\n * because \"Contact now files a lead\" is an incomplete sentence to a reader\n * who can see three forms called Contact.\n */\nexport function useTurnOnLeadRouting(): {\n turningOn: LeadRoutingTarget | null\n turnOn: (target: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => Promise<void>\n} {\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const postPluginApi = usePluginApiPost()\n const [turningOn, setTurningOn] = useState<LeadRoutingTarget | null>(null)\n const turnOn = useCallback(\n async ({\n hostId,\n form,\n siteName,\n }: {\n hostId: string\n form: LeadSurfaceForm\n siteName?: string | null\n }) => {\n if (turningOn) return\n setTurningOn({ hostId, formId: form.$id })\n const where = siteName ? ` on ${siteName}` : ''\n try {\n await updateDoc(doc(firestore, 'hosts', hostId, 'forms', form.$id), {\n 'routing.lead': true,\n updatedAt: serverTimestamp(),\n })\n // A lead-routing form holds `0` leads until its first (AGL-3330); the\n // counters are the forms plugin's to write, so it recounts them —\n // by address, since this plugin may not import that one.\n void postPluginApi('/api/forms/stats', { hostId, formIds: [form.$id] })\n enqueueSnackbar(\n `\"${form.displayName}\"${where} now files a lead from every submission ` +\n 'that carries an email address.',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar(`Lead routing could not be turned on${where}.`, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setTurningOn(null)\n }\n },\n [turningOn, firestore, enqueueSnackbar, postPluginApi],\n )\n return { turningOn, turnOn }\n}\n\n/** A form by name, linked to its own page when the console can address it. */\nexport function LeadSurfaceFormName(props: {\n form: LeadSurfaceForm\n href: string | null\n}) {\n const { form, href } = props\n return href ? (\n <AppLink href={href}>{form.displayName}</AppLink>\n ) : (\n <span>{form.displayName}</span>\n )\n}\nLeadSurfaceFormName.displayName = 'LeadSurfaceFormName'\n\n/** The routed forms as one clause — \"A, B, and possibly more\" — for a sentence to end. */\nexport function LeadSurfaceFormList(props: {\n forms: LeadSurfaceForm[]\n truncated: boolean\n formHref: (formId: string) => string | null\n}) {\n const { forms, truncated, formHref } = props\n return (\n <>\n {forms.map((form, index) => (\n <span key={form.$id}>\n {index ? ', ' : ''}\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </span>\n ))}\n {truncated ? ', and possibly more' : ''}\n </>\n )\n}\nLeadSurfaceFormList.displayName = 'LeadSurfaceFormList'\n\n/**\n * \"Not routing leads:\" — every form that does not route, with the switch\n * beside the ones that could and the reason beside the ones that cannot.\n *\n * Offered only where `leadSurfaceForms` says the publish check would honor\n * it — a form with no email field, or one that records no consent, is\n * refused with the reason as the tooltip, because a switch that flips and\n * then fails at publish is the exact confusion this note exists to remove.\n */\nexport function UnroutedLeadSurfaces(props: {\n hostId: string\n forms: LeadSurfaceForm[]\n formHref: (formId: string) => string | null\n turningOn: LeadRoutingTarget | null\n onTurnOn: (form: LeadSurfaceForm) => void\n}) {\n const { hostId, forms, formHref, turningOn, onTurnOn } = props\n return (\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Not routing leads:'}\n </Typography>\n {forms.map((form) => {\n const pending =\n turningOn?.hostId === hostId && turningOn.formId === form.$id\n return (\n <Stack\n key={form.$id}\n direction=\"row\"\n spacing={0.5}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\" component=\"span\">\n <LeadSurfaceFormName form={form} href={formHref(form.$id)} />\n </Typography>\n {/*\n Disabled with the reason as its tooltip rather than absent:\n an absent control and an inapplicable one look alike, and\n only one of them says what to do about it.\n */}\n <Tooltip title={form.blocker ?? ''}>\n <span>\n <Button\n size=\"small\"\n variant=\"text\"\n disabled={!form.canRoute || turningOn !== null}\n onClick={() => onTurnOn(form)}\n >\n {pending ? 'Turning on…' : 'Turn on lead routing'}\n </Button>\n </span>\n </Tooltip>\n </Stack>\n )\n })}\n </Stack>\n )\n}\nUnroutedLeadSurfaces.displayName = 'UnroutedLeadSurfaces'\n\n/**\n * WHAT CREATES A LEAD ON THIS SITE (AGL-2612).\n *\n * Every capture lands in Contacts; a LEAD is filed only by a lead surface —\n * a member sign-up, a booking, or a form whose author declared\n * `routing.lead`. The first two are always on and the third is a switch on\n * each form's own page, which left the Leads list unable to say why one\n * form's people were in it and another's were not. This says so, by name,\n * and offers the switch here for the forms that could carry it.\n *\n * The reader, the switch and the two rows are shared with the\n * organization-level note (`OrgLeadSurfacesNote`), which draws them once\n * per site; this is the one-site composition. The form links resolve the\n * site's console route from the host index, because under a site that is\n * the only address the note is handed.\n */\nexport function LeadSurfacesNote(props: LeadSurfacesNoteProps) {\n const { hostId } = props\n const { orgSlug, subdomain: host } = useConsoleHostRoute(hostId)\n const { routed, unrouted, truncated, status } = useLeadSurfaceForms(hostId)\n const { turningOn, turnOn } = useTurnOnLeadRouting()\n\n const formHref = useCallback(\n (formId: string) =>\n orgSlug && host\n ? pluginRecordHref('form', { orgSlug, host }, formId)\n : null,\n [orgSlug, host],\n )\n\n return (\n <Stack spacing={0.5}>\n <Typography variant=\"body2\" color=\"text.secondary\" component=\"div\">\n {LEAD_SURFACES_INTRO}\n {status === 'loading' ? (\n '…'\n ) : routed.length ? (\n <>\n {': '}\n <LeadSurfaceFormList forms={routed} truncated={truncated} formHref={formHref} />\n {'.'}\n </>\n ) : (\n ' — no form routes leads yet.'\n )}\n {` ${LEAD_SURFACES_CONTACTS_TOO}`}\n </Typography>\n {unrouted.length ? (\n <UnroutedLeadSurfaces\n hostId={hostId}\n forms={unrouted}\n formHref={formHref}\n turningOn={turningOn}\n onTurnOn={(form) => void turnOn({ hostId, form })}\n />\n ) : null}\n </Stack>\n )\n}\nLeadSurfacesNote.displayName = 'LeadSurfacesNote'\n\nexport default LeadSurfacesNote\n"],"names":["pluginRecordHref","AppLink","useSnackbar","useConsoleHostRoute","useFirestore","useFirestoreCollection","usePluginApiPost","Button","Stack","Tooltip","Typography","collection","doc","limit","orderBy","query","serverTimestamp","updateDoc","useCallback","useMemo","useState","LEAD_SURFACE_FORMS_WINDOW","leadSurfaceForms","LEAD_SURFACES_INTRO","LEAD_SURFACES_CONTACTS_TOO","useLeadSurfaceForms","hostId","firestore","data","formDocs","status","idField","forms","slice","routed","filter","form","unrouted","truncated","length","useTurnOnLeadRouting","enqueueSnackbar","postPluginApi","turningOn","setTurningOn","turnOn","siteName","formId","$id","where","updatedAt","formIds","displayName","variant","persist","error","console","allowDuplicate","LeadSurfaceFormName","props","href","span","LeadSurfaceFormList","formHref","map","index","UnroutedLeadSurfaces","onTurnOn","useFlexGap","direction","spacing","sx","alignItems","flexWrap","rowGap","color","pending","component","title","blocker","size","disabled","canRoute","onClick","LeadSurfacesNote","orgSlug","subdomain","host"],"mappings":"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"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const UNQUALIFY_REASON_MAX = 500;
|
|
1
|
+
export { UNQUALIFY_REASON_MAX } from '../model/lead-status-choices';
|
|
3
2
|
export interface LeadUnqualifyDialogProps {
|
|
4
3
|
open: boolean;
|
|
5
4
|
onClose: () => void;
|
|
@@ -25,7 +25,8 @@ import { useEffect, useState } from "react";
|
|
|
25
25
|
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
26
26
|
import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
|
|
27
27
|
import { useLeadStatusPicklist } from "../hooks/use-lead-status-picklist.js";
|
|
28
|
-
|
|
28
|
+
import { UNQUALIFY_REASON_MAX } from "../model/lead-status-choices.js";
|
|
29
|
+
export { UNQUALIFY_REASON_MAX } from "../model/lead-status-choices.js";
|
|
29
30
|
const REASON_MAX = UNQUALIFY_REASON_MAX;
|
|
30
31
|
/**
|
|
31
32
|
* Close a lead without converting it, with the reason why (AGL-2608).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-unqualify-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 { type CrmLeadFields, crmLeadStatusLabelFor, crmLeadStatusOptions } from '@aglyn/aglyn'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n} from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\nimport { useLeadStatusPicklist } from '../hooks/use-lead-status-picklist'\n\n/** The most a reason may say — shared with the bulk bar's one-reason-for-all (AGL-2662). */\nexport const UNQUALIFY_REASON_MAX = 500\nconst REASON_MAX = UNQUALIFY_REASON_MAX\n\nexport interface LeadUnqualifyDialogProps {\n open: boolean\n onClose: () => void\n /**\n * The lead's own site, or `null` at the organization level (AGL-3278).\n * The write is an org-scoped one-field update; the site is read for the\n * org root, which the mount answers without one.\n */\n hostId: string | null\n leadId: string\n /** How the lead reads in the title — its name, else its address. */\n leadLabel: string\n /**\n * The Unqualified value the lead is closed as, when the reader already\n * picked one (AGL-3512); the meaning's default label otherwise.\n */\n statusLabel?: string | null\n}\n\n/**\n * Close a lead without converting it, with the reason why (AGL-2608).\n *\n * The reason is REQUIRED. An unqualified lead with no reason is a row that\n * says only \"somebody gave up\", and the one thing a report on lost leads\n * wants to count is why. A client-direct write: `hosts/{hostId}/leads` is\n * not in the catch-all's update exclusions, so a site admin, editor or\n * author may update it, and nothing here needs the server.\n */\nexport function LeadUnqualifyDialog(props: LeadUnqualifyDialogProps) {\n const { open, onClose, hostId, leadId, leadLabel, statusLabel } = 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 [reason, setReason] = useState('')\n const [busy, setBusy] = useState(false)\n // The org's Unqualified values (AGL-3512): a choice only when it keeps several.\n const statuses = useLeadStatusPicklist(orgId)\n const closedAs = crmLeadStatusOptions(statuses.picklist, ['unqualified'])\n const [label, setLabel] = useState('')\n\n useEffect(() => {\n if (!open) return\n setReason('')\n setLabel(statusLabel || crmLeadStatusLabelFor(statuses.picklist, 'unqualified'))\n // Seeded as the dialog opens; the list arriving later does not reset a pick.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, statusLabel])\n\n const submit = async () => {\n const trimmed = reason.trim()\n if (!trimmed) 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 setBusy(true)\n try {\n const fields: Required<Pick<CrmLeadFields, 'status' | 'statusLabel' | 'unqualifiedReason'>> = {\n status: 'unqualified',\n // The meaning and the org's label for it, always together (AGL-3512).\n statusLabel: label || crmLeadStatusLabelFor(statuses.picklist, 'unqualified'),\n unqualifiedReason: trimmed.slice(0, REASON_MAX),\n }\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n ...fields,\n updatedAt: serverTimestamp(),\n })\n // A rule may share by status (AGL-3336).\n followUpSharing('leads', [leadId])\n enqueueSnackbar(`Lead marked ${fields.statusLabel}`, { variant: 'success', persist: false })\n onClose()\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <DialogTitle>{`Unqualify ${leadLabel}?`}</DialogTitle>\n <DialogContent>\n <DialogContentText sx={{ mb: 2 }}>\n {'The lead stays on file and drops out of the open list. Say why, ' +\n 'so the reason can be counted later.'}\n </DialogContentText>\n <Stack spacing={2}>\n {closedAs.length > 1 ? (\n <TextField\n select\n size=\"small\"\n label=\"Status\"\n value={closedAs.some((option) => option.label === label) ? label : ''}\n onChange={(event) => setLabel(String(event.target.value))}\n fullWidth\n >\n {closedAs.map((option) => (\n <MenuItem key={option.label} value={option.label}>\n {option.label}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <TextField\n label=\"Reason\"\n value={reason}\n onChange={(event) => setReason(event.target.value)}\n multiline\n minRows={2}\n fullWidth\n autoFocus\n slotProps={{ htmlInput: { maxLength: REASON_MAX } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n color=\"warning\"\n onClick={() => void submit()}\n disabled={busy || !reason.trim()}\n >\n {'Unqualify'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nLeadUnqualifyDialog.displayName = 'LeadUnqualifyDialog'\n\nexport default LeadUnqualifyDialog\n"],"names":["crmLeadStatusLabelFor","crmLeadStatusOptions","useSnackbar","useFirestore","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","MenuItem","Stack","TextField","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","useCrmSharingFollowUp","useLeadStatusPicklist","UNQUALIFY_REASON_MAX","REASON_MAX","LeadUnqualifyDialog","props","open","onClose","hostId","leadId","leadLabel","statusLabel","orgId","followUpSharing","firestore","enqueueSnackbar","reason","setReason","busy","setBusy","statuses","closedAs","picklist","label","setLabel","submit","trimmed","trim","variant","persist","fields","status","unqualifiedReason","slice","updatedAt","error","Error","message","maxWidth","fullWidth","sx","mb","spacing","length","select","size","value","some","option","onChange","event","String","target","map","multiline","minRows","autoFocus","slotProps","htmlInput","maxLength","onClick","disabled","color","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-unqualify-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 { type CrmLeadFields, crmLeadStatusLabelFor, crmLeadStatusOptions } from '@aglyn/aglyn'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogContentText,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n} from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\nimport { useLeadStatusPicklist } from '../hooks/use-lead-status-picklist'\nimport { UNQUALIFY_REASON_MAX } from '../model/lead-status-choices'\n\nexport { UNQUALIFY_REASON_MAX } from '../model/lead-status-choices'\n\nconst REASON_MAX = UNQUALIFY_REASON_MAX\n\nexport interface LeadUnqualifyDialogProps {\n open: boolean\n onClose: () => void\n /**\n * The lead's own site, or `null` at the organization level (AGL-3278).\n * The write is an org-scoped one-field update; the site is read for the\n * org root, which the mount answers without one.\n */\n hostId: string | null\n leadId: string\n /** How the lead reads in the title — its name, else its address. */\n leadLabel: string\n /**\n * The Unqualified value the lead is closed as, when the reader already\n * picked one (AGL-3512); the meaning's default label otherwise.\n */\n statusLabel?: string | null\n}\n\n/**\n * Close a lead without converting it, with the reason why (AGL-2608).\n *\n * The reason is REQUIRED. An unqualified lead with no reason is a row that\n * says only \"somebody gave up\", and the one thing a report on lost leads\n * wants to count is why. A client-direct write: `hosts/{hostId}/leads` is\n * not in the catch-all's update exclusions, so a site admin, editor or\n * author may update it, and nothing here needs the server.\n */\nexport function LeadUnqualifyDialog(props: LeadUnqualifyDialogProps) {\n const { open, onClose, hostId, leadId, leadLabel, statusLabel } = 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 [reason, setReason] = useState('')\n const [busy, setBusy] = useState(false)\n // The org's Unqualified values (AGL-3512): a choice only when it keeps several.\n const statuses = useLeadStatusPicklist(orgId)\n const closedAs = crmLeadStatusOptions(statuses.picklist, ['unqualified'])\n const [label, setLabel] = useState('')\n\n useEffect(() => {\n if (!open) return\n setReason('')\n setLabel(statusLabel || crmLeadStatusLabelFor(statuses.picklist, 'unqualified'))\n // Seeded as the dialog opens; the list arriving later does not reset a pick.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, statusLabel])\n\n const submit = async () => {\n const trimmed = reason.trim()\n if (!trimmed) 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 setBusy(true)\n try {\n const fields: Required<Pick<CrmLeadFields, 'status' | 'statusLabel' | 'unqualifiedReason'>> = {\n status: 'unqualified',\n // The meaning and the org's label for it, always together (AGL-3512).\n statusLabel: label || crmLeadStatusLabelFor(statuses.picklist, 'unqualified'),\n unqualifiedReason: trimmed.slice(0, REASON_MAX),\n }\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n ...fields,\n updatedAt: serverTimestamp(),\n })\n // A rule may share by status (AGL-3336).\n followUpSharing('leads', [leadId])\n enqueueSnackbar(`Lead marked ${fields.statusLabel}`, { variant: 'success', persist: false })\n onClose()\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <Dialog open={open} onClose={onClose} maxWidth=\"xs\" fullWidth>\n <DialogTitle>{`Unqualify ${leadLabel}?`}</DialogTitle>\n <DialogContent>\n <DialogContentText sx={{ mb: 2 }}>\n {'The lead stays on file and drops out of the open list. Say why, ' +\n 'so the reason can be counted later.'}\n </DialogContentText>\n <Stack spacing={2}>\n {closedAs.length > 1 ? (\n <TextField\n select\n size=\"small\"\n label=\"Status\"\n value={closedAs.some((option) => option.label === label) ? label : ''}\n onChange={(event) => setLabel(String(event.target.value))}\n fullWidth\n >\n {closedAs.map((option) => (\n <MenuItem key={option.label} value={option.label}>\n {option.label}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <TextField\n label=\"Reason\"\n value={reason}\n onChange={(event) => setReason(event.target.value)}\n multiline\n minRows={2}\n fullWidth\n autoFocus\n slotProps={{ htmlInput: { maxLength: REASON_MAX } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button\n variant=\"contained\"\n color=\"warning\"\n onClick={() => void submit()}\n disabled={busy || !reason.trim()}\n >\n {'Unqualify'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nLeadUnqualifyDialog.displayName = 'LeadUnqualifyDialog'\n\nexport default LeadUnqualifyDialog\n"],"names":["crmLeadStatusLabelFor","crmLeadStatusOptions","useSnackbar","useFirestore","Button","Dialog","DialogActions","DialogContent","DialogContentText","DialogTitle","MenuItem","Stack","TextField","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","useCrmSharingFollowUp","useLeadStatusPicklist","UNQUALIFY_REASON_MAX","REASON_MAX","LeadUnqualifyDialog","props","open","onClose","hostId","leadId","leadLabel","statusLabel","orgId","followUpSharing","firestore","enqueueSnackbar","reason","setReason","busy","setBusy","statuses","closedAs","picklist","label","setLabel","submit","trimmed","trim","variant","persist","fields","status","unqualifiedReason","slice","updatedAt","error","Error","message","maxWidth","fullWidth","sx","mb","spacing","length","select","size","value","some","option","onChange","event","String","target","map","multiline","minRows","autoFocus","slotProps","htmlInput","maxLength","onClick","disabled","color","displayName"],"mappings":"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"}
|
|
@@ -130,6 +130,7 @@ import { DealEditDrawer } from "./deal-edit-drawer.js";
|
|
|
130
130
|
var _ref;
|
|
131
131
|
const stage = dealStageById(pipelineState.pipelineById(deal.pipelineId), deal.stageId);
|
|
132
132
|
return /*#__PURE__*/ _jsxs(Stack, {
|
|
133
|
+
useFlexGap: true,
|
|
133
134
|
direction: "row",
|
|
134
135
|
spacing: 1,
|
|
135
136
|
sx: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/linked-deals-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { dealContactRolesOf, dealStageById, pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport { type CrmOrgDoc, useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmFoldsScope } from '../hooks/use-crm-list-query'\nimport { useContactRoleDeals, useLinkedDeals } from '../hooks/use-deals'\nimport { usePipeline } from '../hooks/use-pipeline'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n type DealDoc,\n DEAL_STATUS_LABELS,\n formatMoney,\n} from '../model/deal-board-model'\nimport type { DealFormValues } from '../model/deal-form-model'\nimport { DealEditDrawer } from './deal-edit-drawer'\n\nexport interface LinkedDealsCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org: CrmOrgDoc\n /** The CRM surface's own path, for links into deals. */\n basePath: string\n /** Which record this card sits on. */\n link: { contactId: string; contactName?: string } | { companyId: string; companyName?: string }\n}\n\n/**\n * The deals that name one record, on that record's page (AGL-2598).\n *\n * One bounded listener over the indexed link — `(visibleTo, contactId |\n * companyId, updatedAt DESC)` — and a \"New deal\" that opens the shared\n * drawer with this record already chosen, so a deal started from a person's\n * page is linked to them without anybody searching for them again. The\n * contact card and the company card are this component with the link\n * named; the two exports below exist so the pages that host them import\n * one name each.\n *\n * On a contact's page the card also lists the deals the person is on in\n * any other role (AGL-3521) — a second listener, `useContactRoleDeals`,\n * merged by id — and names the part they play on each.\n */\nexport function LinkedDealsCard(props: LinkedDealsCardProps) {\n const { hostId, org, basePath, link } = props\n const routes = crmRoutes(basePath)\n const scope = useCrmScope({ hostId, org })\n const pipelineState = usePipeline(scope.orgId, {\n hostId,\n org: (org ?? null) as Record<string, unknown> | null,\n })\n const { data: linked, status } = useLinkedDeals(\n scope.orgId,\n scope.visibleTo,\n 'contactId' in link ? { contactId: link.contactId } : { companyId: link.companyId },\n )\n const contactId = 'contactId' in link ? link.contactId : null\n const foldsScope = useCrmFoldsScope(scope.orgId, scope.visibleTo)\n const { data: roleDeals } = useContactRoleDeals(scope.orgId, scope.visibleTo, foldsScope, contactId)\n // The two answers as one list, newest first; a deal in both is listed once.\n const deals = useMemo(() => {\n const byId = new Map<string, DealDoc>()\n for (const deal of [...(linked ?? []), ...(roleDeals ?? [])]) byId.set(deal.$id, deal)\n return [...byId.values()].sort((a, b) => updatedMs(b) - updatedMs(a))\n }, [linked, roleDeals])\n const [creating, setCreating] = useState(false)\n const defaults = useMemo<Partial<DealFormValues>>(\n () =>\n 'contactId' in link\n ? { contactId: link.contactId, contactName: link.contactName ?? '' }\n : { companyId: link.companyId, companyName: link.companyName ?? '' },\n [link],\n )\n\n return (\n <>\n <CardDisplay\n header={'Deals'}\n help={pluginDocsHelp('deals', { anchor: '#a-deals-page' })}\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipelineState.pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n {status === 'success' && deals.length === 0 ? (\n <EmptyStateComponent\n compact\n label={'No deals yet'}\n description={'A deal started here is linked to this record from the first save.'}\n action={\n <Button\n size=\"small\"\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipelineState.pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n }\n />\n ) : (\n <Stack spacing={1}>\n {deals.map((deal) => {\n const stage = dealStageById(\n pipelineState.pipelineById(deal.pipelineId),\n deal.stageId,\n )\n return (\n <Stack\n key={deal.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Typography variant=\"body2\" sx={{ flex: 1, minWidth: 160 }} noWrap>\n <AppLink href={routes.deal(deal.$id)}>{deal.title || 'Untitled deal'}</AppLink>\n </Typography>\n {contactId ? <ContactRoleChip deal={deal} contactId={contactId} /> : null}\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={\n deal.status === 'open'\n ? (stage?.name ?? deal.stageId)\n : DEAL_STATUS_LABELS[deal.status]\n }\n color={deal.status === 'won' ? 'success' : 'default'}\n />\n <Typography variant=\"body2\" sx={{ minWidth: 90, textAlign: 'right' }}>\n {typeof deal.amountCents === 'number'\n ? formatMoney(deal.amountCents, deal.currency)\n : '—'}\n </Typography>\n </Stack>\n )\n })}\n </Stack>\n )}\n </CardDisplay>\n <DealEditDrawer\n open={creating}\n onClose={() => setCreating(false)}\n hostId={hostId}\n org={org}\n defaults={defaults}\n pipelines={pipelineState.activePipelines}\n defaultPipeline={pipelineState.pipeline}\n />\n </>\n )\n}\nLinkedDealsCard.displayName = 'LinkedDealsCard'\n\n/** When a deal was last written, for the merged list's order. */\nfunction updatedMs(deal: DealDoc): number {\n const value = (deal as { updatedAt?: unknown }).updatedAt\n if (value instanceof Date) return value.getTime()\n const stamp = value as { toMillis?: () => number } | null | undefined\n return typeof stamp?.toMillis === 'function' ? stamp.toMillis() : 0\n}\n\n/** The part this contact plays on a deal — its role, and Primary when they are. */\nfunction ContactRoleChip(props: { deal: DealDoc; contactId: string }) {\n const row = dealContactRolesOf(props.deal).find((entry) => entry.contactId === props.contactId)\n if (!row) return null\n const label = [row.role, row.primary ? 'Primary' : null].filter(Boolean).join(' · ')\n return label ? <Chip size=\"small\" label={label} /> : null\n}\n\nexport default LinkedDealsCard\n"],"names":["dealContactRolesOf","dealStageById","pluginDocsHelp","mdiPlus","AppLink","CardDisplay","MdiIcon","EmptyStateComponent","Button","Chip","Stack","Typography","useMemo","useState","useCrmScope","useCrmFoldsScope","useContactRoleDeals","useLinkedDeals","usePipeline","crmRoutes","DEAL_STATUS_LABELS","formatMoney","DealEditDrawer","LinkedDealsCard","props","hostId","org","basePath","link","routes","scope","pipelineState","orgId","data","linked","status","visibleTo","contactId","companyId","foldsScope","roleDeals","deals","byId","Map","deal","set","$id","values","sort","a","b","updatedMs","creating","setCreating","defaults","contactName","companyName","header","help","anchor","HeaderProps","action","size","startIcon","path","disabled","pipeline","onClick","contentGutterX","contentGutterY","length","compact","label","description","variant","spacing","map","stage","pipelineById","pipelineId","stageId","direction","sx","alignItems","flexWrap","rowGap","flex","minWidth","noWrap","href","title","ContactRoleChip","name","color","textAlign","amountCents","currency","open","onClose","pipelines","activePipelines","defaultPipeline","displayName","value","updatedAt","Date","getTime","stamp","toMillis","row","find","entry","role","primary","filter","Boolean","join"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/linked-deals-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { dealContactRolesOf, dealStageById, pluginDocsHelp } from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { AppLink, CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport { type CrmOrgDoc, useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmFoldsScope } from '../hooks/use-crm-list-query'\nimport { useContactRoleDeals, useLinkedDeals } from '../hooks/use-deals'\nimport { usePipeline } from '../hooks/use-pipeline'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n type DealDoc,\n DEAL_STATUS_LABELS,\n formatMoney,\n} from '../model/deal-board-model'\nimport type { DealFormValues } from '../model/deal-form-model'\nimport { DealEditDrawer } from './deal-edit-drawer'\n\nexport interface LinkedDealsCardProps {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org: CrmOrgDoc\n /** The CRM surface's own path, for links into deals. */\n basePath: string\n /** Which record this card sits on. */\n link: { contactId: string; contactName?: string } | { companyId: string; companyName?: string }\n}\n\n/**\n * The deals that name one record, on that record's page (AGL-2598).\n *\n * One bounded listener over the indexed link — `(visibleTo, contactId |\n * companyId, updatedAt DESC)` — and a \"New deal\" that opens the shared\n * drawer with this record already chosen, so a deal started from a person's\n * page is linked to them without anybody searching for them again. The\n * contact card and the company card are this component with the link\n * named; the two exports below exist so the pages that host them import\n * one name each.\n *\n * On a contact's page the card also lists the deals the person is on in\n * any other role (AGL-3521) — a second listener, `useContactRoleDeals`,\n * merged by id — and names the part they play on each.\n */\nexport function LinkedDealsCard(props: LinkedDealsCardProps) {\n const { hostId, org, basePath, link } = props\n const routes = crmRoutes(basePath)\n const scope = useCrmScope({ hostId, org })\n const pipelineState = usePipeline(scope.orgId, {\n hostId,\n org: (org ?? null) as Record<string, unknown> | null,\n })\n const { data: linked, status } = useLinkedDeals(\n scope.orgId,\n scope.visibleTo,\n 'contactId' in link ? { contactId: link.contactId } : { companyId: link.companyId },\n )\n const contactId = 'contactId' in link ? link.contactId : null\n const foldsScope = useCrmFoldsScope(scope.orgId, scope.visibleTo)\n const { data: roleDeals } = useContactRoleDeals(scope.orgId, scope.visibleTo, foldsScope, contactId)\n // The two answers as one list, newest first; a deal in both is listed once.\n const deals = useMemo(() => {\n const byId = new Map<string, DealDoc>()\n for (const deal of [...(linked ?? []), ...(roleDeals ?? [])]) byId.set(deal.$id, deal)\n return [...byId.values()].sort((a, b) => updatedMs(b) - updatedMs(a))\n }, [linked, roleDeals])\n const [creating, setCreating] = useState(false)\n const defaults = useMemo<Partial<DealFormValues>>(\n () =>\n 'contactId' in link\n ? { contactId: link.contactId, contactName: link.contactName ?? '' }\n : { companyId: link.companyId, companyName: link.companyName ?? '' },\n [link],\n )\n\n return (\n <>\n <CardDisplay\n header={'Deals'}\n help={pluginDocsHelp('deals', { anchor: '#a-deals-page' })}\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipelineState.pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n {status === 'success' && deals.length === 0 ? (\n <EmptyStateComponent\n compact\n label={'No deals yet'}\n description={'A deal started here is linked to this record from the first save.'}\n action={\n <Button\n size=\"small\"\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipelineState.pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n }\n />\n ) : (\n <Stack spacing={1}>\n {deals.map((deal) => {\n const stage = dealStageById(\n pipelineState.pipelineById(deal.pipelineId),\n deal.stageId,\n )\n return (\n <Stack\n useFlexGap\n key={deal.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 0.5 }}\n >\n <Typography variant=\"body2\" sx={{ flex: 1, minWidth: 160 }} noWrap>\n <AppLink href={routes.deal(deal.$id)}>{deal.title || 'Untitled deal'}</AppLink>\n </Typography>\n {contactId ? <ContactRoleChip deal={deal} contactId={contactId} /> : null}\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={\n deal.status === 'open'\n ? (stage?.name ?? deal.stageId)\n : DEAL_STATUS_LABELS[deal.status]\n }\n color={deal.status === 'won' ? 'success' : 'default'}\n />\n <Typography variant=\"body2\" sx={{ minWidth: 90, textAlign: 'right' }}>\n {typeof deal.amountCents === 'number'\n ? formatMoney(deal.amountCents, deal.currency)\n : '—'}\n </Typography>\n </Stack>\n )\n })}\n </Stack>\n )}\n </CardDisplay>\n <DealEditDrawer\n open={creating}\n onClose={() => setCreating(false)}\n hostId={hostId}\n org={org}\n defaults={defaults}\n pipelines={pipelineState.activePipelines}\n defaultPipeline={pipelineState.pipeline}\n />\n </>\n )\n}\nLinkedDealsCard.displayName = 'LinkedDealsCard'\n\n/** When a deal was last written, for the merged list's order. */\nfunction updatedMs(deal: DealDoc): number {\n const value = (deal as { updatedAt?: unknown }).updatedAt\n if (value instanceof Date) return value.getTime()\n const stamp = value as { toMillis?: () => number } | null | undefined\n return typeof stamp?.toMillis === 'function' ? stamp.toMillis() : 0\n}\n\n/** The part this contact plays on a deal — its role, and Primary when they are. */\nfunction ContactRoleChip(props: { deal: DealDoc; contactId: string }) {\n const row = dealContactRolesOf(props.deal).find((entry) => entry.contactId === props.contactId)\n if (!row) return null\n const label = [row.role, row.primary ? 'Primary' : null].filter(Boolean).join(' · ')\n return label ? <Chip size=\"small\" label={label} /> : null\n}\n\nexport default LinkedDealsCard\n"],"names":["dealContactRolesOf","dealStageById","pluginDocsHelp","mdiPlus","AppLink","CardDisplay","MdiIcon","EmptyStateComponent","Button","Chip","Stack","Typography","useMemo","useState","useCrmScope","useCrmFoldsScope","useContactRoleDeals","useLinkedDeals","usePipeline","crmRoutes","DEAL_STATUS_LABELS","formatMoney","DealEditDrawer","LinkedDealsCard","props","hostId","org","basePath","link","routes","scope","pipelineState","orgId","data","linked","status","visibleTo","contactId","companyId","foldsScope","roleDeals","deals","byId","Map","deal","set","$id","values","sort","a","b","updatedMs","creating","setCreating","defaults","contactName","companyName","header","help","anchor","HeaderProps","action","size","startIcon","path","disabled","pipeline","onClick","contentGutterX","contentGutterY","length","compact","label","description","variant","spacing","map","stage","pipelineById","pipelineId","stageId","useFlexGap","direction","sx","alignItems","flexWrap","rowGap","flex","minWidth","noWrap","href","title","ContactRoleChip","name","color","textAlign","amountCents","currency","open","onClose","pipelines","activePipelines","defaultPipeline","displayName","value","updatedAt","Date","getTime","stamp","toMillis","row","find","entry","role","primary","filter","Boolean","join"],"mappings":"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"}
|