@aglyn/plugins-crm 1.0.0-beta.162 → 1.0.0-beta.163
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 +13 -13
- package/src/lib/components/leads-bulk-bar.js +51 -5
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +93 -4
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-lead-drawer.d.ts +2 -0
- package/src/lib/components/new-lead-drawer.js +27 -1
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/declarations.server.js +15 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/crm-lead-import.d.ts +11 -2
- package/src/lib/model/crm-lead-import.js +22 -2
- package/src/lib/model/crm-lead-import.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +8 -0
- package/src/lib/model/lead-filters.js +11 -1
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/server/lead-campaign-carry.d.ts +50 -0
- package/src/lib/server/lead-campaign-carry.js +75 -0
- package/src/lib/server/lead-campaign-carry.js.map +1 -0
- package/src/lib/server/lead-create.d.ts +13 -0
- package/src/lib/server/lead-create.js +30 -0
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/leads-import.js +50 -5
- package/src/lib/server/leads-import.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/leads-section.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 { ConsolePluginPageProps, CrmLeadFields, CrmLeadStatus } from '@aglyn/aglyn'\nimport {\n mdiAccountArrowRight,\n mdiAccountCancelOutline,\n mdiAccountConvertOutline,\n mdiAccountTieOutline,\n mdiMagnify,\n} from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n type OrgMemberOptions,\n useOrgMemberOptions,\n} from '../hooks/use-org-member-options'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport { useOrgLeads } from '../hooks/use-org-leads'\nimport CrmViewsControl from './crm-views-control'\nimport RowActionsMenu from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Box,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControl,\n InputAdornment,\n InputLabel,\n MenuItem,\n Select,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n deleteField,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n updateDoc,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useId, useMemo, useState } from 'react'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n LEAD_FILTER_LABELS,\n LEAD_EMAIL_FILTER_LABELS,\n LEAD_EMAIL_FILTERS,\n LEAD_FILTERS,\n type LeadEmailFilter,\n type LeadFilter,\n leadMatchesEmailFilter,\n leadMatchesFilter,\n leadMatchesSearch,\n} from '../model/lead-filters'\nimport { type LeadCsvOptions, leadsCsv } from '../model/leads-csv'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport { leadSourceLabel, leadSources, leadTimeLabel } from './lead-history-card'\nimport { LeadImportButton } from './lead-import-drawer'\nimport NewLeadDrawer, { type NewLeadValues } from './new-lead-drawer'\nimport { useCrmApi } from './use-crm-api'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { CONVERT_PENDING_ERASURE_REASON } from './lead-properties-card'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { LeadStatusChip } from './lead-status-chip'\nimport LeadSurfacesNote from './lead-surfaces-note'\nimport { LeadUnqualifyDialog } from './lead-unqualify-dialog'\nimport LeadsBulkBar from './leads-bulk-bar'\nimport OrgLeadSurfacesNote from './org-lead-surfaces-note'\n\n/**\n * How many leads the section reads: the newest by last seen, plus one probe\n * row so \"there are more\" is a fact rather than a guess at the boundary.\n *\n * A CEILING and a client-side filter rather than a paged status query, and\n * the reason is the lead documents themselves. Every lead the capture door\n * writes carries NO `status` — the field exists only once somebody in the\n * CRM has touched the lead — and Firestore cannot select documents by a\n * field's absence: `where('status','in',[…])`, `!=` and `not-in` all skip a\n * document without the field. A server-side \"open leads\" query would\n * therefore hide every lead nobody has worked yet, which is the entire\n * population the section exists to show on the day it ships. So the query\n * is the one order every lead can satisfy (`lastSeenAtMs`, stamped on every\n * capture), and the status filter narrows the loaded window — said out loud\n * beneath the table whenever the window is not the whole collection.\n *\n * The window is then PAGED in memory under the shared footer, the way the\n * workspace pickers page a slice of a window they cannot re-key: the rows\n * are already in the snapshot, so turning a page costs nothing, and the\n * footer's count is exact because it counts the filtered window rather than\n * a collection nobody has measured.\n */\nconst LEADS_WINDOW = 200\n\n/**\n * One row of the list. `$id` keys the grid — the document id under a site,\n * `{hostId}/{leadId}` at the organization level, where a lead's id is a\n * person key the same on every site that met the person — and `leadId`\n * and `hostId` say which document, under which site, a write or a link\n * names.\n */\ntype LeadRow = Record<string, unknown> &\n CrmLeadFields & { $id: string; leadId: string; hostId: string }\n\n/**\n * `/crm/leads` — the people a site has met but not yet qualified (AGL-2608).\n *\n * A section of its own, the way Salesforce keeps Leads apart from Contacts:\n * a lead is a capture — a form, a booking, a sign-up — that somebody has\n * still to work, and it converts into a contact, a company and a deal when\n * it is real. Reads `hosts/{hostId}/leads`, host-scoped by path, so there is\n * no `visibleTo` filter; the Firestore rules admit any member of the site to\n * read it and an admin, editor or author to update it, which is what makes\n * the inline status and owner changes client-direct writes.\n *\n * At the ORGANIZATION level (AGL-2630) there is no one site to read: the\n * section opens the same query under every site the org has (`useOrgLeads`)\n * and lists the merged window with a Site column, every row naming the site\n * its writes and its link go to. The per-site notes — which of a site's\n * forms file a lead — belong to a site's own hub and are not drawn here.\n */\nexport function CrmLeadsSection(props: ConsolePluginPageProps) {\n const { hostId, org, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, createHostId } = useCrmScope({ hostId, org })\n const mount = useCrmOrgMount()\n const roster = useOrgMemberOptions(orgId)\n const routes = crmRoutes(basePath ?? '')\n\n // Under a site: the site's own window, rows keyed by document id.\n const site = useFirestoreCollection<Record<string, unknown> & CrmLeadFields & { $id: string }>(\n () =>\n hostId\n ? query(\n collection(firestore, 'hosts', hostId, 'leads'),\n orderBy('lastSeenAtMs', 'desc'),\n limit(LEADS_WINDOW + 1),\n )\n : null,\n [firestore, hostId],\n { idField: '$id' },\n )\n // At the organization level: every site's window, merged.\n const orgHostIds = useMemo(\n () => (hostId ? [] : (mount?.hosts ?? []).map((host) => host.id)),\n [hostId, mount?.hosts],\n )\n const orgLeads = useOrgLeads({ hostIds: orgHostIds, windowSize: LEADS_WINDOW })\n const leadDocs = useMemo<LeadRow[]>(\n () =>\n hostId\n ? site.data.map((row) => ({ ...row, leadId: row.$id, hostId }))\n : orgLeads.data,\n [hostId, site.data, orgLeads.data],\n )\n const status = hostId\n ? site.status\n : mount?.hostsReady && !orgHostIds.length\n ? 'success'\n : orgLeads.status\n const truncated = hostId ? leadDocs.length > LEADS_WINDOW : orgLeads.truncated\n const window = useMemo(() => leadDocs.slice(0, LEADS_WINDOW), [leadDocs])\n\n /*\n * The `Show` filter is the saved VIEW'S (AGL-2617): a saved view of leads\n * holds the status beside the columns and the sort, and the select below\n * writes into it. Unset reads as `open`, which is what the section opened\n * on before views existed and the one reading a query cannot express.\n */\n const views = useCrmSavedView({\n section: 'leads',\n hostId,\n org: props.org,\n basePath: basePath ?? '',\n })\n const filter: LeadFilter = useMemo(() => {\n const value = views.state.filters.find((clause) => clause.field === 'status')?.value\n return (LEAD_FILTERS as readonly string[]).includes(value ?? '')\n ? (value as LeadFilter)\n : 'open'\n }, [views.state.filters])\n /*\n * The `Email` filter (AGL-3245) is the view's too, as an `emailState`\n * clause beside the status one. Each setter keeps the other's clause:\n * narrowing to bounced leads does not reopen the unqualified ones.\n */\n const emailFilter: LeadEmailFilter = useMemo(() => {\n const value = views.state.filters.find((clause) => clause.field === 'emailState')?.value\n return (LEAD_EMAIL_FILTERS as readonly string[]).includes(value ?? '')\n ? (value as LeadEmailFilter)\n : 'any'\n }, [views.state.filters])\n const setFilter = useCallback(\n (next: LeadFilter) =>\n views.setFilters([\n ...views.state.filters.filter((clause) => clause.field !== 'status'),\n ...(next === 'open' ? [] : [{ field: 'status', op: 'equals', value: next }]),\n ]),\n [views.setFilters, views.state.filters],\n )\n const setEmailFilter = useCallback(\n (next: LeadEmailFilter) =>\n views.setFilters([\n ...views.state.filters.filter((clause) => clause.field !== 'emailState'),\n ...(next === 'any' ? [] : [{ field: 'emailState', op: 'equals', value: next }]),\n ]),\n [views.setFilters, views.state.filters],\n )\n // The label's id, so the filter's combobox is named \"Show\" rather than\n // after the option it shows — see `LeadOwnerSelect`.\n const filterLabelId = useId()\n const emailFilterLabelId = useId()\n /*\n * The search box is the SECTION'S, not the grid's (AGL-3246). The grid's\n * quick filter runs over the rows the grid holds, and the grid holds one\n * PAGE of the window — so a lead on page three answered \"no match\" while\n * the footer below went on counting the unfiltered window. The term\n * narrows the whole loaded window here, beside the status filter and\n * before the footer's count and the page slice, over the fields a person\n * types to find a lead: name, email, company, title and tags.\n */\n const [search, setSearch] = useState('')\n const rows = useMemo(\n () =>\n window.filter(\n (lead) =>\n leadMatchesFilter(lead, filter) &&\n leadMatchesEmailFilter(lead, emailFilter) &&\n leadMatchesSearch(lead, search),\n ),\n [window, filter, emailFilter, search],\n )\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n // A new filter or search term starts on page one: page three of the open\n // leads is not a page of the unqualified ones, and an out-of-range page\n // renders empty.\n useEffect(() => {\n setPage(0)\n }, [filter, emailFilter, search])\n const pageRows = useMemo(\n () => rows.slice(page * pageSize, (page + 1) * pageSize),\n [rows, page, pageSize],\n )\n\n /*\n * The ticked rows, for the bulk bar (AGL-2662). Cleared when the filter or\n * the search term changes: a selection made on the open leads is not a\n * selection of the unqualified ones, and the bar's count would be over\n * rows no longer listed.\n */\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n useEffect(() => setSelectedIds([]), [filter, emailFilter, search])\n // How the file names the owner and, at the org level, the site.\n const csvOptions: LeadCsvOptions = useMemo(\n () => ({\n ownerEmail: roster.emailFor,\n ...(hostId ? {} : { siteName: (id: string) => mount?.siteName(id) }),\n }),\n [roster.emailFor, hostId, mount],\n )\n // The listed window — every row the filter and the search admit, not just\n // the page.\n const handleExport = useCallback(() => {\n downloadTextFile('leads.csv', 'text/csv', leadsCsv(rows, csvOptions))\n }, [rows, csvOptions])\n\n const [assigning, setAssigning] = useState<LeadRow | null>(null)\n const [unqualifying, setUnqualifying] = useState<LeadRow | null>(null)\n // The row whose conversion dialog is open (AGL-2641) — the same dialog\n // the lead's page opens, fed the row so the list is one click shorter.\n const [converting, setConverting] = useState<LeadRow | null>(null)\n\n /*==========================================\n * NEW LEAD (AGL-3231) — Salesforce's New Lead, in a drawer over the\n * list. The route files the lead through the one lead door under the\n * mounted site, or at the organization level under the site the drawer's\n * picker named; the drawer holds its submit until one is known. It makes\n * a lead and nothing else — the conversion is what makes the contact.\n *=========================================*/\n const crmApi = useCrmApi(createHostId)\n const [createOpen, setCreateOpen] = useState(false)\n const [createBusy, setCreateBusy] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const handleCreate = useCallback(\n async (values: NewLeadValues) => {\n setCreateBusy(true)\n setCreateError(null)\n try {\n const { response, payload } = await crmApi('leads-create', {\n email: values.email,\n ...(values.name ? { name: values.name } : {}),\n ...(values.company ? { company: values.company } : {}),\n ...(values.jobTitle ? { jobTitle: values.jobTitle } : {}),\n ...(values.phone ? { phone: values.phone } : {}),\n ...(values.website ? { website: values.website } : {}),\n ...(values.leadSource ? { leadSource: values.leadSource } : {}),\n ...(values.address ? { address: values.address } : {}),\n ...(values.tags.length ? { tags: values.tags } : {}),\n ...(values.ownerUid ? { ownerUid: values.ownerUid } : {}),\n ...(values.notes ? { notes: values.notes } : {}),\n status: values.status,\n })\n if (!response.ok) {\n // The route's own sentence, shown above the form unchanged.\n setCreateError(String(payload['error'] ?? 'The lead could not be added.'))\n return\n }\n // The activity entry is the route's: it verified the caller and\n // performed the write.\n enqueueSnackbar(\n payload['created']\n ? 'Lead added'\n : 'This site already held a lead for that address — it was updated',\n { variant: 'success', persist: false },\n )\n setCreateOpen(false)\n } catch (error) {\n console.error(error)\n setCreateError('The lead could not be added.')\n } finally {\n setCreateBusy(false)\n }\n },\n [crmApi, enqueueSnackbar],\n )\n\n const writeLead = useCallback(\n async (lead: LeadRow, fields: Record<string, unknown>, done: string) => {\n try {\n await updateDoc(doc(firestore, 'hosts', lead.hostId, 'leads', lead.leadId), {\n ...fields,\n updatedAt: serverTimestamp(),\n })\n enqueueSnackbar(done, { variant: 'success', persist: false })\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The lead could not be updated.',\n { variant: 'error' },\n )\n }\n },\n [firestore, enqueueSnackbar],\n )\n\n const columns = useMemo<GridColDef[]>(\n () => [\n {\n field: 'name',\n headerName: 'Lead',\n flex: 1.4,\n minWidth: 160,\n valueGetter: (_value, row: LeadRow) => String(row['name'] || row['email'] || ''),\n renderCell: ({ row }: { row: LeadRow }) => (\n <Stack spacing={0} sx={{ minWidth: 0, justifyContent: 'center', height: '100%' }}>\n <Typography variant=\"body2\" noWrap>\n {String(row['name'] || row['email'] || row.$id)}\n </Typography>\n {row['name'] ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {String(row['email'] ?? '')}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n // The lead's own profile (AGL-3231): the two facts a work queue is\n // scanned by, beside the person.\n {\n field: 'company',\n headerName: 'Company',\n flex: 1,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) => String(row.company ?? ''),\n },\n {\n field: 'jobTitle',\n headerName: 'Title',\n flex: 0.9,\n minWidth: 130,\n valueGetter: (_value, row: LeadRow) => String(row.jobTitle ?? ''),\n },\n {\n field: 'status',\n headerName: 'Status',\n flex: 0.9,\n minWidth: 150,\n valueGetter: (_value, row: LeadRow) => Aglyn.crmLeadStatus(row),\n renderCell: ({ row }: { row: LeadRow }) => (\n <InlineStatus\n lead={row}\n onChange={(next) => {\n if (next === 'unqualified') {\n setUnqualifying(row)\n return\n }\n void writeLead(\n row,\n {\n status: next,\n ...(Aglyn.crmLeadStatus(row) === 'unqualified'\n ? { unqualifiedReason: deleteField() }\n : {}),\n },\n 'Status updated',\n )\n }}\n />\n ),\n },\n {\n /*\n * The verdict on the address (AGL-3245): the chip the lead's page\n * carries, so a bounced lead is told apart in the queue it is worked\n * from; its label for the sort and the export.\n */\n field: 'emailState',\n headerName: 'Email',\n flex: 0.9,\n minWidth: 150,\n valueGetter: (_value, row: LeadRow) => {\n const state = Aglyn.readEmailState(row)\n return state ? Aglyn.EMAIL_STATE_LABELS[state.status] : ''\n },\n renderCell: ({ row }: { row: LeadRow }) => {\n const state = Aglyn.readEmailState(row)\n return state ? (\n <CrmEmailStateChip state={state} />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n )\n },\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 1,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) =>\n row.ownerUid ? roster.labelFor(row.ownerUid) : 'Unassigned',\n },\n // Only at the organization level, where a row can be any site's.\n ...(hostId\n ? []\n : [\n {\n field: 'hostId',\n headerName: 'Site',\n flex: 0.9,\n minWidth: 140,\n valueGetter: (_value: unknown, row: LeadRow) =>\n mount?.siteName(row.hostId) ?? row.hostId,\n } satisfies GridColDef,\n ]),\n {\n field: 'sources',\n headerName: 'Source',\n flex: 1,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) =>\n leadSources(row).map(leadSourceLabel).join(', '),\n },\n {\n field: 'tags',\n headerName: 'Tags',\n flex: 0.9,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) => (row.tags ?? []).join(', '),\n },\n {\n field: 'lastSeenAtMs',\n headerName: 'Last seen',\n flex: 0.9,\n minWidth: 160,\n valueGetter: (_value, row: LeadRow) =>\n leadTimeLabel(row['lastSeenAtMs'] ?? row['createdAt']),\n },\n {\n field: 'actions',\n headerName: '',\n width: 56,\n sortable: false,\n filterable: false,\n disableColumnMenu: true,\n renderCell: ({ row }: { row: LeadRow }) => {\n /*\n * Why Convert is refused, in the order the lead's page refuses it:\n * a converted lead has its contact already, a closed one was\n * judged not real, and a person with an erasure pending must not\n * be captured again — a conversion is a capture (AGL-2623).\n */\n const converted = Boolean(row.convertedContactId)\n const erasurePending = Aglyn.readErasureRequestedAtMs(row) !== null\n const convertRefusal = converted\n ? 'This lead was converted'\n : !Aglyn.isCrmLeadOpen(row)\n ? 'This lead was unqualified'\n : erasurePending\n ? CONVERT_PENDING_ERASURE_REASON\n : null\n return (\n <Box\n onClick={(event) => event.stopPropagation()}\n sx={{ display: 'flex', alignItems: 'center', height: '100%' }}\n >\n <RowActionsMenu\n label={String(row['email'] ?? row.$id)}\n items={[\n {\n key: 'open',\n label: 'Open lead',\n icon: <MdiIcon path={mdiAccountArrowRight.path} size={0.8} />,\n href: routes.lead(row.leadId, hostId ? null : row.hostId),\n },\n {\n key: 'convert',\n label: 'Convert…',\n icon: <MdiIcon path={mdiAccountConvertOutline.path} size={0.8} />,\n onClick: () => setConverting(row),\n disabled: convertRefusal !== null,\n disabledReason: convertRefusal ?? undefined,\n },\n {\n key: 'assign',\n label: 'Assign owner',\n icon: <MdiIcon path={mdiAccountTieOutline.path} size={0.8} />,\n onClick: () => setAssigning(row),\n },\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />,\n onClick: () => setUnqualifying(row),\n disabled: !Aglyn.isCrmLeadOpen(row) || Boolean(row.convertedContactId),\n disabledReason: row.convertedContactId\n ? 'This lead was converted'\n : 'This lead is already closed',\n },\n ]}\n />\n </Box>\n )\n },\n },\n ],\n [roster, routes, writeLead, hostId, mount],\n )\n /* The column and sort models are the view's (AGL-2617). */\n const grid = useCrmViewGrid(views, columns)\n\n return (\n <>\n <CardDisplay\n header={'Leads'}\n help={Aglyn.pluginDocsHelp('contacts', { anchor: '#whats-in-the-crm-area' })}\n actions={\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n {/* The saved view this list is showing, beside the status it narrows to (AGL-2617). */}\n <CrmViewsControl controller={views} allLabel=\"All leads\" />\n <FormControl size=\"small\" sx={{ minWidth: 160 }}>\n <InputLabel id={filterLabelId}>{'Show'}</InputLabel>\n <Select\n labelId={filterLabelId}\n label=\"Show\"\n value={filter}\n onChange={(event) => setFilter(event.target.value as LeadFilter)}\n >\n {LEAD_FILTERS.map((option) => (\n <MenuItem key={option} value={option}>\n {LEAD_FILTER_LABELS[option]}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n {/* The verdict on the address (AGL-3245): the bounced, the blocked, the ones who left. */}\n <FormControl size=\"small\" sx={{ minWidth: 160 }}>\n <InputLabel id={emailFilterLabelId}>{'Email'}</InputLabel>\n <Select\n labelId={emailFilterLabelId}\n label=\"Email\"\n value={emailFilter}\n onChange={(event) => setEmailFilter(event.target.value as LeadEmailFilter)}\n >\n {LEAD_EMAIL_FILTERS.map((option) => (\n <MenuItem key={option} value={option}>\n {LEAD_EMAIL_FILTER_LABELS[option]}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n <TextField\n size=\"small\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n placeholder=\"Search leads\"\n slotProps={{\n input: {\n startAdornment: (\n <InputAdornment position=\"start\">\n <MdiIcon path={mdiMagnify.path} size={0.8} />\n </InputAdornment>\n ),\n },\n htmlInput: { 'aria-label': 'Search leads', type: 'search' },\n }}\n sx={{ minWidth: 200 }}\n />\n <LeadImportButton hostId={hostId} />\n <Button size=\"small\" onClick={handleExport} disabled={!rows.length}>\n {'Export CSV'}\n </Button>\n <Button size=\"small\" variant=\"contained\" onClick={() => setCreateOpen(true)}>\n {'New lead'}\n </Button>\n </Stack>\n }\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {/* Which surfaces file a lead, by name (AGL-2612) — under a site its own, at the org level every site's (AGL-2638). */}\n {hostId ? <LeadSurfacesNote hostId={hostId} /> : <OrgLeadSurfacesNote />}\n {status === 'success' && window.length === 0 ? (\n <EmptyStateComponent\n label={'No leads yet'}\n description={'Bookings and lead-routed forms on your site become leads on their own — or add one with New lead, or bring a list in with Import CSV.'}\n />\n ) : status === 'success' && rows.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`No ${LEAD_FILTER_LABELS[filter].toLowerCase()} leads` +\n (search.trim() ? ` match “${search.trim()}”` : '') +\n ` among the ${window.length.toLocaleString()} most recently seen.`}\n </Typography>\n ) : (\n <>\n <LeadsBulkBar\n rows={rows}\n selected={selectedIds}\n onSelectedChange={setSelectedIds}\n roster={roster}\n csv={csvOptions}\n orgId={orgId}\n hostId={hostId}\n />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={pageRows}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n loading={status === 'loading'}\n onOpen={(_id, row: LeadRow) =>\n router.push(routes.lead(row.leadId, hostId ? null : row.hostId))\n }\n // Columns and sort are the view's, controlled (AGL-2617).\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={grid.onColumnVisibilityModelChange}\n sortModel={grid.sortModel}\n onSortModelChange={grid.onSortModelChange}\n // The search is the section's, above: the grid's own box\n // would search this page alone.\n quickFilter={false}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n </CrmColumnOrderProvider>\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={rows.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n {truncated ? (\n <Alert severity=\"info\">\n {`Showing the ${LEADS_WINDOW.toLocaleString()} most recently seen ` +\n 'leads. The search box and the status filter narrow these ' +\n `${LEADS_WINDOW.toLocaleString()} only; older leads are still ` +\n 'listed in the Inbox and reached by campaign audiences.'}\n </Alert>\n ) : null}\n </Stack>\n </CardDisplay>\n <NewLeadDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n hostId={hostId ?? null}\n org={org as Record<string, unknown> | undefined}\n busy={createBusy}\n error={createError}\n roster={roster}\n onSubmit={(values) => void handleCreate(values)}\n />\n <AssignOwnerDialog\n lead={assigning}\n roster={roster}\n onClose={() => setAssigning(null)}\n onAssign={(uid) => {\n if (!assigning) return\n void writeLead(\n assigning,\n { ownerUid: uid || deleteField() },\n uid ? 'Owner assigned' : 'Owner cleared',\n )\n setAssigning(null)\n }}\n />\n <LeadUnqualifyDialog\n open={Boolean(unqualifying)}\n onClose={() => setUnqualifying(null)}\n hostId={unqualifying?.hostId ?? hostId ?? ''}\n leadId={unqualifying?.leadId ?? ''}\n leadLabel={String(unqualifying?.['name'] || unqualifying?.['email'] || '')}\n />\n {/* The row's site, not the mounted one: at the organization level a\n lead is its own site's record, and the conversion is that site's\n capture (AGL-2641). Under a site the two are the same. */}\n <LeadConvertDialog\n open={Boolean(converting)}\n onClose={() => setConverting(null)}\n hostId={converting?.hostId ?? hostId ?? ''}\n orgId={orgId}\n org={org as Record<string, unknown> | undefined}\n leadId={converting?.leadId ?? ''}\n lead={converting ?? {}}\n basePath={basePath ?? ''}\n roster={roster}\n />\n </>\n )\n}\nCrmLeadsSection.displayName = 'CrmLeadsSection'\n\n/**\n * The status, editable in place for a lead that is still open.\n *\n * A converted lead shows the chip alone: its status IS the conversion, and\n * the route stamped it. The select stops its click from reaching the row, or\n * every status change would also open the record.\n */\nfunction InlineStatus(props: {\n lead: LeadRow\n onChange: (next: CrmLeadStatus) => void\n}) {\n const { lead, onChange } = props\n if (lead.convertedContactId) {\n return (\n <Box sx={{ display: 'flex', alignItems: 'center', height: '100%' }}>\n <LeadStatusChip lead={lead} />\n </Box>\n )\n }\n const status = Aglyn.crmLeadStatus(lead)\n return (\n <Box\n onClick={(event) => event.stopPropagation()}\n sx={{ display: 'flex', alignItems: 'center', height: '100%', width: '100%' }}\n >\n <Select\n size=\"small\"\n variant=\"standard\"\n disableUnderline\n value={status}\n onChange={(event) => onChange(event.target.value as CrmLeadStatus)}\n renderValue={() => <LeadStatusChip lead={lead} />}\n sx={{ width: '100%' }}\n >\n <MenuItem value=\"new\">{Aglyn.CRM_LEAD_STATUS_LABELS.new}</MenuItem>\n <MenuItem value=\"working\">{Aglyn.CRM_LEAD_STATUS_LABELS.working}</MenuItem>\n <MenuItem value=\"unqualified\">{`${Aglyn.CRM_LEAD_STATUS_LABELS.unqualified}…`}</MenuItem>\n </Select>\n </Box>\n )\n}\nInlineStatus.displayName = 'InlineStatus'\n\n/** Hand a lead to a team member. */\nfunction AssignOwnerDialog(props: {\n lead: LeadRow | null\n roster: OrgMemberOptions\n onClose: () => void\n onAssign: (uid: string) => void\n}) {\n const { lead, roster, onClose, onAssign } = props\n const [uid, setUid] = useState<string | null>(null)\n const current = uid ?? String(lead?.ownerUid ?? '')\n return (\n <Dialog\n open={Boolean(lead)}\n onClose={onClose}\n maxWidth=\"xs\"\n fullWidth\n slotProps={{ transition: { onExited: () => setUid(null) } }}\n >\n <DialogTitle>{`Assign ${String(lead?.['name'] || lead?.['email'] || 'lead')}`}</DialogTitle>\n <DialogContent>\n <Box sx={{ pt: 1 }}>\n <LeadOwnerSelect value={current} onChange={setUid} roster={roster} size=\"medium\" />\n </Box>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{'Cancel'}</Button>\n <Button variant=\"contained\" onClick={() => onAssign(current)}>\n {'Assign'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nAssignOwnerDialog.displayName = 'AssignOwnerDialog'\n\nexport default CrmLeadsSection\n"],"names":["Aglyn","mdiAccountArrowRight","mdiAccountCancelOutline","mdiAccountConvertOutline","mdiAccountTieOutline","mdiMagnify","CardDisplay","MdiIcon","ListPagination","ListTable","useOrgMemberOptions","useCrmOrgMount","useCrmSavedView","useCrmScope","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","useOrgLeads","CrmViewsControl","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","EmptyStateComponent","useSnackbar","useFirestore","useFirestoreCollection","Alert","Box","Button","Dialog","DialogActions","DialogContent","DialogTitle","FormControl","InputAdornment","InputLabel","MenuItem","Select","Stack","TextField","Typography","collection","deleteField","doc","limit","orderBy","query","serverTimestamp","updateDoc","useRouter","useCallback","useEffect","useId","useMemo","useState","downloadTextFile","crmRoutes","LEAD_FILTER_LABELS","LEAD_EMAIL_FILTER_LABELS","LEAD_EMAIL_FILTERS","LEAD_FILTERS","leadMatchesEmailFilter","leadMatchesFilter","leadMatchesSearch","leadsCsv","LeadConvertDialog","leadSourceLabel","leadSources","leadTimeLabel","LeadImportButton","NewLeadDrawer","useCrmApi","LeadOwnerSelect","CONVERT_PENDING_ERASURE_REASON","CrmEmailStateChip","LeadStatusChip","LeadSurfacesNote","LeadUnqualifyDialog","LeadsBulkBar","OrgLeadSurfacesNote","LEADS_WINDOW","CrmLeadsSection","props","unqualifying","converting","hostId","org","basePath","firestore","router","enqueueSnackbar","orgId","createHostId","mount","roster","routes","site","idField","orgHostIds","hosts","map","host","id","orgLeads","hostIds","windowSize","leadDocs","data","row","leadId","$id","status","hostsReady","length","truncated","window","slice","views","section","filter","value","state","filters","find","clause","field","includes","emailFilter","setFilter","next","setFilters","op","setEmailFilter","filterLabelId","emailFilterLabelId","search","setSearch","rows","lead","page","setPage","pageSize","setPageSize","pageRows","selectedIds","setSelectedIds","csvOptions","ownerEmail","emailFor","siteName","handleExport","assigning","setAssigning","setUnqualifying","setConverting","crmApi","createOpen","setCreateOpen","createBusy","setCreateBusy","createError","setCreateError","handleCreate","values","response","payload","email","name","company","jobTitle","phone","website","leadSource","address","tags","ownerUid","notes","ok","String","variant","persist","error","console","writeLead","fields","done","updatedAt","Error","message","columns","headerName","flex","minWidth","valueGetter","_value","renderCell","spacing","sx","justifyContent","height","noWrap","color","crmLeadStatus","InlineStatus","onChange","unqualifiedReason","readEmailState","EMAIL_STATE_LABELS","labelFor","join","width","sortable","filterable","disableColumnMenu","converted","Boolean","convertedContactId","erasurePending","readErasureRequestedAtMs","convertRefusal","isCrmLeadOpen","onClick","event","stopPropagation","display","alignItems","label","items","key","icon","path","size","href","disabled","disabledReason","undefined","grid","header","help","pluginDocsHelp","anchor","actions","direction","controller","allLabel","labelId","target","option","placeholder","slotProps","input","startAdornment","position","htmlInput","type","contentGutterX","contentGutterY","description","toLowerCase","trim","toLocaleString","selected","onSelectedChange","csv","columnOrder","slots","selectable","loading","onOpen","_id","push","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","quickFilter","hideFooter","rowCount","count","onPageChange","onPageSizeChange","severity","open","onClose","busy","onSubmit","AssignOwnerDialog","onAssign","uid","leadLabel","displayName","disableUnderline","renderValue","CRM_LEAD_STATUS_LABELS","new","working","unqualified","setUid","current","maxWidth","fullWidth","transition","onExited","pt"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,YAAYA,WAAW,eAAc;AAErC,SACEC,oBAAoB,EACpBC,uBAAuB,EACvBC,wBAAwB,EACxBC,oBAAoB,EACpBC,UAAU,QACL,yBAAwB;AAC/B,SAASC,WAAW,EAAEC,OAAO,QAAQ,uBAAsB;AAC3D,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,SAASC,SAAS,QAAQ,uDAAsD;AAChF,SAEEC,mBAAmB,QACd,qCAAiC;AACxC,SAASC,cAAc,QAAQ,gCAA4B;AAC3D,SAASC,eAAe,QAAQ,iCAA6B;AAC7D,SAASC,WAAW,QAAQ,4BAAwB;AACpD,SAASC,cAAc,QAAQ,gCAA4B;AAC3D,SAASC,cAAc,EAAEC,sBAAsB,QAAQ,uBAAmB;AAC1E,SAASC,WAAW,QAAQ,4BAAwB;AACpD,OAAOC,qBAAqB,yBAAqB;AACjD,OAAOC,oBAAoB,6DAA4D;AACvF,SAASC,uBAAuB,QAAQ,8CAA6C;AACrF,OAAOC,yBAAyB,wDAAuD;AACvF,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,YAAY,EACZC,sBAAsB,QACjB,iCAAgC;AACvC,SACEC,KAAK,EACLC,GAAG,EACHC,MAAM,EACNC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,WAAW,EACXC,WAAW,EACXC,cAAc,EACdC,UAAU,EACVC,QAAQ,EACRC,MAAM,EACNC,KAAK,EACLC,SAAS,EACTC,UAAU,QACL,gBAAe;AAEtB,SACEC,UAAU,EACVC,WAAW,EACXC,GAAG,EACHC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,eAAe,EACfC,SAAS,QACJ,qBAAoB;AAC3B,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,WAAW,EAAEC,SAAS,EAAEC,KAAK,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACxE,SAASC,gBAAgB,QAAQ,2BAAuB;AACxD,SAASC,SAAS,QAAQ,yBAAqB;AAC/C,SACEC,kBAAkB,EAClBC,wBAAwB,EACxBC,kBAAkB,EAClBC,YAAY,EAGZC,sBAAsB,EACtBC,iBAAiB,EACjBC,iBAAiB,QACZ,2BAAuB;AAC9B,SAA8BC,QAAQ,QAAQ,wBAAoB;AAClE,SAASC,iBAAiB,QAAQ,2BAAuB;AACzD,SAASC,eAAe,EAAEC,WAAW,EAAEC,aAAa,QAAQ,yBAAqB;AACjF,SAASC,gBAAgB,QAAQ,0BAAsB;AACvD,OAAOC,mBAA2C,uBAAmB;AACrE,SAASC,SAAS,QAAQ,mBAAe;AACzC,SAASC,eAAe,QAAQ,yBAAqB;AACrD,SAASC,8BAA8B,QAAQ,4BAAwB;AACvE,SAASC,iBAAiB,QAAQ,4BAAwB;AAC1D,SAASC,cAAc,QAAQ,wBAAoB;AACnD,OAAOC,sBAAsB,0BAAsB;AACnD,SAASC,mBAAmB,QAAQ,6BAAyB;AAC7D,OAAOC,kBAAkB,sBAAkB;AAC3C,OAAOC,yBAAyB,8BAA0B;AAE1D;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,MAAMC,eAAe;AAYrB;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,gBAAgBC,KAA6B;QAolB7CC,oBAUAC;IA7lBd,MAAM,EAAEC,MAAM,EAAEC,GAAG,EAAEC,QAAQ,EAAE,GAAGL;IAClC,MAAMM,YAAYhE;IAClB,MAAMiE,SAASxC;IACf,MAAM,EAAEyC,eAAe,EAAE,GAAGnE;IAC5B,MAAM,EAAEoE,KAAK,EAAEC,YAAY,EAAE,GAAG9E,YAAY;QAAEuE;QAAQC;IAAI;IAC1D,MAAMO,QAAQjF;IACd,MAAMkF,SAASnF,oBAAoBgF;IACnC,MAAMI,SAASvC,UAAU+B,mBAAAA,WAAY;IAErC,kEAAkE;IAClE,MAAMS,OAAOvE,uBACX,IACE4D,SACIvC,MACEL,WAAW+C,WAAW,SAASH,QAAQ,UACvCxC,QAAQ,gBAAgB,SACxBD,MAAMoC,eAAe,MAEvB,MACN;QAACQ;QAAWH;KAAO,EACnB;QAAEY,SAAS;IAAM;IAEnB,0DAA0D;IAC1D,MAAMC,aAAa7C,QACjB;;eAAOgC,SAAS,EAAE,GAAG,SAACQ,yBAAAA,MAAOM,KAAK,mBAAI,EAAE,EAAEC,GAAG,CAAC,CAACC,OAASA,KAAKC,EAAE;OAC/D;QAACjB;QAAQQ,yBAAAA,MAAOM,KAAK;KAAC;IAExB,MAAMI,WAAWrF,YAAY;QAAEsF,SAASN;QAAYO,YAAYzB;IAAa;IAC7E,MAAM0B,WAAWrD,QACf,IACEgC,SACIW,KAAKW,IAAI,CAACP,GAAG,CAAC,CAACQ,MAAS,aAAKA;gBAAKC,QAAQD,IAAIE,GAAG;gBAAEzB;kBACnDkB,SAASI,IAAI,EACnB;QAACtB;QAAQW,KAAKW,IAAI;QAAEJ,SAASI,IAAI;KAAC;IAEpC,MAAMI,SAAS1B,SACXW,KAAKe,MAAM,GACXlB,CAAAA,yBAAAA,MAAOmB,UAAU,KAAI,CAACd,WAAWe,MAAM,GACrC,YACAV,SAASQ,MAAM;IACrB,MAAMG,YAAY7B,SAASqB,SAASO,MAAM,GAAGjC,eAAeuB,SAASW,SAAS;IAC9E,MAAMC,SAAS9D,QAAQ,IAAMqD,SAASU,KAAK,CAAC,GAAGpC,eAAe;QAAC0B;KAAS;IAExE;;;;;GAKC,GACD,MAAMW,QAAQxG,gBAAgB;QAC5ByG,SAAS;QACTjC;QACAC,KAAKJ,MAAMI,GAAG;QACdC,QAAQ,EAAEA,mBAAAA,WAAY;IACxB;IACA,MAAMgC,SAAqBlE,QAAQ;YACnBgE;QAAd,MAAMG,SAAQH,4BAAAA,MAAMI,KAAK,CAACC,OAAO,CAACC,IAAI,CAAC,CAACC,SAAWA,OAAOC,KAAK,KAAK,8BAAtDR,0BAAiEG,KAAK;QACpF,OAAO,AAAC5D,aAAmCkE,QAAQ,CAACN,gBAAAA,QAAS,MACxDA,QACD;IACN,GAAG;QAACH,MAAMI,KAAK,CAACC,OAAO;KAAC;IACxB;;;;GAIC,GACD,MAAMK,cAA+B1E,QAAQ;YAC7BgE;QAAd,MAAMG,SAAQH,4BAAAA,MAAMI,KAAK,CAACC,OAAO,CAACC,IAAI,CAAC,CAACC,SAAWA,OAAOC,KAAK,KAAK,kCAAtDR,0BAAqEG,KAAK;QACxF,OAAO,AAAC7D,mBAAyCmE,QAAQ,CAACN,gBAAAA,QAAS,MAC9DA,QACD;IACN,GAAG;QAACH,MAAMI,KAAK,CAACC,OAAO;KAAC;IACxB,MAAMM,YAAY9E,YAChB,CAAC+E,OACCZ,MAAMa,UAAU,CAAC;eACZb,MAAMI,KAAK,CAACC,OAAO,CAACH,MAAM,CAAC,CAACK,SAAWA,OAAOC,KAAK,KAAK;eACvDI,SAAS,SAAS,EAAE,GAAG;gBAAC;oBAAEJ,OAAO;oBAAUM,IAAI;oBAAUX,OAAOS;gBAAK;aAAE;SAC5E,GACH;QAACZ,MAAMa,UAAU;QAAEb,MAAMI,KAAK,CAACC,OAAO;KAAC;IAEzC,MAAMU,iBAAiBlF,YACrB,CAAC+E,OACCZ,MAAMa,UAAU,CAAC;eACZb,MAAMI,KAAK,CAACC,OAAO,CAACH,MAAM,CAAC,CAACK,SAAWA,OAAOC,KAAK,KAAK;eACvDI,SAAS,QAAQ,EAAE,GAAG;gBAAC;oBAAEJ,OAAO;oBAAcM,IAAI;oBAAUX,OAAOS;gBAAK;aAAE;SAC/E,GACH;QAACZ,MAAMa,UAAU;QAAEb,MAAMI,KAAK,CAACC,OAAO;KAAC;IAEzC,uEAAuE;IACvE,qDAAqD;IACrD,MAAMW,gBAAgBjF;IACtB,MAAMkF,qBAAqBlF;IAC3B;;;;;;;;GAQC,GACD,MAAM,CAACmF,QAAQC,UAAU,GAAGlF,SAAS;IACrC,MAAMmF,OAAOpF,QACX,IACE8D,OAAOI,MAAM,CACX,CAACmB,OACC5E,kBAAkB4E,MAAMnB,WACxB1D,uBAAuB6E,MAAMX,gBAC7BhE,kBAAkB2E,MAAMH,UAE9B;QAACpB;QAAQI;QAAQQ;QAAaQ;KAAO;IAEvC,MAAM,CAACI,MAAMC,QAAQ,GAAGtF,SAAS;IACjC,MAAM,CAACuF,UAAUC,YAAY,GAAGxF,SAASjC;IACzC,yEAAyE;IACzE,wEAAwE;IACxE,iBAAiB;IACjB8B,UAAU;QACRyF,QAAQ;IACV,GAAG;QAACrB;QAAQQ;QAAaQ;KAAO;IAChC,MAAMQ,WAAW1F,QACf,IAAMoF,KAAKrB,KAAK,CAACuB,OAAOE,UAAU,AAACF,CAAAA,OAAO,CAAA,IAAKE,WAC/C;QAACJ;QAAME;QAAME;KAAS;IAGxB;;;;;GAKC,GACD,MAAM,CAACG,aAAaC,eAAe,GAAG3F,SAAmB,EAAE;IAC3DH,UAAU,IAAM8F,eAAe,EAAE,GAAG;QAAC1B;QAAQQ;QAAaQ;KAAO;IACjE,gEAAgE;IAChE,MAAMW,aAA6B7F,QACjC,IAAO;YACL8F,YAAYrD,OAAOsD,QAAQ;WACvB/D,SAAS,CAAC,IAAI;YAAEgE,UAAU,CAAC/C,KAAeT,yBAAAA,MAAOwD,QAAQ,CAAC/C;QAAI,IAEpE;QAACR,OAAOsD,QAAQ;QAAE/D;QAAQQ;KAAM;IAElC,0EAA0E;IAC1E,YAAY;IACZ,MAAMyD,eAAepG,YAAY;QAC/BK,iBAAiB,aAAa,YAAYS,SAASyE,MAAMS;IAC3D,GAAG;QAACT;QAAMS;KAAW;IAErB,MAAM,CAACK,WAAWC,aAAa,GAAGlG,SAAyB;IAC3D,MAAM,CAAC6B,cAAcsE,gBAAgB,GAAGnG,SAAyB;IACjE,uEAAuE;IACvE,uEAAuE;IACvE,MAAM,CAAC8B,YAAYsE,cAAc,GAAGpG,SAAyB;IAE7D;;;;;;6CAM2C,GAC3C,MAAMqG,SAASpF,UAAUqB;IACzB,MAAM,CAACgE,YAAYC,cAAc,GAAGvG,SAAS;IAC7C,MAAM,CAACwG,YAAYC,cAAc,GAAGzG,SAAS;IAC7C,MAAM,CAAC0G,aAAaC,eAAe,GAAG3G,SAAwB;IAC9D,MAAM4G,eAAehH,YACnB,OAAOiH;QACLJ,cAAc;QACdE,eAAe;QACf,IAAI;YACF,MAAM,EAAEG,QAAQ,EAAEC,OAAO,EAAE,GAAG,MAAMV,OAAO,gBAAgB;gBACzDW,OAAOH,OAAOG,KAAK;eACfH,OAAOI,IAAI,GAAG;gBAAEA,MAAMJ,OAAOI,IAAI;YAAC,IAAI,CAAC,GACvCJ,OAAOK,OAAO,GAAG;gBAAEA,SAASL,OAAOK,OAAO;YAAC,IAAI,CAAC,GAChDL,OAAOM,QAAQ,GAAG;gBAAEA,UAAUN,OAAOM,QAAQ;YAAC,IAAI,CAAC,GACnDN,OAAOO,KAAK,GAAG;gBAAEA,OAAOP,OAAOO,KAAK;YAAC,IAAI,CAAC,GAC1CP,OAAOQ,OAAO,GAAG;gBAAEA,SAASR,OAAOQ,OAAO;YAAC,IAAI,CAAC,GAChDR,OAAOS,UAAU,GAAG;gBAAEA,YAAYT,OAAOS,UAAU;YAAC,IAAI,CAAC,GACzDT,OAAOU,OAAO,GAAG;gBAAEA,SAASV,OAAOU,OAAO;YAAC,IAAI,CAAC,GAChDV,OAAOW,IAAI,CAAC7D,MAAM,GAAG;gBAAE6D,MAAMX,OAAOW,IAAI;YAAC,IAAI,CAAC,GAC9CX,OAAOY,QAAQ,GAAG;gBAAEA,UAAUZ,OAAOY,QAAQ;YAAC,IAAI,CAAC,GACnDZ,OAAOa,KAAK,GAAG;gBAAEA,OAAOb,OAAOa,KAAK;YAAC,IAAI,CAAC;gBAC9CjE,QAAQoD,OAAOpD,MAAM;;YAEvB,IAAI,CAACqD,SAASa,EAAE,EAAE;oBAEMZ;gBADtB,4DAA4D;gBAC5DJ,eAAeiB,QAAOb,iBAAAA,OAAO,CAAC,QAAQ,YAAhBA,iBAAoB;gBAC1C;YACF;YACA,gEAAgE;YAChE,uBAAuB;YACvB3E,gBACE2E,OAAO,CAAC,UAAU,GACd,eACA,mEACJ;gBAAEc,SAAS;gBAAWC,SAAS;YAAM;YAEvCvB,cAAc;QAChB,EAAE,OAAOwB,OAAO;YACdC,QAAQD,KAAK,CAACA;YACdpB,eAAe;QACjB,SAAU;YACRF,cAAc;QAChB;IACF,GACA;QAACJ;QAAQjE;KAAgB;IAG3B,MAAM6F,YAAYrI,YAChB,OAAOwF,MAAe8C,QAAiCC;QACrD,IAAI;YACF,MAAMzI,UAAUL,IAAI6C,WAAW,SAASkD,KAAKrD,MAAM,EAAE,SAASqD,KAAK7B,MAAM,GAAG,aACvE2E;gBACHE,WAAW3I;;YAEb2C,gBAAgB+F,MAAM;gBAAEN,SAAS;gBAAWC,SAAS;YAAM;QAC7D,EAAE,OAAOC,OAAO;YACd3F,gBACE2F,iBAAiBM,QAAQN,MAAMO,OAAO,GAAG,kCACzC;gBAAET,SAAS;YAAQ;QAEvB;IACF,GACA;QAAC3F;QAAWE;KAAgB;IAG9B,MAAMmG,UAAUxI,QACd,IAAM;YACJ;gBACEwE,OAAO;gBACPiE,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACVC,aAAa,CAACC,QAAQtF,MAAiBsE,OAAOtE,GAAG,CAAC,OAAO,IAAIA,GAAG,CAAC,QAAQ,IAAI;gBAC7EuF,YAAY,CAAC,EAAEvF,GAAG,EAAoB;wBAOtBA;yCANd,MAACtE;wBAAM8J,SAAS;wBAAGC,IAAI;4BAAEL,UAAU;4BAAGM,gBAAgB;4BAAUC,QAAQ;wBAAO;;0CAC7E,KAAC/J;gCAAW2I,SAAQ;gCAAQqB,MAAM;0CAC/BtB,OAAOtE,GAAG,CAAC,OAAO,IAAIA,GAAG,CAAC,QAAQ,IAAIA,IAAIE,GAAG;;4BAE/CF,GAAG,CAAC,OAAO,iBACV,KAACpE;gCAAW2I,SAAQ;gCAAUsB,OAAM;gCAAiBD,MAAM;0CACxDtB,QAAOtE,aAAAA,GAAG,CAAC,QAAQ,YAAZA,aAAgB;iCAExB;;;;YAGV;YACA,mEAAmE;YACnE,iCAAiC;YACjC;gBACEiB,OAAO;gBACPiE,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACVC,aAAa,CAACC,QAAQtF;wBAAwBA;2BAAPsE,QAAOtE,eAAAA,IAAI4D,OAAO,YAAX5D,eAAe;;YAC/D;YACA;gBACEiB,OAAO;gBACPiE,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACVC,aAAa,CAACC,QAAQtF;wBAAwBA;2BAAPsE,QAAOtE,gBAAAA,IAAI6D,QAAQ,YAAZ7D,gBAAgB;;YAChE;YACA;gBACEiB,OAAO;gBACPiE,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACVC,aAAa,CAACC,QAAQtF,MAAiB3G,MAAMyM,aAAa,CAAC9F;gBAC3DuF,YAAY,CAAC,EAAEvF,GAAG,EAAoB,iBACpC,KAAC+F;wBACCjE,MAAM9B;wBACNgG,UAAU,CAAC3E;4BACT,IAAIA,SAAS,eAAe;gCAC1BwB,gBAAgB7C;gCAChB;4BACF;4BACA,KAAK2E,UACH3E,KACA;gCACEG,QAAQkB;+BACJhI,MAAMyM,aAAa,CAAC9F,SAAS,gBAC7B;gCAAEiG,mBAAmBnK;4BAAc,IACnC,CAAC,IAEP;wBAEJ;;YAGN;YACA;gBACE;;;;SAIC,GACDmF,OAAO;gBACPiE,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACVC,aAAa,CAACC,QAAQtF;oBACpB,MAAMa,QAAQxH,MAAM6M,cAAc,CAAClG;oBACnC,OAAOa,QAAQxH,MAAM8M,kBAAkB,CAACtF,MAAMV,MAAM,CAAC,GAAG;gBAC1D;gBACAoF,YAAY,CAAC,EAAEvF,GAAG,EAAoB;oBACpC,MAAMa,QAAQxH,MAAM6M,cAAc,CAAClG;oBACnC,OAAOa,sBACL,KAAC/C;wBAAkB+C,OAAOA;uCAE1B,KAACjF;wBAAW2I,SAAQ;wBAAUsB,OAAM;kCACjC;;gBAGP;YACF;YACA;gBACE5E,OAAO;gBACPiE,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACVC,aAAa,CAACC,QAAQtF,MACpBA,IAAImE,QAAQ,GAAGjF,OAAOkH,QAAQ,CAACpG,IAAImE,QAAQ,IAAI;YACnD;YACA,iEAAiE;eAC7D1F,SACA,EAAE,GACF;gBACE;oBACEwC,OAAO;oBACPiE,YAAY;oBACZC,MAAM;oBACNC,UAAU;oBACVC,aAAa,CAACC,QAAiBtF;;uCAC7Bf,yBAAAA,MAAOwD,QAAQ,CAACzC,IAAIvB,MAAM,oBAAKuB,IAAIvB,MAAM;;gBAC7C;aACD;YACL;gBACEwC,OAAO;gBACPiE,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACVC,aAAa,CAACC,QAAQtF,MACpBzC,YAAYyC,KAAKR,GAAG,CAAClC,iBAAiB+I,IAAI,CAAC;YAC/C;YACA;gBACEpF,OAAO;gBACPiE,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACVC,aAAa,CAACC,QAAQtF;wBAAkBA;2BAAD,EAACA,YAAAA,IAAIkE,IAAI,YAARlE,YAAY,EAAE,EAAEqG,IAAI,CAAC;;YAC/D;YACA;gBACEpF,OAAO;gBACPiE,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACVC,aAAa,CAACC,QAAQtF;wBACNA;2BAAdxC,eAAcwC,oBAAAA,GAAG,CAAC,eAAe,YAAnBA,oBAAuBA,GAAG,CAAC,YAAY;;YACzD;YACA;gBACEiB,OAAO;gBACPiE,YAAY;gBACZoB,OAAO;gBACPC,UAAU;gBACVC,YAAY;gBACZC,mBAAmB;gBACnBlB,YAAY,CAAC,EAAEvF,GAAG,EAAoB;wBAsBhBA;oBArBpB;;;;;WAKC,GACD,MAAM0G,YAAYC,QAAQ3G,IAAI4G,kBAAkB;oBAChD,MAAMC,iBAAiBxN,MAAMyN,wBAAwB,CAAC9G,SAAS;oBAC/D,MAAM+G,iBAAiBL,YACnB,4BACA,CAACrN,MAAM2N,aAAa,CAAChH,OACnB,8BACA6G,iBACEhJ,iCACA;oBACR,qBACE,KAAC9C;wBACCkM,SAAS,CAACC,QAAUA,MAAMC,eAAe;wBACzC1B,IAAI;4BAAE2B,SAAS;4BAAQC,YAAY;4BAAU1B,QAAQ;wBAAO;kCAE5D,cAAA,KAACnL;4BACC8M,OAAOhD,QAAOtE,aAAAA,GAAG,CAAC,QAAQ,YAAZA,aAAgBA,IAAIE,GAAG;4BACrCqH,OAAO;gCACL;oCACEC,KAAK;oCACLF,OAAO;oCACPG,oBAAM,KAAC7N;wCAAQ8N,MAAMpO,qBAAqBoO,IAAI;wCAAEC,MAAM;;oCACtDC,MAAMzI,OAAO2C,IAAI,CAAC9B,IAAIC,MAAM,EAAExB,SAAS,OAAOuB,IAAIvB,MAAM;gCAC1D;gCACA;oCACE+I,KAAK;oCACLF,OAAO;oCACPG,oBAAM,KAAC7N;wCAAQ8N,MAAMlO,yBAAyBkO,IAAI;wCAAEC,MAAM;;oCAC1DV,SAAS,IAAMnE,cAAc9C;oCAC7B6H,UAAUd,mBAAmB;oCAC7Be,cAAc,EAAEf,yBAAAA,iBAAkBgB;gCACpC;gCACA;oCACEP,KAAK;oCACLF,OAAO;oCACPG,oBAAM,KAAC7N;wCAAQ8N,MAAMjO,qBAAqBiO,IAAI;wCAAEC,MAAM;;oCACtDV,SAAS,IAAMrE,aAAa5C;gCAC9B;gCACA;oCACEwH,KAAK;oCACLF,OAAO;oCACPG,oBAAM,KAAC7N;wCAAQ8N,MAAMnO,wBAAwBmO,IAAI;wCAAEC,MAAM;;oCACzDV,SAAS,IAAMpE,gBAAgB7C;oCAC/B6H,UAAU,CAACxO,MAAM2N,aAAa,CAAChH,QAAQ2G,QAAQ3G,IAAI4G,kBAAkB;oCACrEkB,gBAAgB9H,IAAI4G,kBAAkB,GAClC,4BACA;gCACN;6BACD;;;gBAIT;YACF;SACD,EACD;QAAC1H;QAAQC;QAAQwF;QAAWlG;QAAQQ;KAAM;IAE5C,yDAAyD,GACzD,MAAM+I,OAAO7N,eAAesG,OAAOwE;IAEnC,qBACE;;0BACE,KAACtL;gBACCsO,QAAQ;gBACRC,MAAM7O,MAAM8O,cAAc,CAAC,YAAY;oBAAEC,QAAQ;gBAAyB;gBAC1EC,uBACE,MAAC3M;oBAAM4M,WAAU;oBAAM9C,SAAS;oBAAGC,IAAI;wBAAE4B,YAAY;oBAAS;;sCAE5D,KAAC9M;4BAAgBgO,YAAY9H;4BAAO+H,UAAS;;sCAC7C,MAACnN;4BAAYsM,MAAK;4BAAQlC,IAAI;gCAAEL,UAAU;4BAAI;;8CAC5C,KAAC7J;oCAAWmE,IAAI+B;8CAAgB;;8CAChC,KAAChG;oCACCgN,SAAShH;oCACT6F,OAAM;oCACN1G,OAAOD;oCACPqF,UAAU,CAACkB,QAAU9F,UAAU8F,MAAMwB,MAAM,CAAC9H,KAAK;8CAEhD5D,aAAawC,GAAG,CAAC,CAACmJ,uBACjB,KAACnN;4CAAsBoF,OAAO+H;sDAC3B9L,kBAAkB,CAAC8L,OAAO;2CADdA;;;;sCAOrB,MAACtN;4BAAYsM,MAAK;4BAAQlC,IAAI;gCAAEL,UAAU;4BAAI;;8CAC5C,KAAC7J;oCAAWmE,IAAIgC;8CAAqB;;8CACrC,KAACjG;oCACCgN,SAAS/G;oCACT4F,OAAM;oCACN1G,OAAOO;oCACP6E,UAAU,CAACkB,QAAU1F,eAAe0F,MAAMwB,MAAM,CAAC9H,KAAK;8CAErD7D,mBAAmByC,GAAG,CAAC,CAACmJ,uBACvB,KAACnN;4CAAsBoF,OAAO+H;sDAC3B7L,wBAAwB,CAAC6L,OAAO;2CADpBA;;;;sCAMrB,KAAChN;4BACCgM,MAAK;4BACL/G,OAAOe;4BACPqE,UAAU,CAACkB,QAAUtF,UAAUsF,MAAMwB,MAAM,CAAC9H,KAAK;4BACjDgI,aAAY;4BACZC,WAAW;gCACTC,OAAO;oCACLC,8BACE,KAACzN;wCAAe0N,UAAS;kDACvB,cAAA,KAACpP;4CAAQ8N,MAAMhO,WAAWgO,IAAI;4CAAEC,MAAM;;;gCAG5C;gCACAsB,WAAW;oCAAE,cAAc;oCAAgBC,MAAM;gCAAS;4BAC5D;4BACAzD,IAAI;gCAAEL,UAAU;4BAAI;;sCAEtB,KAAC3H;4BAAiBgB,QAAQA;;sCAC1B,KAACzD;4BAAO2M,MAAK;4BAAQV,SAASvE;4BAAcmF,UAAU,CAAChG,KAAKxB,MAAM;sCAC/D;;sCAEH,KAACrF;4BAAO2M,MAAK;4BAAQpD,SAAQ;4BAAY0C,SAAS,IAAMhE,cAAc;sCACnE;;;;gBAIPkG,cAAc;gBACdC,cAAc;0BAEd,cAAA,MAAC1N;oBAAM8J,SAAS;;wBAEb/G,uBAAS,KAACT;4BAAiBS,QAAQA;2CAAa,KAACN;wBACjDgC,WAAW,aAAaI,OAAOF,MAAM,KAAK,kBACzC,KAAC3F;4BACC4M,OAAO;4BACP+B,aAAa;6BAEblJ,WAAW,aAAa0B,KAAKxB,MAAM,KAAK,kBAC1C,KAACzE;4BAAW2I,SAAQ;4BAAQsB,OAAM;sCAC/B,CAAC,GAAG,EAAEhJ,kBAAkB,CAAC8D,OAAO,CAAC2I,WAAW,GAAG,MAAM,CAAC,GACpD3H,CAAAA,OAAO4H,IAAI,KAAK,CAAC,QAAQ,EAAE5H,OAAO4H,IAAI,GAAG,CAAC,CAAC,GAAG,EAAC,IAChD,CAAC,WAAW,EAAEhJ,OAAOF,MAAM,CAACmJ,cAAc,GAAG,oBAAoB,CAAC;2CAGtE;;8CACE,KAACtL;oCACC2D,MAAMA;oCACN4H,UAAUrH;oCACVsH,kBAAkBrH;oCAClBnD,QAAQA;oCACRyK,KAAKrH;oCACLvD,OAAOA;oCACPN,QAAQA;;8CAEV,KAACpE;oCAAuBuG,OAAOoH,KAAK4B,WAAW;8CAC7C,cAAA,KAAC9P;wCACC+H,MAAMM;wCACN8C,SAAS+C,KAAK/C,OAAO;wCACrB4E,OAAOzP;wCACP0P,YAAY;4CAAEL,UAAUrH;4CAAa4D,UAAU3D;wCAAe;wCAC9D0H,SAAS5J,WAAW;wCACpB6J,QAAQ,CAACC,KAAKjK,MACZnB,OAAOqL,IAAI,CAAC/K,OAAO2C,IAAI,CAAC9B,IAAIC,MAAM,EAAExB,SAAS,OAAOuB,IAAIvB,MAAM;wCAEhE,0DAA0D;wCAC1D0L,uBAAuBnC,KAAKmC,qBAAqB;wCACjDC,+BAA+BpC,KAAKoC,6BAA6B;wCACjEC,WAAWrC,KAAKqC,SAAS;wCACzBC,mBAAmBtC,KAAKsC,iBAAiB;wCACzC,yDAAyD;wCACzD,gCAAgC;wCAChCC,aAAa;wCACb,8DAA8D;wCAC9DC,UAAU;;;8CAGd,KAAC3Q;oCACCkI,MAAMA;oCACNE,UAAUA;oCACVwI,UAAUtI,SAAS9B,MAAM;oCACzBqK,OAAO7I,KAAKxB,MAAM;oCAClBsK,cAAc3I;oCACd4I,kBAAkB1I;;;;wBAIvB5B,0BACC,KAACxF;4BAAM+P,UAAS;sCACb,CAAC,YAAY,EAAEzM,aAAaoL,cAAc,GAAG,oBAAoB,CAAC,GACjE,8DACA,GAAGpL,aAAaoL,cAAc,GAAG,6BAA6B,CAAC,GAC/D;6BAEF;;;;0BAGR,KAAC9L;gBACCoN,MAAM9H;gBACN+H,SAAS,IAAM9H,cAAc;gBAC7BxE,MAAM,EAAEA,iBAAAA,SAAU;gBAClBC,KAAKA;gBACLsM,MAAM9H;gBACNuB,OAAOrB;gBACPlE,QAAQA;gBACR+L,UAAU,CAAC1H,SAAW,KAAKD,aAAaC;;0BAE1C,KAAC2H;gBACCpJ,MAAMa;gBACNzD,QAAQA;gBACR6L,SAAS,IAAMnI,aAAa;gBAC5BuI,UAAU,CAACC;oBACT,IAAI,CAACzI,WAAW;oBAChB,KAAKgC,UACHhC,WACA;wBAAEwB,UAAUiH,OAAOtP;oBAAc,GACjCsP,MAAM,mBAAmB;oBAE3BxI,aAAa;gBACf;;0BAEF,KAAC3E;gBACC6M,MAAMnE,QAAQpI;gBACdwM,SAAS,IAAMlI,gBAAgB;gBAC/BpE,MAAM,GAAEF,gBAAAA,gCAAAA,aAAcE,MAAM,oBAAIA,kBAAxBF,OAAkC;gBAC1C0B,MAAM,WAAE1B,gCAAAA,aAAc0B,MAAM,oBAAI;gBAChCoL,WAAW/G,OAAO/F,CAAAA,gCAAAA,YAAc,CAAC,OAAO,MAAIA,gCAAAA,YAAc,CAAC,QAAQ,KAAI;;0BAKzE,KAAClB;gBACCyN,MAAMnE,QAAQnI;gBACduM,SAAS,IAAMjI,cAAc;gBAC7BrE,MAAM,GAAED,iBAAAA,8BAAAA,WAAYC,MAAM,oBAAIA,kBAAtBD,QAAgC;gBACxCO,OAAOA;gBACPL,KAAKA;gBACLuB,MAAM,WAAEzB,8BAAAA,WAAYyB,MAAM,oBAAI;gBAC9B6B,IAAI,EAAEtD,qBAAAA,aAAc,CAAC;gBACrBG,QAAQ,EAAEA,mBAAAA,WAAY;gBACtBO,QAAQA;;;;AAIhB;AACAb,gBAAgBiN,WAAW,GAAG;AAE9B;;;;;;CAMC,GACD,SAASvF,aAAazH,KAGrB;IACC,MAAM,EAAEwD,IAAI,EAAEkE,QAAQ,EAAE,GAAG1H;IAC3B,IAAIwD,KAAK8E,kBAAkB,EAAE;QAC3B,qBACE,KAAC7L;YAAI0K,IAAI;gBAAE2B,SAAS;gBAAQC,YAAY;gBAAU1B,QAAQ;YAAO;sBAC/D,cAAA,KAAC5H;gBAAe+D,MAAMA;;;IAG5B;IACA,MAAM3B,SAAS9G,MAAMyM,aAAa,CAAChE;IACnC,qBACE,KAAC/G;QACCkM,SAAS,CAACC,QAAUA,MAAMC,eAAe;QACzC1B,IAAI;YAAE2B,SAAS;YAAQC,YAAY;YAAU1B,QAAQ;YAAQW,OAAO;QAAO;kBAE3E,cAAA,MAAC7K;YACCkM,MAAK;YACLpD,SAAQ;YACRgH,gBAAgB;YAChB3K,OAAOT;YACP6F,UAAU,CAACkB,QAAUlB,SAASkB,MAAMwB,MAAM,CAAC9H,KAAK;YAChD4K,aAAa,kBAAM,KAACzN;oBAAe+D,MAAMA;;YACzC2D,IAAI;gBAAEa,OAAO;YAAO;;8BAEpB,KAAC9K;oBAASoF,OAAM;8BAAOvH,MAAMoS,sBAAsB,CAACC,GAAG;;8BACvD,KAAClQ;oBAASoF,OAAM;8BAAWvH,MAAMoS,sBAAsB,CAACE,OAAO;;8BAC/D,KAACnQ;oBAASoF,OAAM;8BAAe,GAAGvH,MAAMoS,sBAAsB,CAACG,WAAW,CAAC,CAAC,CAAC;;;;;AAIrF;AACA7F,aAAauF,WAAW,GAAG;AAE3B,kCAAkC,GAClC,SAASJ,kBAAkB5M,KAK1B;;IACC,MAAM,EAAEwD,IAAI,EAAE5C,MAAM,EAAE6L,OAAO,EAAEI,QAAQ,EAAE,GAAG7M;IAC5C,MAAM,CAAC8M,KAAKS,OAAO,GAAGnP,SAAwB;IAC9C,MAAMoP,UAAUV,cAAAA,MAAO9G,eAAOxC,wBAAAA,KAAMqC,QAAQ,mBAAI;IAChD,qBACE,MAAClJ;QACC6P,MAAMnE,QAAQ7E;QACdiJ,SAASA;QACTgB,UAAS;QACTC,SAAS;QACTnD,WAAW;YAAEoD,YAAY;gBAAEC,UAAU,IAAML,OAAO;YAAM;QAAE;;0BAE1D,KAACzQ;0BAAa,CAAC,OAAO,EAAEkJ,OAAOxC,CAAAA,wBAAAA,IAAM,CAAC,OAAO,MAAIA,wBAAAA,IAAM,CAAC,QAAQ,KAAI,SAAS;;0BAC7E,KAAC3G;0BACC,cAAA,KAACJ;oBAAI0K,IAAI;wBAAE0G,IAAI;oBAAE;8BACf,cAAA,KAACvO;wBAAgBgD,OAAOkL;wBAAS9F,UAAU6F;wBAAQ3M,QAAQA;wBAAQyI,MAAK;;;;0BAG5E,MAACzM;;kCACC,KAACF;wBAAOiM,SAAS8D;kCAAU;;kCAC3B,KAAC/P;wBAAOuJ,SAAQ;wBAAY0C,SAAS,IAAMkE,SAASW;kCACjD;;;;;;AAKX;AACAZ,kBAAkBI,WAAW,GAAG;AAEhC,eAAejN,gBAAe"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/leads-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport type {\n ConsolePluginPageProps,\n CrmLeadFields,\n CrmLeadStatus,\n} from '@aglyn/aglyn'\nimport {\n mdiAccountArrowRight,\n mdiAccountCancelOutline,\n mdiAccountConvertOutline,\n mdiAccountTieOutline,\n mdiMagnify,\n} from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n type OrgMemberOptions,\n useOrgMemberOptions,\n} from '../hooks/use-org-member-options'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport { useOrgLeads } from '../hooks/use-org-leads'\nimport CrmViewsControl from './crm-views-control'\nimport RowActionsMenu from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useHostCampaigns,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Box,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n FormControl,\n InputAdornment,\n InputLabel,\n MenuItem,\n Select,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport {\n collection,\n deleteField,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n updateDoc,\n} from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useId, useMemo, useState } from 'react'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n LEAD_FILTER_LABELS,\n LEAD_EMAIL_FILTER_LABELS,\n LEAD_EMAIL_FILTERS,\n LEAD_FILTERS,\n type LeadEmailFilter,\n type LeadFilter,\n leadMatchesCampaignFilter,\n leadMatchesEmailFilter,\n leadMatchesFilter,\n leadMatchesSearch,\n} from '../model/lead-filters'\nimport { type LeadCsvOptions, leadsCsv } from '../model/leads-csv'\nimport { LeadConvertDialog } from './lead-convert-dialog'\nimport {\n leadSourceLabel,\n leadSources,\n leadTimeLabel,\n} from './lead-history-card'\nimport { LeadImportButton } from './lead-import-drawer'\nimport NewLeadDrawer, { type NewLeadValues } from './new-lead-drawer'\nimport { useCrmApi } from './use-crm-api'\nimport { LeadOwnerSelect } from './lead-owner-select'\nimport { CONVERT_PENDING_ERASURE_REASON } from './lead-properties-card'\nimport { CrmEmailStateChip } from './crm-email-state-chip'\nimport { LeadStatusChip } from './lead-status-chip'\nimport LeadSurfacesNote from './lead-surfaces-note'\nimport { LeadUnqualifyDialog } from './lead-unqualify-dialog'\nimport LeadsBulkBar from './leads-bulk-bar'\nimport OrgLeadSurfacesNote from './org-lead-surfaces-note'\n\n/**\n * How many leads the section reads: the newest by last seen, plus one probe\n * row so \"there are more\" is a fact rather than a guess at the boundary.\n *\n * A CEILING and a client-side filter rather than a paged status query, and\n * the reason is the lead documents themselves. Every lead the capture door\n * writes carries NO `status` — the field exists only once somebody in the\n * CRM has touched the lead — and Firestore cannot select documents by a\n * field's absence: `where('status','in',[…])`, `!=` and `not-in` all skip a\n * document without the field. A server-side \"open leads\" query would\n * therefore hide every lead nobody has worked yet, which is the entire\n * population the section exists to show on the day it ships. So the query\n * is the one order every lead can satisfy (`lastSeenAtMs`, stamped on every\n * capture), and the status filter narrows the loaded window — said out loud\n * beneath the table whenever the window is not the whole collection.\n *\n * The window is then PAGED in memory under the shared footer, the way the\n * workspace pickers page a slice of a window they cannot re-key: the rows\n * are already in the snapshot, so turning a page costs nothing, and the\n * footer's count is exact because it counts the filtered window rather than\n * a collection nobody has measured.\n */\nconst LEADS_WINDOW = 200\n\n/**\n * One row of the list. `$id` keys the grid — the document id under a site,\n * `{hostId}/{leadId}` at the organization level, where a lead's id is a\n * person key the same on every site that met the person — and `leadId`\n * and `hostId` say which document, under which site, a write or a link\n * names.\n */\ntype LeadRow = Record<string, unknown> &\n CrmLeadFields & { $id: string; leadId: string; hostId: string }\n\n/**\n * `/crm/leads` — the people a site has met but not yet qualified (AGL-2608).\n *\n * A section of its own, the way Salesforce keeps Leads apart from Contacts:\n * a lead is a capture — a form, a booking, a sign-up — that somebody has\n * still to work, and it converts into a contact, a company and a deal when\n * it is real. Reads `hosts/{hostId}/leads`, host-scoped by path, so there is\n * no `visibleTo` filter; the Firestore rules admit any member of the site to\n * read it and an admin, editor or author to update it, which is what makes\n * the inline status and owner changes client-direct writes.\n *\n * At the ORGANIZATION level (AGL-2630) there is no one site to read: the\n * section opens the same query under every site the org has (`useOrgLeads`)\n * and lists the merged window with a Site column, every row naming the site\n * its writes and its link go to. The per-site notes — which of a site's\n * forms file a lead — belong to a site's own hub and are not drawn here.\n */\nexport function CrmLeadsSection(props: ConsolePluginPageProps) {\n const { hostId, org, basePath } = props\n const firestore = useFirestore()\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, createHostId } = useCrmScope({ hostId, org })\n const mount = useCrmOrgMount()\n const roster = useOrgMemberOptions(orgId)\n const routes = crmRoutes(basePath ?? '')\n\n // Under a site: the site's own window, rows keyed by document id.\n const site = useFirestoreCollection<\n Record<string, unknown> & CrmLeadFields & { $id: string }\n >(\n () =>\n hostId\n ? query(\n collection(firestore, 'hosts', hostId, 'leads'),\n orderBy('lastSeenAtMs', 'desc'),\n limit(LEADS_WINDOW + 1),\n )\n : null,\n [firestore, hostId],\n { idField: '$id' },\n )\n // At the organization level: every site's window, merged.\n const orgHostIds = useMemo(\n () => (hostId ? [] : (mount?.hosts ?? []).map((host) => host.id)),\n [hostId, mount?.hosts],\n )\n const orgLeads = useOrgLeads({\n hostIds: orgHostIds,\n windowSize: LEADS_WINDOW,\n })\n const leadDocs = useMemo<LeadRow[]>(\n () =>\n hostId\n ? site.data.map((row) => ({ ...row, leadId: row.$id, hostId }))\n : orgLeads.data,\n [hostId, site.data, orgLeads.data],\n )\n const status = hostId\n ? site.status\n : mount?.hostsReady && !orgHostIds.length\n ? 'success'\n : orgLeads.status\n const truncated = hostId ? leadDocs.length > LEADS_WINDOW : orgLeads.truncated\n const window = useMemo(() => leadDocs.slice(0, LEADS_WINDOW), [leadDocs])\n\n /*\n * The `Show` filter is the saved VIEW'S (AGL-2617): a saved view of leads\n * holds the status beside the columns and the sort, and the select below\n * writes into it. Unset reads as `open`, which is what the section opened\n * on before views existed and the one reading a query cannot express.\n */\n const views = useCrmSavedView({\n section: 'leads',\n hostId,\n org: props.org,\n basePath: basePath ?? '',\n })\n const filter: LeadFilter = useMemo(() => {\n const value = views.state.filters.find(\n (clause) => clause.field === 'status',\n )?.value\n return (LEAD_FILTERS as readonly string[]).includes(value ?? '')\n ? (value as LeadFilter)\n : 'open'\n }, [views.state.filters])\n /*\n * The `Email` filter (AGL-3245) is the view's too, as an `emailState`\n * clause beside the status one. Each setter keeps the other's clause:\n * narrowing to bounced leads does not reopen the unqualified ones.\n */\n const emailFilter: LeadEmailFilter = useMemo(() => {\n const value = views.state.filters.find(\n (clause) => clause.field === 'emailState',\n )?.value\n return (LEAD_EMAIL_FILTERS as readonly string[]).includes(value ?? '')\n ? (value as LeadEmailFilter)\n : 'any'\n }, [views.state.filters])\n const setFilter = useCallback(\n (next: LeadFilter) =>\n views.setFilters([\n ...views.state.filters.filter((clause) => clause.field !== 'status'),\n ...(next === 'open'\n ? []\n : [{ field: 'status', op: 'equals', value: next }]),\n ]),\n [views.setFilters, views.state.filters],\n )\n const setEmailFilter = useCallback(\n (next: LeadEmailFilter) =>\n views.setFilters([\n ...views.state.filters.filter(\n (clause) => clause.field !== 'emailState',\n ),\n ...(next === 'any'\n ? []\n : [{ field: 'emailState', op: 'equals', value: next }]),\n ]),\n [views.setFilters, views.state.filters],\n )\n /*\n * The `Campaign` filter (AGL-3254) is the view's too, as a `campaignIds`\n * clause: the id of one of the site's campaign containers, resolved to\n * its name from the containers themselves — ids only in storage, so a\n * renamed campaign keeps its leads. Under a site alone: a campaign\n * belongs to one site, and the organization-level list spans them all.\n */\n const campaignFilter = useMemo(\n () =>\n views.state.filters.find((clause) => clause.field === 'campaignIds')\n ?.value ?? '',\n [views.state.filters],\n )\n const setCampaignFilter = useCallback(\n (next: string) =>\n views.setFilters([\n ...views.state.filters.filter(\n (clause) => clause.field !== 'campaignIds',\n ),\n ...(next\n ? [{ field: 'campaignIds', op: 'contains', value: next }]\n : []),\n ]),\n [views.setFilters, views.state.filters],\n )\n const campaigns = useHostCampaigns(hostId, { enabled: Boolean(hostId) })\n const campaignName = useCallback(\n (id: string) =>\n campaigns.options.find((option) => option.value === id)?.label ?? id,\n [campaigns.options],\n )\n // The label's id, so the filter's combobox is named \"Show\" rather than\n // after the option it shows — see `LeadOwnerSelect`.\n const filterLabelId = useId()\n const emailFilterLabelId = useId()\n const campaignFilterLabelId = useId()\n /*\n * The search box is the SECTION'S, not the grid's (AGL-3246). The grid's\n * quick filter runs over the rows the grid holds, and the grid holds one\n * PAGE of the window — so a lead on page three answered \"no match\" while\n * the footer below went on counting the unfiltered window. The term\n * narrows the whole loaded window here, beside the status filter and\n * before the footer's count and the page slice, over the fields a person\n * types to find a lead: name, email, company, title and tags.\n */\n const [search, setSearch] = useState('')\n const rows = useMemo(\n () =>\n window.filter(\n (lead) =>\n leadMatchesFilter(lead, filter) &&\n leadMatchesEmailFilter(lead, emailFilter) &&\n leadMatchesCampaignFilter(lead, campaignFilter) &&\n leadMatchesSearch(lead, search),\n ),\n [window, filter, emailFilter, campaignFilter, search],\n )\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n // A new filter or search term starts on page one: page three of the open\n // leads is not a page of the unqualified ones, and an out-of-range page\n // renders empty.\n useEffect(() => {\n setPage(0)\n }, [filter, emailFilter, campaignFilter, search])\n const pageRows = useMemo(\n () => rows.slice(page * pageSize, (page + 1) * pageSize),\n [rows, page, pageSize],\n )\n\n /*\n * The ticked rows, for the bulk bar (AGL-2662). Cleared when the filter or\n * the search term changes: a selection made on the open leads is not a\n * selection of the unqualified ones, and the bar's count would be over\n * rows no longer listed.\n */\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n useEffect(\n () => setSelectedIds([]),\n [filter, emailFilter, campaignFilter, search],\n )\n // How the file names the owner and, at the org level, the site.\n const csvOptions: LeadCsvOptions = useMemo(\n () => ({\n ownerEmail: roster.emailFor,\n ...(hostId ? {} : { siteName: (id: string) => mount?.siteName(id) }),\n }),\n [roster.emailFor, hostId, mount],\n )\n // The listed window — every row the filter and the search admit, not just\n // the page.\n const handleExport = useCallback(() => {\n downloadTextFile('leads.csv', 'text/csv', leadsCsv(rows, csvOptions))\n }, [rows, csvOptions])\n\n const [assigning, setAssigning] = useState<LeadRow | null>(null)\n const [unqualifying, setUnqualifying] = useState<LeadRow | null>(null)\n // The row whose conversion dialog is open (AGL-2641) — the same dialog\n // the lead's page opens, fed the row so the list is one click shorter.\n const [converting, setConverting] = useState<LeadRow | null>(null)\n\n /*==========================================\n * NEW LEAD (AGL-3231) — Salesforce's New Lead, in a drawer over the\n * list. The route files the lead through the one lead door under the\n * mounted site, or at the organization level under the site the drawer's\n * picker named; the drawer holds its submit until one is known. It makes\n * a lead and nothing else — the conversion is what makes the contact.\n *=========================================*/\n const crmApi = useCrmApi(createHostId)\n const [createOpen, setCreateOpen] = useState(false)\n const [createBusy, setCreateBusy] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const handleCreate = useCallback(\n async (values: NewLeadValues) => {\n setCreateBusy(true)\n setCreateError(null)\n try {\n const { response, payload } = await crmApi('leads-create', {\n email: values.email,\n ...(values.name ? { name: values.name } : {}),\n ...(values.company ? { company: values.company } : {}),\n ...(values.jobTitle ? { jobTitle: values.jobTitle } : {}),\n ...(values.phone ? { phone: values.phone } : {}),\n ...(values.website ? { website: values.website } : {}),\n ...(values.leadSource ? { leadSource: values.leadSource } : {}),\n ...(values.address ? { address: values.address } : {}),\n ...(values.tags.length ? { tags: values.tags } : {}),\n ...(values.campaignIds.length\n ? { campaignIds: values.campaignIds }\n : {}),\n ...(values.ownerUid ? { ownerUid: values.ownerUid } : {}),\n ...(values.notes ? { notes: values.notes } : {}),\n status: values.status,\n })\n if (!response.ok) {\n // The route's own sentence, shown above the form unchanged.\n setCreateError(\n String(payload['error'] ?? 'The lead could not be added.'),\n )\n return\n }\n // The activity entry is the route's: it verified the caller and\n // performed the write.\n enqueueSnackbar(\n payload['created']\n ? 'Lead added'\n : 'This site already held a lead for that address — it was updated',\n { variant: 'success', persist: false },\n )\n setCreateOpen(false)\n } catch (error) {\n console.error(error)\n setCreateError('The lead could not be added.')\n } finally {\n setCreateBusy(false)\n }\n },\n [crmApi, enqueueSnackbar],\n )\n\n const writeLead = useCallback(\n async (lead: LeadRow, fields: Record<string, unknown>, done: string) => {\n try {\n await updateDoc(\n doc(firestore, 'hosts', lead.hostId, 'leads', lead.leadId),\n {\n ...fields,\n updatedAt: serverTimestamp(),\n },\n )\n enqueueSnackbar(done, { variant: 'success', persist: false })\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error\n ? error.message\n : 'The lead could not be updated.',\n { variant: 'error' },\n )\n }\n },\n [firestore, enqueueSnackbar],\n )\n\n const columns = useMemo<GridColDef[]>(\n () => [\n {\n field: 'name',\n headerName: 'Lead',\n flex: 1.4,\n minWidth: 160,\n valueGetter: (_value, row: LeadRow) =>\n String(row['name'] || row['email'] || ''),\n renderCell: ({ row }: { row: LeadRow }) => (\n <Stack\n spacing={0}\n sx={{ minWidth: 0, justifyContent: 'center', height: '100%' }}\n >\n <Typography variant=\"body2\" noWrap>\n {String(row['name'] || row['email'] || row.$id)}\n </Typography>\n {row['name'] ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {String(row['email'] ?? '')}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n // The lead's own profile (AGL-3231): the two facts a work queue is\n // scanned by, beside the person.\n {\n field: 'company',\n headerName: 'Company',\n flex: 1,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) => String(row.company ?? ''),\n },\n {\n field: 'jobTitle',\n headerName: 'Title',\n flex: 0.9,\n minWidth: 130,\n valueGetter: (_value, row: LeadRow) => String(row.jobTitle ?? ''),\n },\n {\n field: 'status',\n headerName: 'Status',\n flex: 0.9,\n minWidth: 150,\n valueGetter: (_value, row: LeadRow) => Aglyn.crmLeadStatus(row),\n renderCell: ({ row }: { row: LeadRow }) => (\n <InlineStatus\n lead={row}\n onChange={(next) => {\n if (next === 'unqualified') {\n setUnqualifying(row)\n return\n }\n void writeLead(\n row,\n {\n status: next,\n ...(Aglyn.crmLeadStatus(row) === 'unqualified'\n ? { unqualifiedReason: deleteField() }\n : {}),\n },\n 'Status updated',\n )\n }}\n />\n ),\n },\n {\n /*\n * The verdict on the address (AGL-3245): the chip the lead's page\n * carries, so a bounced lead is told apart in the queue it is worked\n * from; its label for the sort and the export.\n */\n field: 'emailState',\n headerName: 'Email',\n flex: 0.9,\n minWidth: 150,\n valueGetter: (_value, row: LeadRow) => {\n const state = Aglyn.readEmailState(row)\n return state ? Aglyn.EMAIL_STATE_LABELS[state.status] : ''\n },\n renderCell: ({ row }: { row: LeadRow }) => {\n const state = Aglyn.readEmailState(row)\n return state ? (\n <CrmEmailStateChip state={state} />\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'—'}\n </Typography>\n )\n },\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 1,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) =>\n row.ownerUid ? roster.labelFor(row.ownerUid) : 'Unassigned',\n },\n // Only at the organization level, where a row can be any site's.\n ...(hostId\n ? []\n : [\n {\n field: 'hostId',\n headerName: 'Site',\n flex: 0.9,\n minWidth: 140,\n valueGetter: (_value: unknown, row: LeadRow) =>\n mount?.siteName(row.hostId) ?? row.hostId,\n } satisfies GridColDef,\n ]),\n {\n field: 'sources',\n headerName: 'Source',\n flex: 1,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) =>\n leadSources(row).map(leadSourceLabel).join(', '),\n },\n {\n field: 'tags',\n headerName: 'Tags',\n flex: 0.9,\n minWidth: 140,\n valueGetter: (_value, row: LeadRow) => (row.tags ?? []).join(', '),\n },\n // The campaigns the lead is filed under (AGL-3254), by name — the\n // ids are the storage. Under a site only, like the filter.\n ...(hostId\n ? [\n {\n field: 'campaignIds',\n headerName: 'Campaign',\n flex: 1,\n minWidth: 150,\n valueGetter: (_value: unknown, row: LeadRow) =>\n Aglyn.readCampaignIds(row).map(campaignName).join(', '),\n } satisfies GridColDef,\n ]\n : []),\n {\n field: 'lastSeenAtMs',\n headerName: 'Last seen',\n flex: 0.9,\n minWidth: 160,\n valueGetter: (_value, row: LeadRow) =>\n leadTimeLabel(row['lastSeenAtMs'] ?? row['createdAt']),\n },\n {\n field: 'actions',\n headerName: '',\n width: 56,\n sortable: false,\n filterable: false,\n disableColumnMenu: true,\n renderCell: ({ row }: { row: LeadRow }) => {\n /*\n * Why Convert is refused, in the order the lead's page refuses it:\n * a converted lead has its contact already, a closed one was\n * judged not real, and a person with an erasure pending must not\n * be captured again — a conversion is a capture (AGL-2623).\n */\n const converted = Boolean(row.convertedContactId)\n const erasurePending = Aglyn.readErasureRequestedAtMs(row) !== null\n const convertRefusal = converted\n ? 'This lead was converted'\n : !Aglyn.isCrmLeadOpen(row)\n ? 'This lead was unqualified'\n : erasurePending\n ? CONVERT_PENDING_ERASURE_REASON\n : null\n return (\n <Box\n onClick={(event) => event.stopPropagation()}\n sx={{ display: 'flex', alignItems: 'center', height: '100%' }}\n >\n <RowActionsMenu\n label={String(row['email'] ?? row.$id)}\n items={[\n {\n key: 'open',\n label: 'Open lead',\n icon: (\n <MdiIcon path={mdiAccountArrowRight.path} size={0.8} />\n ),\n href: routes.lead(row.leadId, hostId ? null : row.hostId),\n },\n {\n key: 'convert',\n label: 'Convert…',\n icon: (\n <MdiIcon\n path={mdiAccountConvertOutline.path}\n size={0.8}\n />\n ),\n onClick: () => setConverting(row),\n disabled: convertRefusal !== null,\n disabledReason: convertRefusal ?? undefined,\n },\n {\n key: 'assign',\n label: 'Assign owner',\n icon: (\n <MdiIcon path={mdiAccountTieOutline.path} size={0.8} />\n ),\n onClick: () => setAssigning(row),\n },\n {\n key: 'unqualify',\n label: 'Unqualify',\n icon: (\n <MdiIcon path={mdiAccountCancelOutline.path} size={0.8} />\n ),\n onClick: () => setUnqualifying(row),\n disabled:\n !Aglyn.isCrmLeadOpen(row) ||\n Boolean(row.convertedContactId),\n disabledReason: row.convertedContactId\n ? 'This lead was converted'\n : 'This lead is already closed',\n },\n ]}\n />\n </Box>\n )\n },\n },\n ],\n [roster, routes, writeLead, hostId, mount, campaignName],\n )\n /* The column and sort models are the view's (AGL-2617). */\n const grid = useCrmViewGrid(views, columns)\n\n return (\n <>\n <CardDisplay\n header={'Leads'}\n help={Aglyn.pluginDocsHelp('contacts', {\n anchor: '#whats-in-the-crm-area',\n })}\n actions={\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n {/* The saved view this list is showing, beside the status it narrows to (AGL-2617). */}\n <CrmViewsControl controller={views} allLabel=\"All leads\" />\n <FormControl size=\"small\" sx={{ minWidth: 160 }}>\n <InputLabel id={filterLabelId}>{'Show'}</InputLabel>\n <Select\n labelId={filterLabelId}\n label=\"Show\"\n value={filter}\n onChange={(event) =>\n setFilter(event.target.value as LeadFilter)\n }\n >\n {LEAD_FILTERS.map((option) => (\n <MenuItem key={option} value={option}>\n {LEAD_FILTER_LABELS[option]}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n {/* The verdict on the address (AGL-3245): the bounced, the blocked, the ones who left. */}\n <FormControl size=\"small\" sx={{ minWidth: 160 }}>\n <InputLabel id={emailFilterLabelId}>{'Email'}</InputLabel>\n <Select\n labelId={emailFilterLabelId}\n label=\"Email\"\n value={emailFilter}\n onChange={(event) =>\n setEmailFilter(event.target.value as LeadEmailFilter)\n }\n >\n {LEAD_EMAIL_FILTERS.map((option) => (\n <MenuItem key={option} value={option}>\n {LEAD_EMAIL_FILTER_LABELS[option]}\n </MenuItem>\n ))}\n </Select>\n </FormControl>\n {/* The campaign the lead is filed under (AGL-3254): the site's containers, by name. */}\n {hostId ? (\n <FormControl size=\"small\" sx={{ minWidth: 180 }}>\n <InputLabel id={campaignFilterLabelId} shrink>\n {'Campaign'}\n </InputLabel>\n <Select\n labelId={campaignFilterLabelId}\n label=\"Campaign\"\n notched\n value={campaignFilter}\n onChange={(event) =>\n setCampaignFilter(String(event.target.value))\n }\n displayEmpty\n >\n <MenuItem value=\"\">{'Any campaign'}</MenuItem>\n {campaigns.options.map((option) => (\n <MenuItem key={option.value} value={option.value}>\n {option.label}\n </MenuItem>\n ))}\n {/* A stored filter naming a campaign the site no longer lists stays selectable, by id, so it can be cleared. */}\n {campaignFilter &&\n !campaigns.options.some(\n (option) => option.value === campaignFilter,\n ) ? (\n <MenuItem value={campaignFilter}>{campaignFilter}</MenuItem>\n ) : null}\n </Select>\n </FormControl>\n ) : null}\n <TextField\n size=\"small\"\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n placeholder=\"Search leads\"\n slotProps={{\n input: {\n startAdornment: (\n <InputAdornment position=\"start\">\n <MdiIcon path={mdiMagnify.path} size={0.8} />\n </InputAdornment>\n ),\n },\n htmlInput: { 'aria-label': 'Search leads', type: 'search' },\n }}\n sx={{ minWidth: 200 }}\n />\n <LeadImportButton hostId={hostId} />\n <Button size=\"small\" onClick={handleExport} disabled={!rows.length}>\n {'Export CSV'}\n </Button>\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => setCreateOpen(true)}\n >\n {'New lead'}\n </Button>\n </Stack>\n }\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n {/* Which surfaces file a lead, by name (AGL-2612) — under a site its own, at the org level every site's (AGL-2638). */}\n {hostId ? (\n <LeadSurfacesNote hostId={hostId} />\n ) : (\n <OrgLeadSurfacesNote />\n )}\n {status === 'success' && window.length === 0 ? (\n <EmptyStateComponent\n label={'No leads yet'}\n description={\n 'Bookings and lead-routed forms on your site become leads on their own — or add one with New lead, or bring a list in with Import CSV.'\n }\n />\n ) : status === 'success' && rows.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`No ${LEAD_FILTER_LABELS[filter].toLowerCase()} leads` +\n (search.trim() ? ` match “${search.trim()}”` : '') +\n ` among the ${window.length.toLocaleString()} most recently seen.`}\n </Typography>\n ) : (\n <>\n <LeadsBulkBar\n rows={rows}\n selected={selectedIds}\n onSelectedChange={setSelectedIds}\n roster={roster}\n csv={csvOptions}\n orgId={orgId}\n hostId={hostId}\n />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={pageRows}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{\n selected: selectedIds,\n onChange: setSelectedIds,\n }}\n loading={status === 'loading'}\n onOpen={(_id, row: LeadRow) =>\n router.push(\n routes.lead(row.leadId, hostId ? null : row.hostId),\n )\n }\n // Columns and sort are the view's, controlled (AGL-2617).\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={\n grid.onColumnVisibilityModelChange\n }\n sortModel={grid.sortModel}\n onSortModelChange={grid.onSortModelChange}\n // The search is the section's, above: the grid's own box\n // would search this page alone.\n quickFilter={false}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n </CrmColumnOrderProvider>\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={pageRows.length}\n count={rows.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </>\n )}\n {truncated ? (\n <Alert severity=\"info\">\n {`Showing the ${LEADS_WINDOW.toLocaleString()} most recently seen ` +\n 'leads. The search box and the status filter narrow these ' +\n `${LEADS_WINDOW.toLocaleString()} only; older leads are still ` +\n 'listed in the Inbox and reached by campaign audiences.'}\n </Alert>\n ) : null}\n </Stack>\n </CardDisplay>\n <NewLeadDrawer\n open={createOpen}\n onClose={() => setCreateOpen(false)}\n hostId={hostId ?? null}\n org={org as Record<string, unknown> | undefined}\n busy={createBusy}\n error={createError}\n roster={roster}\n onSubmit={(values) => void handleCreate(values)}\n />\n <AssignOwnerDialog\n lead={assigning}\n roster={roster}\n onClose={() => setAssigning(null)}\n onAssign={(uid) => {\n if (!assigning) return\n void writeLead(\n assigning,\n { ownerUid: uid || deleteField() },\n uid ? 'Owner assigned' : 'Owner cleared',\n )\n setAssigning(null)\n }}\n />\n <LeadUnqualifyDialog\n open={Boolean(unqualifying)}\n onClose={() => setUnqualifying(null)}\n hostId={unqualifying?.hostId ?? hostId ?? ''}\n leadId={unqualifying?.leadId ?? ''}\n leadLabel={String(\n unqualifying?.['name'] || unqualifying?.['email'] || '',\n )}\n />\n {/* The row's site, not the mounted one: at the organization level a\n lead is its own site's record, and the conversion is that site's\n capture (AGL-2641). Under a site the two are the same. */}\n <LeadConvertDialog\n open={Boolean(converting)}\n onClose={() => setConverting(null)}\n hostId={converting?.hostId ?? hostId ?? ''}\n orgId={orgId}\n org={org as Record<string, unknown> | undefined}\n leadId={converting?.leadId ?? ''}\n lead={converting ?? {}}\n basePath={basePath ?? ''}\n roster={roster}\n />\n </>\n )\n}\nCrmLeadsSection.displayName = 'CrmLeadsSection'\n\n/**\n * The status, editable in place for a lead that is still open.\n *\n * A converted lead shows the chip alone: its status IS the conversion, and\n * the route stamped it. The select stops its click from reaching the row, or\n * every status change would also open the record.\n */\nfunction InlineStatus(props: {\n lead: LeadRow\n onChange: (next: CrmLeadStatus) => void\n}) {\n const { lead, onChange } = props\n if (lead.convertedContactId) {\n return (\n <Box sx={{ display: 'flex', alignItems: 'center', height: '100%' }}>\n <LeadStatusChip lead={lead} />\n </Box>\n )\n }\n const status = Aglyn.crmLeadStatus(lead)\n return (\n <Box\n onClick={(event) => event.stopPropagation()}\n sx={{\n display: 'flex',\n alignItems: 'center',\n height: '100%',\n width: '100%',\n }}\n >\n <Select\n size=\"small\"\n variant=\"standard\"\n disableUnderline\n value={status}\n onChange={(event) => onChange(event.target.value as CrmLeadStatus)}\n renderValue={() => <LeadStatusChip lead={lead} />}\n sx={{ width: '100%' }}\n >\n <MenuItem value=\"new\">{Aglyn.CRM_LEAD_STATUS_LABELS.new}</MenuItem>\n <MenuItem value=\"working\">\n {Aglyn.CRM_LEAD_STATUS_LABELS.working}\n </MenuItem>\n <MenuItem value=\"unqualified\">{`${Aglyn.CRM_LEAD_STATUS_LABELS.unqualified}…`}</MenuItem>\n </Select>\n </Box>\n )\n}\nInlineStatus.displayName = 'InlineStatus'\n\n/** Hand a lead to a team member. */\nfunction AssignOwnerDialog(props: {\n lead: LeadRow | null\n roster: OrgMemberOptions\n onClose: () => void\n onAssign: (uid: string) => void\n}) {\n const { lead, roster, onClose, onAssign } = props\n const [uid, setUid] = useState<string | null>(null)\n const current = uid ?? String(lead?.ownerUid ?? '')\n return (\n <Dialog\n open={Boolean(lead)}\n onClose={onClose}\n maxWidth=\"xs\"\n fullWidth\n slotProps={{ transition: { onExited: () => setUid(null) } }}\n >\n <DialogTitle>{`Assign ${String(lead?.['name'] || lead?.['email'] || 'lead')}`}</DialogTitle>\n <DialogContent>\n <Box sx={{ pt: 1 }}>\n <LeadOwnerSelect\n value={current}\n onChange={setUid}\n roster={roster}\n size=\"medium\"\n />\n </Box>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose}>{'Cancel'}</Button>\n <Button variant=\"contained\" onClick={() => onAssign(current)}>\n {'Assign'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nAssignOwnerDialog.displayName = 'AssignOwnerDialog'\n\nexport default CrmLeadsSection\n"],"names":["Aglyn","mdiAccountArrowRight","mdiAccountCancelOutline","mdiAccountConvertOutline","mdiAccountTieOutline","mdiMagnify","CardDisplay","MdiIcon","ListPagination","ListTable","useOrgMemberOptions","useCrmOrgMount","useCrmSavedView","useCrmScope","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","useOrgLeads","CrmViewsControl","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","EmptyStateComponent","useSnackbar","useFirestore","useFirestoreCollection","useHostCampaigns","Alert","Box","Button","Dialog","DialogActions","DialogContent","DialogTitle","FormControl","InputAdornment","InputLabel","MenuItem","Select","Stack","TextField","Typography","collection","deleteField","doc","limit","orderBy","query","serverTimestamp","updateDoc","useRouter","useCallback","useEffect","useId","useMemo","useState","downloadTextFile","crmRoutes","LEAD_FILTER_LABELS","LEAD_EMAIL_FILTER_LABELS","LEAD_EMAIL_FILTERS","LEAD_FILTERS","leadMatchesCampaignFilter","leadMatchesEmailFilter","leadMatchesFilter","leadMatchesSearch","leadsCsv","LeadConvertDialog","leadSourceLabel","leadSources","leadTimeLabel","LeadImportButton","NewLeadDrawer","useCrmApi","LeadOwnerSelect","CONVERT_PENDING_ERASURE_REASON","CrmEmailStateChip","LeadStatusChip","LeadSurfacesNote","LeadUnqualifyDialog","LeadsBulkBar","OrgLeadSurfacesNote","LEADS_WINDOW","CrmLeadsSection","props","unqualifying","converting","hostId","org","basePath","firestore","router","enqueueSnackbar","orgId","createHostId","mount","roster","routes","site","idField","orgHostIds","hosts","map","host","id","orgLeads","hostIds","windowSize","leadDocs","data","row","leadId","$id","status","hostsReady","length","truncated","window","slice","views","section","filter","value","state","filters","find","clause","field","includes","emailFilter","setFilter","next","setFilters","op","setEmailFilter","campaignFilter","setCampaignFilter","campaigns","enabled","Boolean","campaignName","options","option","label","filterLabelId","emailFilterLabelId","campaignFilterLabelId","search","setSearch","rows","lead","page","setPage","pageSize","setPageSize","pageRows","selectedIds","setSelectedIds","csvOptions","ownerEmail","emailFor","siteName","handleExport","assigning","setAssigning","setUnqualifying","setConverting","crmApi","createOpen","setCreateOpen","createBusy","setCreateBusy","createError","setCreateError","handleCreate","values","response","payload","email","name","company","jobTitle","phone","website","leadSource","address","tags","campaignIds","ownerUid","notes","ok","String","variant","persist","error","console","writeLead","fields","done","updatedAt","Error","message","columns","headerName","flex","minWidth","valueGetter","_value","renderCell","spacing","sx","justifyContent","height","noWrap","color","crmLeadStatus","InlineStatus","onChange","unqualifiedReason","readEmailState","EMAIL_STATE_LABELS","labelFor","join","readCampaignIds","width","sortable","filterable","disableColumnMenu","converted","convertedContactId","erasurePending","readErasureRequestedAtMs","convertRefusal","isCrmLeadOpen","onClick","event","stopPropagation","display","alignItems","items","key","icon","path","size","href","disabled","disabledReason","undefined","grid","header","help","pluginDocsHelp","anchor","actions","direction","controller","allLabel","labelId","target","shrink","notched","displayEmpty","some","placeholder","slotProps","input","startAdornment","position","htmlInput","type","contentGutterX","contentGutterY","description","toLowerCase","trim","toLocaleString","selected","onSelectedChange","csv","columnOrder","slots","selectable","loading","onOpen","_id","push","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","quickFilter","hideFooter","rowCount","count","onPageChange","onPageSizeChange","severity","open","onClose","busy","onSubmit","AssignOwnerDialog","onAssign","uid","leadLabel","displayName","disableUnderline","renderValue","CRM_LEAD_STATUS_LABELS","new","working","unqualified","setUid","current","maxWidth","fullWidth","transition","onExited","pt"],"mappings":"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"}
|
|
@@ -12,6 +12,8 @@ export interface NewLeadValues {
|
|
|
12
12
|
status: Extract<CrmLeadStatus, 'new' | 'working'>;
|
|
13
13
|
ownerUid: string;
|
|
14
14
|
tags: string[];
|
|
15
|
+
/** The site's campaigns to file the lead under (AGL-3254), by id. */
|
|
16
|
+
campaignIds: string[];
|
|
15
17
|
address: AglynPostalAddress | null;
|
|
16
18
|
notes: string;
|
|
17
19
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { CRM_LEAD_STATUS_LABELS, CRM_LEAD_TEXT_MAX, normalizeCrmLeadTags, normalizeContactEmail, normalizeCrmLeadProfile } from "@aglyn/aglyn";
|
|
3
|
+
import { CRM_LEAD_STATUS_LABELS, CRM_LEAD_TEXT_MAX, campaignMembershipValue, normalizeCrmLeadTags, normalizeContactEmail, normalizeCrmLeadProfile } from "@aglyn/aglyn";
|
|
4
4
|
import { ICON_VARIANT_CLOSE } from "@aglyn/shared-data-enums";
|
|
5
5
|
import { Container, MdiIcon, SrOnly } from "@aglyn/shared-ui-jsx";
|
|
6
6
|
import { NavigationDrawerComponent } from "@aglyn/shared-ui-jsx/components/navigation-drawer.component";
|
|
7
7
|
import { Alert, Button, IconButton, MenuItem, Stack, TextField, Typography } from "@mui/material";
|
|
8
|
+
import CampaignPicker from "@aglyn/shared-ui-email-campaigns/components/campaign-picker.component";
|
|
9
|
+
import { useHostCampaigns } from "@aglyn/tenant-feature-instance";
|
|
8
10
|
import { useEffect, useState } from "react";
|
|
9
11
|
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
10
12
|
import { ContactAddressFields, EMPTY_ADDRESS } from "./contact-address-fields.js";
|
|
@@ -47,11 +49,20 @@ const NOTES_MAX = 4000;
|
|
|
47
49
|
const [status, setStatus] = useState('new');
|
|
48
50
|
const [ownerUid, setOwnerUid] = useState('');
|
|
49
51
|
const [tags, setTags] = useState('');
|
|
52
|
+
const [campaignIds, setCampaignIds] = useState([]);
|
|
50
53
|
const [address, setAddress] = useState(EMPTY_ADDRESS);
|
|
51
54
|
const [notes, setNotes] = useState('');
|
|
52
55
|
const [emailError, setEmailError] = useState('');
|
|
53
56
|
const [phoneError, setPhoneError] = useState('');
|
|
54
57
|
const [websiteError, setWebsiteError] = useState('');
|
|
58
|
+
/*
|
|
59
|
+
* The site's campaigns, for the picker (AGL-3254): read while the drawer
|
|
60
|
+
* is open, under the site the lead will be filed under — at the
|
|
61
|
+
* organization level, the picked one — and re-read when that changes,
|
|
62
|
+
* since a campaign belongs to one site.
|
|
63
|
+
*/ const campaigns = useHostCampaigns(createHostId != null ? createHostId : undefined, {
|
|
64
|
+
enabled: open && Boolean(createHostId)
|
|
65
|
+
});
|
|
55
66
|
// A fresh form on every opening: a person typed and then abandoned must
|
|
56
67
|
// not reappear half-filled the next time somebody reaches for New lead.
|
|
57
68
|
useEffect(()=>{
|
|
@@ -66,6 +77,7 @@ const NOTES_MAX = 4000;
|
|
|
66
77
|
setStatus('new');
|
|
67
78
|
setOwnerUid('');
|
|
68
79
|
setTags('');
|
|
80
|
+
setCampaignIds([]);
|
|
69
81
|
setAddress(EMPTY_ADDRESS);
|
|
70
82
|
setNotes('');
|
|
71
83
|
setEmailError('');
|
|
@@ -74,6 +86,10 @@ const NOTES_MAX = 4000;
|
|
|
74
86
|
}, [
|
|
75
87
|
open
|
|
76
88
|
]);
|
|
89
|
+
// A campaign picked under one site is not one of another site's.
|
|
90
|
+
useEffect(()=>setCampaignIds([]), [
|
|
91
|
+
createHostId
|
|
92
|
+
]);
|
|
77
93
|
const handleSubmit = ()=>{
|
|
78
94
|
var _errors_phone, _errors_website, _patch_company, _patch_jobTitle, _patch_phone, _patch_website, _patch_leadSource, _patch_tags, _patch_address;
|
|
79
95
|
const normalizedEmail = normalizeContactEmail(email);
|
|
@@ -101,6 +117,7 @@ const NOTES_MAX = 4000;
|
|
|
101
117
|
status,
|
|
102
118
|
ownerUid,
|
|
103
119
|
tags: (_patch_tags = patch.tags) != null ? _patch_tags : [],
|
|
120
|
+
campaignIds: campaignMembershipValue(campaignIds),
|
|
104
121
|
address: (_patch_address = patch.address) != null ? _patch_address : null,
|
|
105
122
|
notes: notes.trim().slice(0, NOTES_MAX)
|
|
106
123
|
});
|
|
@@ -287,6 +304,15 @@ const NOTES_MAX = 4000;
|
|
|
287
304
|
onChange: (event)=>setTags(event.target.value),
|
|
288
305
|
fullWidth: true
|
|
289
306
|
}),
|
|
307
|
+
createHostId ? /*#__PURE__*/ _jsx(CampaignPicker, {
|
|
308
|
+
options: campaigns.options,
|
|
309
|
+
value: campaignIds,
|
|
310
|
+
onChange: setCampaignIds,
|
|
311
|
+
helperText: "The campaigns this lead is part of. It does not decide who a campaign mails.",
|
|
312
|
+
disabled: Boolean(busy),
|
|
313
|
+
empty: campaigns.ready && !campaigns.options.length,
|
|
314
|
+
emptyText: "This site has no campaigns yet. Create one from Marketing to file leads under it."
|
|
315
|
+
}) : null,
|
|
290
316
|
/*#__PURE__*/ _jsx(Typography, {
|
|
291
317
|
variant: "subtitle2",
|
|
292
318
|
children: 'Address'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/new-lead-drawer.tsx"],"sourcesContent":["'use client'\n\nimport {\n CRM_LEAD_STATUS_LABELS,\n CRM_LEAD_TEXT_MAX,\n type CrmLeadStatus,\n normalizeCrmLeadTags,\n normalizeContactEmail,\n normalizeCrmLeadProfile,\n type AglynPostalAddress,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport {\n Alert,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport {\n ContactAddressFields,\n EMPTY_ADDRESS,\n type AddressDraft,\n} from './contact-address-fields'\nimport { CrmSitePicker } from './crm-site-picker'\nimport { LeadOwnerSelect } from './lead-owner-select'\n\n/** What the drawer hands back — already normalized where the route would. */\nexport interface NewLeadValues {\n email: string\n name: string\n company: string\n jobTitle: string\n phone: string\n website: string\n leadSource: string\n status: Extract<CrmLeadStatus, 'new' | 'working'>\n ownerUid: string\n tags: string[]\n address: AglynPostalAddress | null\n notes: string\n}\n\nexport interface NewLeadDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the lead is filed under — a lead is private to one site by\n * path. `null` at the organization level, where the drawer asks which\n * site with a picker and holds its submit until one is named.\n */\n hostId: string | null\n org?: Record<string, unknown> | null\n /** The request is in flight — the form holds still and the button says so. */\n busy?: boolean\n /** What the route answered when it refused, shown above the form. */\n error?: string | null\n /** The team, for the owner picker. */\n roster: OrgMemberOptions\n onSubmit: (values: NewLeadValues) => void\n}\n\nconst NOTES_MAX = 4000\n\n/**\n * ADDING ONE LEAD BY HAND, IN A DRAWER (AGL-3231).\n *\n * Salesforce's New Lead, on the Leads list: a person the team has heard of\n * and not yet qualified, entered with what is known — the address, the\n * name, the company as text, a title, a phone, a website, an address, tags\n * and where they came from. It makes a lead and nothing else: no contact,\n * no company record. Those are the conversion's to create once the lead is\n * real, which is what keeps one person from sitting in two lists.\n *\n * A drawer and not a form above the list, for the reason every list in this\n * console gives. The two fields a person can mistype in a way the record\n * cannot hold — the email and the phone, and the website beside them — are\n * checked before the request leaves, with the same normalizer the route\n * runs, so the refusal lands under the field. Everything else is the\n * route's to decide: whether the site already holds the address (it\n * updates that lead), whether the plan carries the suite, whether the site\n * is at the platform ceiling.\n */\nexport function NewLeadDrawer(props: NewLeadDrawerProps) {\n const { open, onClose, hostId, org, busy, error, roster, onSubmit } = props\n // The site the route files the lead under: the mounted site, or at the\n // organization level the picked one. `null` until it is known.\n const { createHostId } = useCrmScope({ hostId, org: org as never })\n\n const [email, setEmail] = useState('')\n const [name, setName] = useState('')\n const [company, setCompany] = useState('')\n const [jobTitle, setJobTitle] = useState('')\n const [phone, setPhone] = useState('')\n const [website, setWebsite] = useState('')\n const [leadSource, setLeadSource] = useState('')\n const [status, setStatus] = useState<NewLeadValues['status']>('new')\n const [ownerUid, setOwnerUid] = useState('')\n const [tags, setTags] = useState('')\n const [address, setAddress] = useState<AddressDraft>(EMPTY_ADDRESS)\n const [notes, setNotes] = useState('')\n const [emailError, setEmailError] = useState('')\n const [phoneError, setPhoneError] = useState('')\n const [websiteError, setWebsiteError] = useState('')\n\n // A fresh form on every opening: a person typed and then abandoned must\n // not reappear half-filled the next time somebody reaches for New lead.\n useEffect(() => {\n if (!open) return\n setEmail('')\n setName('')\n setCompany('')\n setJobTitle('')\n setPhone('')\n setWebsite('')\n setLeadSource('')\n setStatus('new')\n setOwnerUid('')\n setTags('')\n setAddress(EMPTY_ADDRESS)\n setNotes('')\n setEmailError('')\n setPhoneError('')\n setWebsiteError('')\n }, [open])\n\n const handleSubmit = () => {\n const normalizedEmail = normalizeContactEmail(email)\n const { patch, errors } = normalizeCrmLeadProfile({\n company,\n jobTitle,\n phone,\n website,\n leadSource,\n address,\n tags: normalizeCrmLeadTags(tags),\n })\n setEmailError(normalizedEmail ? '' : 'Enter a valid email address.')\n setPhoneError(errors.phone ?? '')\n setWebsiteError(errors.website ?? '')\n if (!normalizedEmail || errors.phone || errors.website) return\n onSubmit({\n email: normalizedEmail,\n name: name.trim().replace(/\\s+/g, ' ').slice(0, CRM_LEAD_TEXT_MAX),\n company: patch.company ?? '',\n jobTitle: patch.jobTitle ?? '',\n phone: patch.phone ?? '',\n website: patch.website ?? '',\n leadSource: patch.leadSource ?? '',\n status,\n ownerUid,\n tags: patch.tags ?? [],\n address: patch.address ?? null,\n notes: notes.trim().slice(0, NOTES_MAX),\n })\n }\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n sx={{ '& .MuiDrawer-paper': { width: { xs: '100%', sm: 480 } } }}\n appBarLeft={\n <>\n <IconButton color=\"inherit\" edge=\"start\" onClick={onClose} sx={{ mr: 2 }}>\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {'New lead'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n {/* First, because the lead is filed under the answer. Renders nothing under a site. */}\n <CrmSitePicker\n hostId={hostId}\n disabled={Boolean(busy)}\n helperText=\"The site this lead is filed under — a lead is private to one site.\"\n />\n <TextField\n size=\"small\"\n label=\"Email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n error={Boolean(emailError)}\n helperText={\n emailError ||\n 'The one field that is required — a site holds one lead per address.'\n }\n required\n fullWidth\n autoFocus\n />\n <TextField\n size=\"small\"\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n slotProps={{ htmlInput: { maxLength: CRM_LEAD_TEXT_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Company\"\n value={company}\n onChange={(event) => setCompany(event.target.value)}\n helperText=\"As text — converting the lead is what makes it a company record.\"\n slotProps={{ htmlInput: { maxLength: CRM_LEAD_TEXT_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => setJobTitle(event.target.value)}\n slotProps={{ htmlInput: { maxLength: CRM_LEAD_TEXT_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={Boolean(phoneError)}\n helperText={phoneError || 'With the country code, like +1 512 555 0107'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Website\"\n value={website}\n onChange={(event) => setWebsite(event.target.value)}\n error={Boolean(websiteError)}\n helperText={websiteError || 'Like acme.com'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Lead source\"\n value={leadSource}\n onChange={(event) => setLeadSource(event.target.value)}\n helperText=\"Where this lead came from — a list, an event, a referral\"\n slotProps={{ htmlInput: { maxLength: CRM_LEAD_TEXT_MAX } }}\n fullWidth\n />\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>\n <TextField\n select\n size=\"small\"\n label=\"Status\"\n value={status}\n onChange={(event) =>\n setStatus(event.target.value as NewLeadValues['status'])\n }\n fullWidth\n >\n <MenuItem value=\"new\">{CRM_LEAD_STATUS_LABELS.new}</MenuItem>\n <MenuItem value=\"working\">{CRM_LEAD_STATUS_LABELS.working}</MenuItem>\n </TextField>\n <LeadOwnerSelect value={ownerUid} onChange={setOwnerUid} roster={roster} />\n </Stack>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"icp2, a-list\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => setTags(event.target.value)}\n fullWidth\n />\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n <ContactAddressFields value={address} onChange={setAddress} />\n <TextField\n size=\"small\"\n label=\"Notes\"\n value={notes}\n onChange={(event) => setNotes(event.target.value)}\n multiline\n minRows={2}\n slotProps={{ htmlInput: { maxLength: NOTES_MAX } }}\n fullWidth\n />\n <Button\n variant=\"contained\"\n color=\"primary\"\n // Held until the site is known: the route files the lead under it.\n disabled={Boolean(busy) || !createHostId}\n onClick={handleSubmit}\n >\n {busy ? 'Adding…' : 'Add lead'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\n\nexport default NewLeadDrawer\n"],"names":["CRM_LEAD_STATUS_LABELS","CRM_LEAD_TEXT_MAX","normalizeCrmLeadTags","normalizeContactEmail","normalizeCrmLeadProfile","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","Alert","Button","IconButton","MenuItem","Stack","TextField","Typography","useEffect","useState","useCrmScope","ContactAddressFields","EMPTY_ADDRESS","CrmSitePicker","LeadOwnerSelect","NOTES_MAX","NewLeadDrawer","props","open","onClose","hostId","org","busy","error","roster","onSubmit","createHostId","email","setEmail","name","setName","company","setCompany","jobTitle","setJobTitle","phone","setPhone","website","setWebsite","leadSource","setLeadSource","status","setStatus","ownerUid","setOwnerUid","tags","setTags","address","setAddress","notes","setNotes","emailError","setEmailError","phoneError","setPhoneError","websiteError","setWebsiteError","handleSubmit","errors","patch","normalizedEmail","trim","replace","slice","anchor","variant","AppBarProps","color","sx","width","xs","sm","appBarLeft","edge","onClick","mr","path","component","appBarRight","gutterY","spacing","severity","disabled","Boolean","helperText","size","label","type","value","onChange","event","target","required","fullWidth","autoFocus","slotProps","htmlInput","maxLength","direction","select","new","working","placeholder","multiline","minRows"],"mappings":"AAAA;;AAEA,SACEA,sBAAsB,EACtBC,iBAAiB,EAEjBC,oBAAoB,EACpBC,qBAAqB,EACrBC,uBAAuB,QAElB,eAAc;AACrB,SAASC,kBAAkB,QAAQ,2BAA0B;AAC7D,SAASC,SAAS,EAAEC,OAAO,EAAEC,MAAM,QAAQ,uBAAsB;AACjE,SAASC,yBAAyB,QAAQ,8DAA6D;AACvG,SACEC,KAAK,EACLC,MAAM,EACNC,UAAU,EACVC,QAAQ,EACRC,KAAK,EACLC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,WAAW,QAAQ,4BAAwB;AAEpD,SACEC,oBAAoB,EACpBC,aAAa,QAER,8BAA0B;AACjC,SAASC,aAAa,QAAQ,uBAAmB;AACjD,SAASC,eAAe,QAAQ,yBAAqB;AAqCrD,MAAMC,YAAY;AAElB;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,cAAcC,KAAyB;IACrD,MAAM,EAAEC,IAAI,EAAEC,OAAO,EAAEC,MAAM,EAAEC,GAAG,EAAEC,IAAI,EAAEC,KAAK,EAAEC,MAAM,EAAEC,QAAQ,EAAE,GAAGR;IACtE,uEAAuE;IACvE,+DAA+D;IAC/D,MAAM,EAAES,YAAY,EAAE,GAAGhB,YAAY;QAAEU;QAAQC,KAAKA;IAAa;IAEjE,MAAM,CAACM,OAAOC,SAAS,GAAGnB,SAAS;IACnC,MAAM,CAACoB,MAAMC,QAAQ,GAAGrB,SAAS;IACjC,MAAM,CAACsB,SAASC,WAAW,GAAGvB,SAAS;IACvC,MAAM,CAACwB,UAAUC,YAAY,GAAGzB,SAAS;IACzC,MAAM,CAAC0B,OAAOC,SAAS,GAAG3B,SAAS;IACnC,MAAM,CAAC4B,SAASC,WAAW,GAAG7B,SAAS;IACvC,MAAM,CAAC8B,YAAYC,cAAc,GAAG/B,SAAS;IAC7C,MAAM,CAACgC,QAAQC,UAAU,GAAGjC,SAAkC;IAC9D,MAAM,CAACkC,UAAUC,YAAY,GAAGnC,SAAS;IACzC,MAAM,CAACoC,MAAMC,QAAQ,GAAGrC,SAAS;IACjC,MAAM,CAACsC,SAASC,WAAW,GAAGvC,SAAuBG;IACrD,MAAM,CAACqC,OAAOC,SAAS,GAAGzC,SAAS;IACnC,MAAM,CAAC0C,YAAYC,cAAc,GAAG3C,SAAS;IAC7C,MAAM,CAAC4C,YAAYC,cAAc,GAAG7C,SAAS;IAC7C,MAAM,CAAC8C,cAAcC,gBAAgB,GAAG/C,SAAS;IAEjD,wEAAwE;IACxE,wEAAwE;IACxED,UAAU;QACR,IAAI,CAACU,MAAM;QACXU,SAAS;QACTE,QAAQ;QACRE,WAAW;QACXE,YAAY;QACZE,SAAS;QACTE,WAAW;QACXE,cAAc;QACdE,UAAU;QACVE,YAAY;QACZE,QAAQ;QACRE,WAAWpC;QACXsC,SAAS;QACTE,cAAc;QACdE,cAAc;QACdE,gBAAgB;IAClB,GAAG;QAACtC;KAAK;IAET,MAAMuC,eAAe;YAYLC,eACEA,iBAKLC,gBACCA,iBACHA,cACEA,gBACGA,mBAGNA,aACGA;QAzBX,MAAMC,kBAAkBlE,sBAAsBiC;QAC9C,MAAM,EAAEgC,KAAK,EAAED,MAAM,EAAE,GAAG/D,wBAAwB;YAChDoC;YACAE;YACAE;YACAE;YACAE;YACAQ;YACAF,MAAMpD,qBAAqBoD;QAC7B;QACAO,cAAcQ,kBAAkB,KAAK;QACrCN,eAAcI,gBAAAA,OAAOvB,KAAK,YAAZuB,gBAAgB;QAC9BF,iBAAgBE,kBAAAA,OAAOrB,OAAO,YAAdqB,kBAAkB;QAClC,IAAI,CAACE,mBAAmBF,OAAOvB,KAAK,IAAIuB,OAAOrB,OAAO,EAAE;QACxDZ,SAAS;YACPE,OAAOiC;YACP/B,MAAMA,KAAKgC,IAAI,GAAGC,OAAO,CAAC,QAAQ,KAAKC,KAAK,CAAC,GAAGvE;YAChDuC,OAAO,GAAE4B,iBAAAA,MAAM5B,OAAO,YAAb4B,iBAAiB;YAC1B1B,QAAQ,GAAE0B,kBAAAA,MAAM1B,QAAQ,YAAd0B,kBAAkB;YAC5BxB,KAAK,GAAEwB,eAAAA,MAAMxB,KAAK,YAAXwB,eAAe;YACtBtB,OAAO,GAAEsB,iBAAAA,MAAMtB,OAAO,YAAbsB,iBAAiB;YAC1BpB,UAAU,GAAEoB,oBAAAA,MAAMpB,UAAU,YAAhBoB,oBAAoB;YAChClB;YACAE;YACAE,IAAI,GAAEc,cAAAA,MAAMd,IAAI,YAAVc,cAAc,EAAE;YACtBZ,OAAO,GAAEY,iBAAAA,MAAMZ,OAAO,YAAbY,iBAAiB;YAC1BV,OAAOA,MAAMY,IAAI,GAAGE,KAAK,CAAC,GAAGhD;QAC/B;IACF;IAEA,qBACE,KAACf;QACCkB,MAAMA;QACN8C,QAAO;QACPC,SAAQ;QACR9C,SAASA;QACT+C,aAAa;YAAEC,OAAO;QAAU;QAChCC,IAAI;YAAE,sBAAsB;gBAAEC,OAAO;oBAAEC,IAAI;oBAAQC,IAAI;gBAAI;YAAE;QAAE;QAC/DC,0BACE;;8BACE,MAACrE;oBAAWgE,OAAM;oBAAUM,MAAK;oBAAQC,SAASvD;oBAASiD,IAAI;wBAAEO,IAAI;oBAAE;;sCACrE,KAAC7E;4BAAQ8E,MAAMhF,mBAAmBgF,IAAI;;sCACtC,KAAC7E;sCAAO;;;;8BAEV,KAACQ;oBAAW0D,SAAQ;oBAAKY,WAAU;8BAChC;;;;QAIPC,2BACE,KAAC5E;YAAO+D,SAAQ;YAAWE,OAAM;YAAUO,SAASvD;sBACjD;;kBAIL,cAAA,KAACtB;YAAUkF,OAAO;sBAChB,cAAA,MAAC1E;gBAAM2E,SAAS;;oBACbzD,sBAAQ,KAACtB;wBAAMgF,UAAS;kCAAW1D;yBAAiB;kCAErD,KAACV;wBACCO,QAAQA;wBACR8D,UAAUC,QAAQ7D;wBAClB8D,YAAW;;kCAEb,KAAC9E;wBACC+E,MAAK;wBACLC,OAAM;wBACNC,MAAK;wBACLC,OAAO7D;wBACP8D,UAAU,CAACC,QAAU9D,SAAS8D,MAAMC,MAAM,CAACH,KAAK;wBAChDjE,OAAO4D,QAAQhC;wBACfiC,YACEjC,cACA;wBAEFyC,QAAQ;wBACRC,SAAS;wBACTC,SAAS;;kCAEX,KAACxF;wBACC+E,MAAK;wBACLC,OAAM;wBACNE,OAAO3D;wBACP4D,UAAU,CAACC,QAAU5D,QAAQ4D,MAAMC,MAAM,CAACH,KAAK;wBAC/CO,WAAW;4BAAEC,WAAW;gCAAEC,WAAWzG;4BAAkB;wBAAE;wBACzDqG,SAAS;;kCAEX,KAACvF;wBACC+E,MAAK;wBACLC,OAAM;wBACNE,OAAOzD;wBACP0D,UAAU,CAACC,QAAU1D,WAAW0D,MAAMC,MAAM,CAACH,KAAK;wBAClDJ,YAAW;wBACXW,WAAW;4BAAEC,WAAW;gCAAEC,WAAWzG;4BAAkB;wBAAE;wBACzDqG,SAAS;;kCAEX,KAACvF;wBACC+E,MAAK;wBACLC,OAAM;wBACNE,OAAOvD;wBACPwD,UAAU,CAACC,QAAUxD,YAAYwD,MAAMC,MAAM,CAACH,KAAK;wBACnDO,WAAW;4BAAEC,WAAW;gCAAEC,WAAWzG;4BAAkB;wBAAE;wBACzDqG,SAAS;;kCAEX,KAACvF;wBACC+E,MAAK;wBACLC,OAAM;wBACNE,OAAOrD;wBACPsD,UAAU,CAACC,QAAUtD,SAASsD,MAAMC,MAAM,CAACH,KAAK;wBAChDjE,OAAO4D,QAAQ9B;wBACf+B,YAAY/B,cAAc;wBAC1BwC,SAAS;;kCAEX,KAACvF;wBACC+E,MAAK;wBACLC,OAAM;wBACNE,OAAOnD;wBACPoD,UAAU,CAACC,QAAUpD,WAAWoD,MAAMC,MAAM,CAACH,KAAK;wBAClDjE,OAAO4D,QAAQ5B;wBACf6B,YAAY7B,gBAAgB;wBAC5BsC,SAAS;;kCAEX,KAACvF;wBACC+E,MAAK;wBACLC,OAAM;wBACNE,OAAOjD;wBACPkD,UAAU,CAACC,QAAUlD,cAAckD,MAAMC,MAAM,CAACH,KAAK;wBACrDJ,YAAW;wBACXW,WAAW;4BAAEC,WAAW;gCAAEC,WAAWzG;4BAAkB;wBAAE;wBACzDqG,SAAS;;kCAEX,MAACxF;wBAAM6F,WAAW;4BAAE5B,IAAI;4BAAUC,IAAI;wBAAM;wBAAGS,SAAS;;0CACtD,MAAC1E;gCACC6F,MAAM;gCACNd,MAAK;gCACLC,OAAM;gCACNE,OAAO/C;gCACPgD,UAAU,CAACC,QACThD,UAAUgD,MAAMC,MAAM,CAACH,KAAK;gCAE9BK,SAAS;;kDAET,KAACzF;wCAASoF,OAAM;kDAAOjG,uBAAuB6G,GAAG;;kDACjD,KAAChG;wCAASoF,OAAM;kDAAWjG,uBAAuB8G,OAAO;;;;0CAE3D,KAACvF;gCAAgB0E,OAAO7C;gCAAU8C,UAAU7C;gCAAapB,QAAQA;;;;kCAEnE,KAAClB;wBACC+E,MAAK;wBACLC,OAAM;wBACNgB,aAAY;wBACZlB,YAAW;wBACXI,OAAO3C;wBACP4C,UAAU,CAACC,QAAU5C,QAAQ4C,MAAMC,MAAM,CAACH,KAAK;wBAC/CK,SAAS;;kCAEX,KAACtF;wBAAW0D,SAAQ;kCAAa;;kCACjC,KAACtD;wBAAqB6E,OAAOzC;wBAAS0C,UAAUzC;;kCAChD,KAAC1C;wBACC+E,MAAK;wBACLC,OAAM;wBACNE,OAAOvC;wBACPwC,UAAU,CAACC,QAAUxC,SAASwC,MAAMC,MAAM,CAACH,KAAK;wBAChDe,SAAS;wBACTC,SAAS;wBACTT,WAAW;4BAAEC,WAAW;gCAAEC,WAAWlF;4BAAU;wBAAE;wBACjD8E,SAAS;;kCAEX,KAAC3F;wBACC+D,SAAQ;wBACRE,OAAM;wBACN,mEAAmE;wBACnEe,UAAUC,QAAQ7D,SAAS,CAACI;wBAC5BgD,SAASjB;kCAERnC,OAAO,YAAY;;;;;;AAMhC;AAEA,eAAeN,cAAa"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/new-lead-drawer.tsx"],"sourcesContent":["'use client'\n\nimport {\n CRM_LEAD_STATUS_LABELS,\n CRM_LEAD_TEXT_MAX,\n type CrmLeadStatus,\n campaignMembershipValue,\n normalizeCrmLeadTags,\n normalizeContactEmail,\n normalizeCrmLeadProfile,\n type AglynPostalAddress,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport {\n Alert,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { useHostCampaigns } from '@aglyn/tenant-feature-instance'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport {\n ContactAddressFields,\n EMPTY_ADDRESS,\n type AddressDraft,\n} from './contact-address-fields'\nimport { CrmSitePicker } from './crm-site-picker'\nimport { LeadOwnerSelect } from './lead-owner-select'\n\n/** What the drawer hands back — already normalized where the route would. */\nexport interface NewLeadValues {\n email: string\n name: string\n company: string\n jobTitle: string\n phone: string\n website: string\n leadSource: string\n status: Extract<CrmLeadStatus, 'new' | 'working'>\n ownerUid: string\n tags: string[]\n /** The site's campaigns to file the lead under (AGL-3254), by id. */\n campaignIds: string[]\n address: AglynPostalAddress | null\n notes: string\n}\n\nexport interface NewLeadDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the lead is filed under — a lead is private to one site by\n * path. `null` at the organization level, where the drawer asks which\n * site with a picker and holds its submit until one is named.\n */\n hostId: string | null\n org?: Record<string, unknown> | null\n /** The request is in flight — the form holds still and the button says so. */\n busy?: boolean\n /** What the route answered when it refused, shown above the form. */\n error?: string | null\n /** The team, for the owner picker. */\n roster: OrgMemberOptions\n onSubmit: (values: NewLeadValues) => void\n}\n\nconst NOTES_MAX = 4000\n\n/**\n * ADDING ONE LEAD BY HAND, IN A DRAWER (AGL-3231).\n *\n * Salesforce's New Lead, on the Leads list: a person the team has heard of\n * and not yet qualified, entered with what is known — the address, the\n * name, the company as text, a title, a phone, a website, an address, tags\n * and where they came from. It makes a lead and nothing else: no contact,\n * no company record. Those are the conversion's to create once the lead is\n * real, which is what keeps one person from sitting in two lists.\n *\n * A drawer and not a form above the list, for the reason every list in this\n * console gives. The two fields a person can mistype in a way the record\n * cannot hold — the email and the phone, and the website beside them — are\n * checked before the request leaves, with the same normalizer the route\n * runs, so the refusal lands under the field. Everything else is the\n * route's to decide: whether the site already holds the address (it\n * updates that lead), whether the plan carries the suite, whether the site\n * is at the platform ceiling.\n */\nexport function NewLeadDrawer(props: NewLeadDrawerProps) {\n const { open, onClose, hostId, org, busy, error, roster, onSubmit } = props\n // The site the route files the lead under: the mounted site, or at the\n // organization level the picked one. `null` until it is known.\n const { createHostId } = useCrmScope({ hostId, org: org as never })\n\n const [email, setEmail] = useState('')\n const [name, setName] = useState('')\n const [company, setCompany] = useState('')\n const [jobTitle, setJobTitle] = useState('')\n const [phone, setPhone] = useState('')\n const [website, setWebsite] = useState('')\n const [leadSource, setLeadSource] = useState('')\n const [status, setStatus] = useState<NewLeadValues['status']>('new')\n const [ownerUid, setOwnerUid] = useState('')\n const [tags, setTags] = useState('')\n const [campaignIds, setCampaignIds] = useState<string[]>([])\n const [address, setAddress] = useState<AddressDraft>(EMPTY_ADDRESS)\n const [notes, setNotes] = useState('')\n const [emailError, setEmailError] = useState('')\n const [phoneError, setPhoneError] = useState('')\n const [websiteError, setWebsiteError] = useState('')\n /*\n * The site's campaigns, for the picker (AGL-3254): read while the drawer\n * is open, under the site the lead will be filed under — at the\n * organization level, the picked one — and re-read when that changes,\n * since a campaign belongs to one site.\n */\n const campaigns = useHostCampaigns(createHostId ?? undefined, {\n enabled: open && Boolean(createHostId),\n })\n\n // A fresh form on every opening: a person typed and then abandoned must\n // not reappear half-filled the next time somebody reaches for New lead.\n useEffect(() => {\n if (!open) return\n setEmail('')\n setName('')\n setCompany('')\n setJobTitle('')\n setPhone('')\n setWebsite('')\n setLeadSource('')\n setStatus('new')\n setOwnerUid('')\n setTags('')\n setCampaignIds([])\n setAddress(EMPTY_ADDRESS)\n setNotes('')\n setEmailError('')\n setPhoneError('')\n setWebsiteError('')\n }, [open])\n // A campaign picked under one site is not one of another site's.\n useEffect(() => setCampaignIds([]), [createHostId])\n\n const handleSubmit = () => {\n const normalizedEmail = normalizeContactEmail(email)\n const { patch, errors } = normalizeCrmLeadProfile({\n company,\n jobTitle,\n phone,\n website,\n leadSource,\n address,\n tags: normalizeCrmLeadTags(tags),\n })\n setEmailError(normalizedEmail ? '' : 'Enter a valid email address.')\n setPhoneError(errors.phone ?? '')\n setWebsiteError(errors.website ?? '')\n if (!normalizedEmail || errors.phone || errors.website) return\n onSubmit({\n email: normalizedEmail,\n name: name.trim().replace(/\\s+/g, ' ').slice(0, CRM_LEAD_TEXT_MAX),\n company: patch.company ?? '',\n jobTitle: patch.jobTitle ?? '',\n phone: patch.phone ?? '',\n website: patch.website ?? '',\n leadSource: patch.leadSource ?? '',\n status,\n ownerUid,\n tags: patch.tags ?? [],\n campaignIds: campaignMembershipValue(campaignIds),\n address: patch.address ?? null,\n notes: notes.trim().slice(0, NOTES_MAX),\n })\n }\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n sx={{ '& .MuiDrawer-paper': { width: { xs: '100%', sm: 480 } } }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={onClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {'New lead'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n {/* First, because the lead is filed under the answer. Renders nothing under a site. */}\n <CrmSitePicker\n hostId={hostId}\n disabled={Boolean(busy)}\n helperText=\"The site this lead is filed under — a lead is private to one site.\"\n />\n <TextField\n size=\"small\"\n label=\"Email\"\n type=\"email\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n error={Boolean(emailError)}\n helperText={\n emailError ||\n 'The one field that is required — a site holds one lead per address.'\n }\n required\n fullWidth\n autoFocus\n />\n <TextField\n size=\"small\"\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n slotProps={{ htmlInput: { maxLength: CRM_LEAD_TEXT_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Company\"\n value={company}\n onChange={(event) => setCompany(event.target.value)}\n helperText=\"As text — converting the lead is what makes it a company record.\"\n slotProps={{ htmlInput: { maxLength: CRM_LEAD_TEXT_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Job title\"\n value={jobTitle}\n onChange={(event) => setJobTitle(event.target.value)}\n slotProps={{ htmlInput: { maxLength: CRM_LEAD_TEXT_MAX } }}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Phone\"\n value={phone}\n onChange={(event) => setPhone(event.target.value)}\n error={Boolean(phoneError)}\n helperText={\n phoneError || 'With the country code, like +1 512 555 0107'\n }\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Website\"\n value={website}\n onChange={(event) => setWebsite(event.target.value)}\n error={Boolean(websiteError)}\n helperText={websiteError || 'Like acme.com'}\n fullWidth\n />\n <TextField\n size=\"small\"\n label=\"Lead source\"\n value={leadSource}\n onChange={(event) => setLeadSource(event.target.value)}\n helperText=\"Where this lead came from — a list, an event, a referral\"\n slotProps={{ htmlInput: { maxLength: CRM_LEAD_TEXT_MAX } }}\n fullWidth\n />\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>\n <TextField\n select\n size=\"small\"\n label=\"Status\"\n value={status}\n onChange={(event) =>\n setStatus(event.target.value as NewLeadValues['status'])\n }\n fullWidth\n >\n <MenuItem value=\"new\">{CRM_LEAD_STATUS_LABELS.new}</MenuItem>\n <MenuItem value=\"working\">\n {CRM_LEAD_STATUS_LABELS.working}\n </MenuItem>\n </TextField>\n <LeadOwnerSelect\n value={ownerUid}\n onChange={setOwnerUid}\n roster={roster}\n />\n </Stack>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"icp2, a-list\"\n helperText=\"Comma-separated\"\n value={tags}\n onChange={(event) => setTags(event.target.value)}\n fullWidth\n />\n {/*\n The campaigns to file the lead under (AGL-3254), picked the way a\n form's page picks them. Grouping, not consent: it decides which\n campaign pages list the lead, never whether anything mails them.\n */}\n {createHostId ? (\n <CampaignPicker\n options={campaigns.options}\n value={campaignIds}\n onChange={setCampaignIds}\n helperText=\"The campaigns this lead is part of. It does not decide who a campaign mails.\"\n disabled={Boolean(busy)}\n empty={campaigns.ready && !campaigns.options.length}\n emptyText=\"This site has no campaigns yet. Create one from Marketing to file leads under it.\"\n />\n ) : null}\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n <ContactAddressFields value={address} onChange={setAddress} />\n <TextField\n size=\"small\"\n label=\"Notes\"\n value={notes}\n onChange={(event) => setNotes(event.target.value)}\n multiline\n minRows={2}\n slotProps={{ htmlInput: { maxLength: NOTES_MAX } }}\n fullWidth\n />\n <Button\n variant=\"contained\"\n color=\"primary\"\n // Held until the site is known: the route files the lead under it.\n disabled={Boolean(busy) || !createHostId}\n onClick={handleSubmit}\n >\n {busy ? 'Adding…' : 'Add lead'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\n\nexport default NewLeadDrawer\n"],"names":["CRM_LEAD_STATUS_LABELS","CRM_LEAD_TEXT_MAX","campaignMembershipValue","normalizeCrmLeadTags","normalizeContactEmail","normalizeCrmLeadProfile","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","Alert","Button","IconButton","MenuItem","Stack","TextField","Typography","CampaignPicker","useHostCampaigns","useEffect","useState","useCrmScope","ContactAddressFields","EMPTY_ADDRESS","CrmSitePicker","LeadOwnerSelect","NOTES_MAX","NewLeadDrawer","props","open","onClose","hostId","org","busy","error","roster","onSubmit","createHostId","email","setEmail","name","setName","company","setCompany","jobTitle","setJobTitle","phone","setPhone","website","setWebsite","leadSource","setLeadSource","status","setStatus","ownerUid","setOwnerUid","tags","setTags","campaignIds","setCampaignIds","address","setAddress","notes","setNotes","emailError","setEmailError","phoneError","setPhoneError","websiteError","setWebsiteError","campaigns","undefined","enabled","Boolean","handleSubmit","errors","patch","normalizedEmail","trim","replace","slice","anchor","variant","AppBarProps","color","sx","width","xs","sm","appBarLeft","edge","onClick","mr","path","component","appBarRight","gutterY","spacing","severity","disabled","helperText","size","label","type","value","onChange","event","target","required","fullWidth","autoFocus","slotProps","htmlInput","maxLength","direction","select","new","working","placeholder","options","empty","ready","length","emptyText","multiline","minRows"],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/ // The contract from its own module, not the plugin-manager barrel: boot needs
|
|
17
17
|
// the registry and nothing else, and the barrel reaches the client contexts.
|
|
18
18
|
import { registerPluginContactCaptureWriter } from "@aglyn/aglyn/plugin-manager/plugin-contact-capture";
|
|
19
|
+
import { registerPluginLeadConversionListener } from "@aglyn/aglyn/plugin-manager/plugin-lead-conversion";
|
|
19
20
|
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
20
21
|
/**
|
|
21
22
|
* The CRM as the plugin that keeps people.
|
|
@@ -57,6 +58,20 @@ import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
|
57
58
|
registerPluginContactCaptureWriter(crmContactCaptureWriter, {
|
|
58
59
|
pluginId: BUNDLE_ID
|
|
59
60
|
});
|
|
61
|
+
// The CRM's own share of a lead conversion (AGL-3254): the lead's
|
|
62
|
+
// campaigns go onto the contact's facet. Through the seam every door
|
|
63
|
+
// that converts a lead reaches, and deferred like the capture: the
|
|
64
|
+
// module that writes is loaded when the first conversion arrives, and
|
|
65
|
+
// it brings the Admin SDK with it — this file defers the plugin's OWN
|
|
66
|
+
// module only. A library imported statically across the plugin cannot
|
|
67
|
+
// also be lazy-loaded here: `enforce-module-boundaries` refuses every
|
|
68
|
+
// static import of a library the project lazy-loads anywhere.
|
|
69
|
+
registerPluginLeadConversionListener(async (request)=>{
|
|
70
|
+
const { carryLeadCampaignsOnConversion } = await import("./server/lead-campaign-carry.js");
|
|
71
|
+
return carryLeadCampaignsOnConversion(request);
|
|
72
|
+
}, {
|
|
73
|
+
pluginId: BUNDLE_ID
|
|
74
|
+
});
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
//# sourceMappingURL=declarations.server.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/declarations.server.ts"],"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// The contract from its own module, not the plugin-manager barrel: boot needs\n// the registry and nothing else, and the barrel reaches the client contexts.\nimport {\n registerPluginContactCaptureWriter,\n type PluginContactCaptureWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport { BUNDLE_ID } from './constants/bundle-common'\n\n/**\n * The CRM as the plugin that keeps people.\n *\n * Light by construction, the way the workflows listener is: the capture\n * itself is imported when the first one arrives, not when the process\n * starts, so a process that never captures anybody pays for this object and\n * nothing else. That matters because this registers in EVERY server process,\n * including ones that will never touch the CRM.\n */\nexport const crmContactCaptureWriter: PluginContactCaptureWriter = {\n async capture(request) {\n const { captureContactForCrm } = await import('./server/capture-contact')\n return await captureContactForCrm(request)\n },\n}\n\n/**\n * The plugin's SERVER declarations: what the server must know at boot, before\n * any door of the CRM has been called.\n *\n * ## Why a declaration and not a `tenantApi` registration\n *\n * The doors that meet a person mostly never load this plugin. A form\n * submission is a core route; an order and a booking are captured inside\n * STRIPE WEBHOOKS. None of them has a reason to load the CRM, and a registry\n * filled by a surface that did not load answers `null` — which\n * `capturePluginContact` defines as \"this workspace has no record system\", a\n * sentence that would be false and silent. Orders would stop creating\n * contacts with nothing red anywhere, which is the AGL-3025 shape.\n *\n * Running from both apps' generated server-declarations manifest at boot is\n * what makes the answer true in every process. It is also called from the\n * plugin's own API register functions, so a process whose boot did not run it\n * still registers the writer the first time a CRM door loads. Registering\n * twice replaces in place.\n *\n * ⚠️ The registry holds ONE writer: a workspace keeps one set of people. A\n * second plugin's is refused naming both, and the incumbent keeps serving.\n */\nexport function registerCrmServerDeclarations(): void {\n registerPluginContactCaptureWriter(crmContactCaptureWriter, {\n pluginId: BUNDLE_ID,\n })\n}\n"],"names":["registerPluginContactCaptureWriter","BUNDLE_ID","crmContactCaptureWriter","capture","request","captureContactForCrm","registerCrmServerDeclarations","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,6EAA6E;AAC7E,SACEA,kCAAkC,QAE7B,qDAAoD;AAC3D,SAASC,SAAS,QAAQ,+BAA2B;AAErD;;;;;;;;CAQC,GACD,OAAO,MAAMC,0BAAsD;IACjE,MAAMC,SAAQC,OAAO;QACnB,MAAM,EAAEC,oBAAoB,EAAE,GAAG,MAAM,MAAM,CAAC;QAC9C,OAAO,MAAMA,qBAAqBD;IACpC;AACF,EAAC;AAED;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,SAASE;
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/crm/src/lib/declarations.server.ts"],"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// The contract from its own module, not the plugin-manager barrel: boot needs\n// the registry and nothing else, and the barrel reaches the client contexts.\nimport {\n registerPluginContactCaptureWriter,\n type PluginContactCaptureWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport { registerPluginLeadConversionListener } from '@aglyn/aglyn/plugin-manager/plugin-lead-conversion'\nimport { BUNDLE_ID } from './constants/bundle-common'\n\n/**\n * The CRM as the plugin that keeps people.\n *\n * Light by construction, the way the workflows listener is: the capture\n * itself is imported when the first one arrives, not when the process\n * starts, so a process that never captures anybody pays for this object and\n * nothing else. That matters because this registers in EVERY server process,\n * including ones that will never touch the CRM.\n */\nexport const crmContactCaptureWriter: PluginContactCaptureWriter = {\n async capture(request) {\n const { captureContactForCrm } = await import('./server/capture-contact')\n return await captureContactForCrm(request)\n },\n}\n\n/**\n * The plugin's SERVER declarations: what the server must know at boot, before\n * any door of the CRM has been called.\n *\n * ## Why a declaration and not a `tenantApi` registration\n *\n * The doors that meet a person mostly never load this plugin. A form\n * submission is a core route; an order and a booking are captured inside\n * STRIPE WEBHOOKS. None of them has a reason to load the CRM, and a registry\n * filled by a surface that did not load answers `null` — which\n * `capturePluginContact` defines as \"this workspace has no record system\", a\n * sentence that would be false and silent. Orders would stop creating\n * contacts with nothing red anywhere, which is the AGL-3025 shape.\n *\n * Running from both apps' generated server-declarations manifest at boot is\n * what makes the answer true in every process. It is also called from the\n * plugin's own API register functions, so a process whose boot did not run it\n * still registers the writer the first time a CRM door loads. Registering\n * twice replaces in place.\n *\n * ⚠️ The registry holds ONE writer: a workspace keeps one set of people. A\n * second plugin's is refused naming both, and the incumbent keeps serving.\n */\nexport function registerCrmServerDeclarations(): void {\n registerPluginContactCaptureWriter(crmContactCaptureWriter, {\n pluginId: BUNDLE_ID,\n })\n // The CRM's own share of a lead conversion (AGL-3254): the lead's\n // campaigns go onto the contact's facet. Through the seam every door\n // that converts a lead reaches, and deferred like the capture: the\n // module that writes is loaded when the first conversion arrives, and\n // it brings the Admin SDK with it — this file defers the plugin's OWN\n // module only. A library imported statically across the plugin cannot\n // also be lazy-loaded here: `enforce-module-boundaries` refuses every\n // static import of a library the project lazy-loads anywhere.\n registerPluginLeadConversionListener(\n async (request) => {\n const { carryLeadCampaignsOnConversion } =\n await import('./server/lead-campaign-carry')\n return carryLeadCampaignsOnConversion(request)\n },\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["registerPluginContactCaptureWriter","registerPluginLeadConversionListener","BUNDLE_ID","crmContactCaptureWriter","capture","request","captureContactForCrm","registerCrmServerDeclarations","pluginId","carryLeadCampaignsOnConversion"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,6EAA6E;AAC7E,SACEA,kCAAkC,QAE7B,qDAAoD;AAC3D,SAASC,oCAAoC,QAAQ,qDAAoD;AACzG,SAASC,SAAS,QAAQ,+BAA2B;AAErD;;;;;;;;CAQC,GACD,OAAO,MAAMC,0BAAsD;IACjE,MAAMC,SAAQC,OAAO;QACnB,MAAM,EAAEC,oBAAoB,EAAE,GAAG,MAAM,MAAM,CAAC;QAC9C,OAAO,MAAMA,qBAAqBD;IACpC;AACF,EAAC;AAED;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,SAASE;IACdP,mCAAmCG,yBAAyB;QAC1DK,UAAUN;IACZ;IACA,kEAAkE;IAClE,qEAAqE;IACrE,mEAAmE;IACnE,sEAAsE;IACtE,sEAAsE;IACtE,sEAAsE;IACtE,sEAAsE;IACtE,8DAA8D;IAC9DD,qCACE,OAAOI;QACL,MAAM,EAAEI,8BAA8B,EAAE,GACtC,MAAM,MAAM,CAAC;QACf,OAAOA,+BAA+BJ;IACxC,GACA;QAAEG,UAAUN;IAAU;AAE1B"}
|
|
@@ -42,7 +42,7 @@ export declare const LEAD_IMPORT_STATUSES: readonly CrmLeadStatus[];
|
|
|
42
42
|
* contact beside it. They read under the same labels the contacts import
|
|
43
43
|
* uses, so one spreadsheet maps the same way into either.
|
|
44
44
|
*/
|
|
45
|
-
export declare const LEAD_IMPORT_FIELDS: readonly ["email", "name", "company", "jobTitle", "phone", "website", "leadSource", "status", "ownerEmail", "addressLine1", "addressLine2", "addressCity", "addressState", "addressPostalCode", "addressCountry", "tags", "unqualifiedReason", "notes"];
|
|
45
|
+
export declare const LEAD_IMPORT_FIELDS: readonly ["email", "name", "company", "jobTitle", "phone", "website", "leadSource", "status", "ownerEmail", "addressLine1", "addressLine2", "addressCity", "addressState", "addressPostalCode", "addressCountry", "tags", "campaigns", "unqualifiedReason", "notes"];
|
|
46
46
|
export type LeadImportField = (typeof LEAD_IMPORT_FIELDS)[number];
|
|
47
47
|
/** How each field reads in the mapping menu. Typed so a field cannot ship unlabeled. */
|
|
48
48
|
export declare const LEAD_IMPORT_FIELD_LABELS: Record<LeadImportField, string>;
|
|
@@ -58,9 +58,11 @@ export declare function guessLeadImportMapping(columns: readonly string[]): Lead
|
|
|
58
58
|
export type LeadImportRawRow = Partial<Record<LeadImportField, unknown>>;
|
|
59
59
|
/** One parsed line under the mapping. Empty cells are left absent; a lead has no custom fields. */
|
|
60
60
|
export declare function mapLeadImportRow(cells: readonly string[], mapping: Record<number, LeadImportField | `custom:${string}`>): LeadImportRawRow;
|
|
61
|
-
export type LeadImportSkipReason = 'invalid-email' | 'duplicate' | 'lead-ceiling' | 'write-failed';
|
|
61
|
+
export type LeadImportSkipReason = 'invalid-email' | 'duplicate' | 'lead-ceiling' | 'campaign-unknown' | 'write-failed';
|
|
62
62
|
/** How a skip reason reads on screen and in the downloaded file. */
|
|
63
63
|
export declare const LEAD_IMPORT_SKIP_LABELS: Record<LeadImportSkipReason, string>;
|
|
64
|
+
/** The most campaigns one row may name — the membership field's own cap. */
|
|
65
|
+
export declare const LEAD_IMPORT_CAMPAIGNS_MAX = 20;
|
|
64
66
|
/** One row, ready for the server to resolve and write. */
|
|
65
67
|
export interface LeadImportRow {
|
|
66
68
|
/** Normalized — the address `personKey` derives the document id from. */
|
|
@@ -73,6 +75,13 @@ export interface LeadImportRow {
|
|
|
73
75
|
* is a cell nobody filled, not a decision to erase what the site knows.
|
|
74
76
|
*/
|
|
75
77
|
profile: CrmLeadProfile;
|
|
78
|
+
/**
|
|
79
|
+
* The campaigns the row files the lead under (AGL-3254), by NAME as the
|
|
80
|
+
* file wrote them: the server resolves each against the site's own
|
|
81
|
+
* containers and refuses the row whole when one is not there, because a
|
|
82
|
+
* row filed under half its campaigns is a row nobody asked for.
|
|
83
|
+
*/
|
|
84
|
+
campaigns?: string[];
|
|
76
85
|
/** Absent leaves an existing lead's status alone and a new one reading as `new`. */
|
|
77
86
|
status?: CrmLeadStatus;
|
|
78
87
|
/** Normalized, for the server to resolve against the org's members. */
|