@aglyn/plugins-crm 1.0.0-beta.178 → 1.0.0-beta.179
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/contact-detail-page.js +1 -0
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-properties-card.d.ts +6 -0
- package/src/lib/components/contact-properties-card.js +25 -2
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/csv-import-drawer.d.ts +7 -0
- package/src/lib/components/csv-import-drawer.js +7 -0
- package/src/lib/components/csv-import-drawer.js.map +1 -1
- package/src/lib/components/fields-section.js +6 -0
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-import-drawer.d.ts +3 -0
- package/src/lib/components/lead-import-drawer.js +29 -6
- package/src/lib/components/lead-import-drawer.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +10 -12
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-source-select.d.ts +30 -0
- package/src/lib/components/lead-source-select.js +96 -0
- package/src/lib/components/lead-source-select.js.map +1 -0
- package/src/lib/components/lead-source-values-card.d.ts +27 -0
- package/src/lib/components/lead-source-values-card.js +606 -0
- package/src/lib/components/lead-source-values-card.js.map +1 -0
- package/src/lib/components/leads-section.js +100 -4
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-lead-drawer.js +27 -11
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/hooks/use-lead-source-picklist.d.ts +23 -0
- package/src/lib/hooks/use-lead-source-picklist.js +50 -0
- package/src/lib/hooks/use-lead-source-picklist.js.map +1 -0
- package/src/lib/model/contact-record.d.ts +2 -0
- package/src/lib/model/contact-record.js +2 -1
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-update.d.ts +6 -0
- package/src/lib/model/contact-update.js +1 -0
- package/src/lib/model/contact-update.js.map +1 -1
- package/src/lib/model/crm-lead-import.d.ts +10 -2
- package/src/lib/model/crm-lead-import.js +19 -1
- package/src/lib/model/crm-lead-import.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +13 -0
- package/src/lib/model/lead-filters.js +17 -1
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/lead-source-values.d.ts +82 -0
- package/src/lib/model/lead-source-values.js +149 -0
- package/src/lib/model/lead-source-values.js.map +1 -0
- package/src/lib/server/contact-update.js +25 -2
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/lead-create.js +19 -0
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-source-picklist.d.ts +42 -0
- package/src/lib/server/lead-source-picklist.js +52 -0
- package/src/lib/server/lead-source-picklist.js.map +1 -0
- package/src/lib/server/lead-source-values.d.ts +50 -0
- package/src/lib/server/lead-source-values.js +187 -0
- package/src/lib/server/lead-source-values.js.map +1 -0
- package/src/lib/server/leads-import.js +31 -2
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/csv-import-drawer.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\n/**\n * IMPORTING A CSV INTO A CRM COLLECTION — the screen, for whichever\n * collection (AGL-2602, shared since AGL-2621).\n *\n * The contact import settled the walk from a file on disk to a result the\n * operator can act on: choose the file, read what is in it, match its\n * columns to fields with the first row beside each, preview the ten rows\n * the mapping produces, run it in bounded requests with a progress bar,\n * and report what was added, updated and skipped with the skipped rows as\n * a file to fix. A companies file walks the same path with a different\n * vocabulary, so the walk lives here once and each collection hands in a\n * {@link CsvImportVocabulary}: its fields and their labels, the one field\n * every row needs, its route, its tally, its template.\n *\n * ## The file is read in the browser, and judged on the server\n *\n * `parseCsv` — the same parser the list import and the dataset import run\n * — turns the file into cells here, so the operator can map and preview\n * without a round trip and without uploading a file they may still decide\n * not to import. Nothing is VALIDATED here beyond counting the rows whose\n * required cell is unusable, and that count is a courtesy: every row is\n * judged by the route, through the normalizers every other door uses, and\n * the numbers on the result panel are the server's.\n *\n * ## Bounded requests, and closing stops them\n *\n * A chunk per request, as many requests as the file needs, and a progress\n * bar between them. Closing the drawer mid-run stops the loop after the\n * request in flight; the rows already written stay written and the result\n * panel says how far it got. There is no durable job to resume — an import\n * is idempotent by construction (the door dedupes on the address, or the\n * domain and the name), so running the same file again finishes what a\n * closed drawer left, reporting the earlier rows as merged.\n */\n\nimport {\n customImportTarget,\n customImportTargetKey,\n type ImportChunkResult,\n type ImportSkippedRow,\n LIST_IMPORT_MAX_CHARACTERS,\n parseCsv,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, HelpTip, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useOrgDataScope, useUser } from '@aglyn/tenant-feature-instance'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n Alert,\n AlertTitle,\n Box,\n Button,\n Chip,\n IconButton,\n LinearProgress,\n MenuItem,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n type ComponentProps,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { importColumns } from '../model/import-column-shapes'\nimport { CrmSitePicker } from './crm-site-picker'\n\n/** A column's target: a standard field, or a custom one as `custom:<key>`. */\nexport type CsvImportTarget<F extends string> = F | `custom:${string}`\n\n/** Column index → target. A column absent from the map is not imported. */\nexport type CsvImportMapping<F extends string> = Record<number, CsvImportTarget<F>>\n\n/** A custom field a column may be mapped to, when the collection has any. */\nexport interface CsvImportCustomField {\n key: string\n label: string\n}\n\n/**\n * Everything one collection's import says for itself. Pure values and pure\n * functions, so a vocabulary is built once outside React and handed in.\n */\nexport interface CsvImportVocabulary<\n F extends string,\n R extends Record<string, unknown>,\n S extends ImportSkippedRow<string>,\n> {\n /** The drawer's title — \"Import contacts from CSV\". */\n title: string\n /**\n * What the import is called where the `importMapping` zone names it —\n * `contacts`, `companies`, `deals`, `leads` (AGL-2917). Absent, the drawer\n * hosts no zone.\n */\n collection?: string\n help: ComponentProps<typeof HelpTip>\n /**\n * What the site picker's answer decides, said under it at the\n * organization level (AGL-2630). Absent, the picker's own sentence.\n */\n sitePickerHelperText?: string\n /** The paragraph under the title. */\n intro: string\n /** The standard targets, in the order the mapping menu lists them. */\n fields: readonly F[]\n fieldLabels: Record<F, string>\n /** Custom targets, offered after the standard ones; none for most collections. */\n customFields?: readonly CsvImportCustomField[]\n /** The one field every row needs, and what the drawer says when it is unmapped. */\n requiredField: F\n requiredWarning: string\n /**\n * Whether a required cell is one the server would refuse — the courtesy\n * count the preview shows. Absent, only a blank cell counts.\n */\n unusable?: (cell: string) => boolean\n /** The preview's sentence about them. */\n unusableNotice: (count: number, total: number) => string\n guessMapping: (columns: readonly string[]) => CsvImportMapping<F>\n mapRow: (cells: readonly string[], mapping: CsvImportMapping<F>) => R\n /** The browser-side URL of the route one chunk is posted to. */\n route: string\n maxRows: number\n chunkSize: number\n previewRows: number\n emptyResult: () => ImportChunkResult<S>\n mergeResults: (\n total: ImportChunkResult<S>,\n chunk: ImportChunkResult<S>,\n offset: number,\n ) => ImportChunkResult<S>\n skipLabels: Record<S['reason'], string>\n skippedCsv: (\n columns: readonly string[],\n entries: readonly { cells: readonly string[]; reason: S['reason'] }[],\n ) => string\n skippedFileName: string\n /** The export's header over no rows. */\n templateCsv: () => string\n templateFileName: string\n /** The result panel's collection-specific lines, when there are any. */\n resultExtras?: (result: ImportChunkResult<S>) => ReactNode\n}\n\nexport interface CsvImportDrawerProps<\n F extends string,\n R extends Record<string, unknown>,\n S extends ImportSkippedRow<string>,\n> {\n open: boolean\n onClose: () => void\n /**\n * The site the file is imported INTO — what the route files every row\n * under — or `null` at the organization level (AGL-2630), where the\n * drawer asks which site with a picker and holds the import until one is\n * named.\n */\n hostId: string | null\n vocabulary: CsvImportVocabulary<F, R, S>\n}\n\n/** The value the mapping select shows for a column that is not imported. */\nconst SKIP_TARGET = ''\n\n/** What a target is called — a standard field's label, or the custom field's. */\nfunction targetLabel<F extends string>(\n target: string,\n vocabulary: Pick<CsvImportVocabulary<F, never, never>, 'fieldLabels' | 'customFields'>,\n): string {\n const customKey = customImportTargetKey(target)\n if (customKey) {\n return vocabulary.customFields?.find((field) => field.key === customKey)?.label ?? customKey\n }\n return vocabulary.fieldLabels[target as F] ?? target\n}\n\n/** What a dropped-value field is called in the result panel — the label without its hint. */\nfunction droppedFieldLabel<F extends string>(\n field: string,\n vocabulary: Pick<CsvImportVocabulary<F, never, never>, 'fieldLabels' | 'customFields'>,\n): string {\n return targetLabel(field, vocabulary).replace(/ \\(.*\\)$/, '')\n}\n\nexport function CsvImportDrawer<\n F extends string,\n R extends Record<string, unknown>,\n S extends ImportSkippedRow<string>,\n>(props: CsvImportDrawerProps<F, R, S>) {\n const { open, onClose, hostId, vocabulary } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n // The site every row is captured under: the mounted one, or at the\n // organization level the one the picker named — `null` until it has.\n const mount = useCrmOrgMount()\n const createHostId = hostId ?? mount?.createHostId ?? null\n\n const [fileName, setFileName] = useState('')\n const [columns, setColumns] = useState<string[]>([])\n const [rows, setRows] = useState<string[][]>([])\n /** How many rows the file had past the ceiling, so the drawer can say so. */\n const [overCeiling, setOverCeiling] = useState(0)\n const [mapping, setMapping] = useState<CsvImportMapping<F>>({})\n const [result, setResult] = useState<ImportChunkResult<S> | null>(null)\n const [progress, setProgress] = useState<{ sent: number; total: number } | null>(\n null,\n )\n const [busy, setBusy] = useState(false)\n /**\n * Set when the drawer closes mid-run, read by the loop.\n *\n * A ref and not state, for the reason the list import gives: the loop is\n * an `async` function that captured its closure when it started, and a\n * state value it read would be the one from that render.\n */\n const abandoned = useRef(false)\n\n useEffect(() => {\n if (open) abandoned.current = false\n }, [open])\n\n const reset = useCallback(() => {\n setFileName('')\n setColumns([])\n setRows([])\n setOverCeiling(0)\n setMapping({})\n setResult(null)\n setProgress(null)\n }, [])\n\n const handleClose = useCallback(() => {\n abandoned.current = true\n onClose()\n }, [onClose])\n\n const handleFile = useCallback(\n async (file: File | null | undefined) => {\n if (!file) return\n const contents = await file.text().catch(() => '')\n if (contents.length > LIST_IMPORT_MAX_CHARACTERS) {\n return void enqueueSnackbar(\n 'That file is too large to read in one go. Split it and import the ' +\n 'pieces.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n const table = parseCsv(contents).filter((cells) =>\n cells.some((cell) => String(cell ?? '').trim()),\n )\n if (table.length < 2) {\n return void enqueueSnackbar(\n 'That file needs a header row and at least one row under it.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n const header = table[0].map((cell) => String(cell ?? '').trim())\n const body = table.slice(1)\n setFileName(file.name)\n setColumns(header)\n setRows(body.slice(0, vocabulary.maxRows))\n setOverCeiling(Math.max(0, body.length - vocabulary.maxRows))\n setMapping(vocabulary.guessMapping(header))\n setResult(null)\n setProgress(null)\n },\n [enqueueSnackbar, vocabulary],\n )\n\n const requiredMapped = Object.values(mapping).includes(vocabulary.requiredField)\n /** Rows whose required cell the server would refuse. */\n const unusableRows = useMemo(() => {\n const at = Number(\n Object.entries(mapping).find(\n ([, target]) => target === vocabulary.requiredField,\n )?.[0] ?? -1,\n )\n if (at < 0) return rows.length\n const unusable = vocabulary.unusable ?? ((cell: string) => !cell.trim())\n return rows.filter((cells) => unusable(String(cells[at] ?? ''))).length\n }, [mapping, rows, vocabulary])\n\n const preview = useMemo(\n () =>\n rows\n .slice(0, vocabulary.previewRows)\n .map((cells) => vocabulary.mapRow(cells, mapping)),\n [rows, mapping, vocabulary],\n )\n const mappedTargets = useMemo(\n () =>\n Object.entries(mapping)\n .sort(([a], [b]) => Number(a) - Number(b))\n .map(([, target]) => target),\n [mapping],\n )\n\n /*\n * The `importMapping` zone (AGL-2917), hosted through the shell's renderer:\n * a widget there reads each column's header and shape, never a cell, and\n * proposes a matching, which replaces the table's above for the preview to\n * show. A target this import does not offer, or one a column before it\n * already took, is dropped, as the table itself would refuse it.\n */\n const MappingSlot = useConsoleWidgetSlot()\n const { orgId } = useOrgDataScope({ hostId: hostId ?? undefined, orgId: mount?.orgId })\n const shapedColumns = useMemo(() => importColumns(columns, rows), [columns, rows])\n const offeredTargets = useMemo(\n () =>\n new Set<string>([\n ...vocabulary.fields,\n ...(vocabulary.customFields ?? []).map((field) => customImportTarget(field.key)),\n ]),\n [vocabulary],\n )\n const proposeMapping = useCallback(\n (proposed: Readonly<Record<number, string>>) => {\n const taken = new Set<string>()\n const next: CsvImportMapping<F> = {}\n for (const [column, target] of Object.entries(proposed).sort(([a], [b]) => Number(a) - Number(b))) {\n const index = Number(column)\n if (!Number.isInteger(index) || index < 0 || index >= columns.length) continue\n if (!offeredTargets.has(target) || taken.has(target)) continue\n taken.add(target)\n next[index] = target as CsvImportTarget<F>\n }\n setMapping(next)\n },\n [offeredTargets, columns.length],\n )\n\n const handleMap = useCallback((index: number, target: string) => {\n setMapping((previous) => {\n const next: CsvImportMapping<F> = {}\n for (const [column, existing] of Object.entries(previous)) {\n // One column per target: choosing a target already taken moves it\n // here rather than importing one field from two columns.\n if (Number(column) !== index && existing !== target) {\n next[Number(column)] = existing\n }\n }\n if (target !== SKIP_TARGET) next[index] = target as CsvImportTarget<F>\n return next\n })\n }, [])\n\n /**\n * Runs the file, one bounded request at a time.\n *\n * Every chunk's result is folded into the running total as it arrives,\n * so a file that stops halfway — a lost connection, a closed drawer —\n * still reports what it did rather than nothing.\n */\n const handleImport = useCallback(async () => {\n if (busy || !rows.length || !requiredMapped || !createHostId) return\n setBusy(true)\n let total = vocabulary.emptyResult()\n setResult(total)\n setProgress({ sent: 0, total: rows.length })\n try {\n for (let offset = 0; offset < rows.length; offset += vocabulary.chunkSize) {\n if (abandoned.current) return\n const chunk = rows\n .slice(offset, offset + vocabulary.chunkSize)\n .map((cells) => vocabulary.mapRow(cells, mapping))\n const response = await authorizedFetch(user, vocabulary.route, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId: createHostId, rows: chunk }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The import could not continue.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n total = vocabulary.mergeResults(total, payload as ImportChunkResult<S>, offset)\n setResult(total)\n setProgress({ sent: Math.min(offset + chunk.length, rows.length), total: rows.length })\n }\n enqueueSnackbar('Import finished', { variant: 'success', persist: false })\n } catch {\n enqueueSnackbar('The import could not continue.', {\n variant: 'warning',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [busy, rows, requiredMapped, mapping, user, createHostId, enqueueSnackbar, vocabulary])\n\n const handleDownloadSkipped = useCallback(() => {\n if (!result?.skipped.length) return\n downloadTextFile(\n vocabulary.skippedFileName,\n 'text/csv',\n vocabulary.skippedCsv(\n columns,\n result.skipped.map((entry) => ({\n cells: rows[entry.index] ?? [],\n reason: entry.reason as S['reason'],\n })),\n ),\n )\n }, [result, columns, rows, vocabulary])\n\n const finished = !!result && !busy\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={handleClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={handleClose}\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 {vocabulary.title}\n </Typography>\n <HelpTip {...vocabulary.help} />\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={handleClose}>\n {finished ? 'Done' : 'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {result ? (\n <ImportResultPanel\n result={result}\n progress={progress}\n busy={busy}\n vocabulary={vocabulary}\n onDownloadSkipped={handleDownloadSkipped}\n onAnother={reset}\n />\n ) : (\n <>\n {/*\n First, because every row is filed under the answer: the site\n the file is imported into. Nothing under a site (AGL-2630).\n */}\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText={vocabulary.sitePickerHelperText}\n />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {vocabulary.intro}\n </Typography>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}>\n <Button variant=\"outlined\" component=\"label\" disabled={busy}>\n {'Choose file'}\n <input\n type=\"file\"\n accept=\".csv,text/csv\"\n hidden\n onChange={(event) => {\n void handleFile(event.target.files?.[0])\n event.target.value = ''\n }}\n />\n </Button>\n {/* The export's own header over no rows, so a sheet filled in\n against it maps itself. */}\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() =>\n downloadTextFile(\n vocabulary.templateFileName,\n 'text/csv',\n vocabulary.templateCsv(),\n )\n }\n >\n {'Download template'}\n </Button>\n {fileName ? (\n <Chip size=\"small\" variant=\"outlined\" label={fileName} />\n ) : null}\n {rows.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${rows.length.toLocaleString()} rows`}\n </Typography>\n ) : null}\n </Stack>\n {overCeiling > 0 ? (\n <Alert severity=\"warning\">\n {`This file has ${(rows.length + overCeiling).toLocaleString()} ` +\n `rows. Only the first ${vocabulary.maxRows.toLocaleString()} ` +\n 'are imported — split the file and import the rest ' +\n 'separately.'}\n </Alert>\n ) : null}\n {columns.length ? (\n <ImportMappingTable\n columns={columns}\n sample={rows[0] ?? []}\n mapping={mapping}\n vocabulary={vocabulary}\n onMap={handleMap}\n />\n ) : null}\n {columns.length && MappingSlot && vocabulary.collection ? (\n <MappingSlot\n slot=\"importMapping\"\n hostId={hostId}\n orgId={orgId ?? undefined}\n collection={vocabulary.collection}\n columns={shapedColumns}\n mapping={mapping}\n proposeMapping={(proposed: Readonly<Record<number, string>>) => proposeMapping(proposed)}\n />\n ) : null}\n {columns.length > 0 && !requiredMapped ? (\n <Alert severity=\"warning\">{vocabulary.requiredWarning}</Alert>\n ) : null}\n {columns.length > 0 && requiredMapped ? (\n <ImportPreviewTable\n targets={mappedTargets}\n preview={preview}\n total={rows.length}\n unusable={unusableRows}\n vocabulary={vocabulary}\n />\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <Button\n variant=\"contained\"\n // Held until the capturing site is known.\n disabled={busy || !rows.length || !requiredMapped || !createHostId}\n onClick={() => void handleImport()}\n >\n {rows.length\n ? `Import ${rows.length.toLocaleString()} rows`\n : 'Import'}\n </Button>\n </Stack>\n </>\n )}\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nCsvImportDrawer.displayName = 'CsvImportDrawer'\n\n/**\n * WHICH COLUMN IS WHICH.\n *\n * One row per column in the file: its header, the first row's value under\n * it so the operator can tell \"Title\" the job from \"Title\" the honorific,\n * and a select naming where it goes. The proposal came from the alias\n * table; every row of this table is the operator's to overrule.\n */\nfunction ImportMappingTable<F extends string>(props: {\n columns: readonly string[]\n sample: readonly string[]\n mapping: CsvImportMapping<F>\n vocabulary: Pick<CsvImportVocabulary<F, never, never>, 'fields' | 'fieldLabels' | 'customFields'>\n onMap: (index: number, target: string) => void\n}) {\n const { columns, sample, mapping, vocabulary, onMap } = props\n return (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Columns'}</Typography>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Column'}</TableCell>\n <TableCell>{'First row'}</TableCell>\n <TableCell>{'Imports as'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {columns.map((column, index) => (\n <TableRow key={`${column}-${index}`}>\n <TableCell>{column || `Column ${index + 1}`}</TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\" noWrap>\n {String(sample[index] ?? '')}\n </Typography>\n </TableCell>\n <TableCell>\n <TextField\n select\n size=\"small\"\n value={mapping[index] ?? SKIP_TARGET}\n onChange={(event) => onMap(index, event.target.value)}\n sx={{ minWidth: 220 }}\n >\n <MenuItem value={SKIP_TARGET}>{'Do not import'}</MenuItem>\n {vocabulary.fields.map((field) => (\n <MenuItem key={field} value={field}>\n {vocabulary.fieldLabels[field]}\n </MenuItem>\n ))}\n {(vocabulary.customFields ?? []).map((field) => (\n <MenuItem key={field.key} value={customImportTarget(field.key)}>\n {`${field.label} (custom)`}\n </MenuItem>\n ))}\n </TextField>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Stack>\n )\n}\n\n/**\n * The first rows, as the mapping reads them.\n *\n * Verbatim cell values under the fields they were mapped to — not the\n * normalized ones, because normalization is the server's and a preview that\n * showed a cleaned phone number would be promising a result the route may\n * refuse. What it does say is how many rows have no usable required cell,\n * since that is the one refusal the operator can fix before sending.\n */\nfunction ImportPreviewTable<F extends string>(props: {\n targets: readonly string[]\n preview: readonly Record<string, unknown>[]\n total: number\n unusable: number\n vocabulary: Pick<\n CsvImportVocabulary<F, never, never>,\n 'fieldLabels' | 'customFields' | 'previewRows' | 'unusableNotice'\n >\n}) {\n const { targets, preview, total, unusable, vocabulary } = props\n const cell = (row: Record<string, unknown>, target: string) => {\n const customKey = customImportTargetKey(target)\n if (customKey) {\n return String((row['custom'] as Record<string, string> | undefined)?.[customKey] ?? '')\n }\n return String(row[target] ?? '')\n }\n return (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">\n {`Preview — first ${Math.min(preview.length, vocabulary.previewRows)} of ${total.toLocaleString()}`}\n </Typography>\n {unusable > 0 ? (\n <Alert severity=\"info\">{vocabulary.unusableNotice(unusable, total)}</Alert>\n ) : null}\n <Box>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {targets.map((target) => (\n <TableCell key={target}>{targetLabel(target, vocabulary)}</TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {preview.map((row, index) => (\n <TableRow key={index}>\n {targets.map((target) => (\n <TableCell key={target}>\n <Typography variant=\"body2\" noWrap sx={{ maxWidth: 240 }}>\n {cell(row, target)}\n </Typography>\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Box>\n </Stack>\n )\n}\n\n/** How far the run has got, and what it did. */\nfunction ImportResultPanel<F extends string, S extends ImportSkippedRow<string>>(props: {\n result: ImportChunkResult<S>\n progress: { sent: number; total: number } | null\n busy: boolean\n vocabulary: Pick<\n CsvImportVocabulary<F, never, S>,\n 'fieldLabels' | 'customFields' | 'skipLabels' | 'resultExtras'\n >\n onDownloadSkipped: () => void\n onAnother: () => void\n}) {\n const { result, progress, busy, vocabulary, onDownloadSkipped, onAnother } = props\n const percent = progress?.total\n ? Math.min((progress.sent / progress.total) * 100, 100)\n : 0\n const skippedByReason = useMemo(() => {\n const counts: Partial<Record<string, number>> = {}\n for (const entry of result.skipped) {\n counts[entry.reason] = (counts[entry.reason] ?? 0) + 1\n }\n return Object.entries(counts) as [S['reason'], number][]\n }, [result.skipped])\n const droppedFields = Object.entries(result.dropped).filter(([, count]) => count > 0)\n return (\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {busy\n ? `${(progress?.sent ?? 0).toLocaleString()} of ${(progress?.total ?? 0).toLocaleString()} rows sent.`\n : `Finished. ${result.created.toLocaleString()} added, ` +\n `${result.merged.toLocaleString()} updated, ` +\n `${result.skipped.length.toLocaleString()} skipped.`}\n </Typography>\n <LinearProgress\n variant=\"determinate\"\n value={percent}\n sx={{ borderRadius: 1 }}\n />\n {skippedByReason.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Skipped'}</TableCell>\n <TableCell align=\"right\">{'Rows'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {skippedByReason.map(([reason, count]) => (\n <TableRow key={reason}>\n <TableCell>{vocabulary.skipLabels[reason] ?? reason}</TableCell>\n <TableCell align=\"right\">{count}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : null}\n {droppedFields.length ? (\n <Alert severity=\"info\">\n <AlertTitle>{'Some values could not be read'}</AlertTitle>\n {'The rows were imported without them: ' +\n droppedFields\n .map(([field, count]) => `${droppedFieldLabel(field, vocabulary)} (${count})`)\n .join(', ') +\n '.'}\n </Alert>\n ) : null}\n {result.ownersUnresolved.length ? (\n <Alert severity=\"info\">\n {'No team member has these owner addresses, so those rows ' +\n `have no owner: ${result.ownersUnresolved.slice(0, 5).join(', ')}` +\n (result.ownersUnresolved.length > 5\n ? ` and ${result.ownersUnresolved.length - 5} more.`\n : '.')}\n </Alert>\n ) : null}\n {vocabulary.resultExtras?.(result)}\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n {result.skipped.length ? (\n <Button variant=\"outlined\" onClick={onDownloadSkipped}>\n {'Download skipped rows'}\n </Button>\n ) : null}\n {!busy ? (\n <Button variant=\"outlined\" onClick={onAnother}>\n {'Import another file'}\n </Button>\n ) : null}\n </Stack>\n </Stack>\n )\n}\n\n/**\n * An \"Import CSV\" button that mounts its drawer only once it has been\n * opened, so the list pays nothing — no hooks, no listeners — for a\n * feature the operator has not reached for.\n */\nexport function CsvImportButton(props: {\n label?: string\n children: (open: boolean, onClose: () => void) => ReactNode\n}) {\n const { label = 'Import CSV', children } = props\n const [open, setOpen] = useState(false)\n const [mounted, setMounted] = useState(false)\n const handleOpen = useCallback(() => {\n setMounted(true)\n setOpen(true)\n }, [])\n const handleClose = useCallback(() => setOpen(false), [])\n return (\n <>\n <Button size=\"small\" onClick={handleOpen}>\n {label}\n </Button>\n {mounted ? children(open, handleClose) : null}\n </>\n )\n}\nCsvImportButton.displayName = 'CsvImportButton'\n\nexport default CsvImportDrawer\n"],"names":["customImportTarget","customImportTargetKey","LIST_IMPORT_MAX_CHARACTERS","parseCsv","ICON_VARIANT_CLOSE","Container","HelpTip","MdiIcon","SrOnly","NavigationDrawerComponent","ScrollTable","useSnackbar","authorizedFetch","useOrgDataScope","useUser","useConsoleWidgetSlot","Alert","AlertTitle","Box","Button","Chip","IconButton","LinearProgress","MenuItem","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","useCallback","useEffect","useMemo","useRef","useState","useCrmOrgMount","downloadTextFile","importColumns","CrmSitePicker","SKIP_TARGET","targetLabel","target","vocabulary","customKey","customFields","find","field","key","label","fieldLabels","droppedFieldLabel","replace","CsvImportDrawer","props","hostId","rows","open","onClose","data","user","enqueueSnackbar","mount","createHostId","fileName","setFileName","columns","setColumns","setRows","overCeiling","setOverCeiling","mapping","setMapping","result","setResult","progress","setProgress","busy","setBusy","abandoned","current","reset","handleClose","handleFile","file","contents","text","catch","length","variant","allowDuplicate","table","filter","cells","some","cell","String","trim","header","map","body","slice","name","maxRows","Math","max","guessMapping","requiredMapped","Object","values","includes","requiredField","unusableRows","at","Number","entries","unusable","preview","previewRows","mapRow","mappedTargets","sort","a","b","MappingSlot","orgId","undefined","shapedColumns","offeredTargets","Set","fields","proposeMapping","proposed","taken","next","column","index","isInteger","has","add","handleMap","previous","existing","handleImport","total","emptyResult","sent","offset","chunkSize","chunk","response","route","method","headers","JSON","stringify","payload","json","ok","error","mergeResults","min","persist","handleDownloadSkipped","skipped","skippedFileName","skippedCsv","entry","reason","finished","anchor","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","title","help","appBarRight","gutterY","spacing","ImportResultPanel","onDownloadSkipped","onAnother","disabled","helperText","sitePickerHelperText","intro","direction","alignItems","flexWrap","rowGap","input","type","accept","hidden","onChange","event","files","value","size","templateFileName","templateCsv","toLocaleString","severity","ImportMappingTable","sample","onMap","collection","slot","requiredWarning","ImportPreviewTable","targets","justifyContent","displayName","noWrap","select","minWidth","row","unusableNotice","maxWidth","percent","skippedByReason","counts","droppedFields","dropped","count","created","merged","borderRadius","align","skipLabels","join","ownersUnresolved","resultExtras","CsvImportButton","children","setOpen","mounted","setMounted","handleOpen"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/csv-import-drawer.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\n/**\n * IMPORTING A CSV INTO A CRM COLLECTION — the screen, for whichever\n * collection (AGL-2602, shared since AGL-2621).\n *\n * The contact import settled the walk from a file on disk to a result the\n * operator can act on: choose the file, read what is in it, match its\n * columns to fields with the first row beside each, preview the ten rows\n * the mapping produces, run it in bounded requests with a progress bar,\n * and report what was added, updated and skipped with the skipped rows as\n * a file to fix. A companies file walks the same path with a different\n * vocabulary, so the walk lives here once and each collection hands in a\n * {@link CsvImportVocabulary}: its fields and their labels, the one field\n * every row needs, its route, its tally, its template.\n *\n * ## The file is read in the browser, and judged on the server\n *\n * `parseCsv` — the same parser the list import and the dataset import run\n * — turns the file into cells here, so the operator can map and preview\n * without a round trip and without uploading a file they may still decide\n * not to import. Nothing is VALIDATED here beyond counting the rows whose\n * required cell is unusable, and that count is a courtesy: every row is\n * judged by the route, through the normalizers every other door uses, and\n * the numbers on the result panel are the server's.\n *\n * ## Bounded requests, and closing stops them\n *\n * A chunk per request, as many requests as the file needs, and a progress\n * bar between them. Closing the drawer mid-run stops the loop after the\n * request in flight; the rows already written stay written and the result\n * panel says how far it got. There is no durable job to resume — an import\n * is idempotent by construction (the door dedupes on the address, or the\n * domain and the name), so running the same file again finishes what a\n * closed drawer left, reporting the earlier rows as merged.\n */\n\nimport {\n customImportTarget,\n customImportTargetKey,\n type ImportChunkResult,\n type ImportSkippedRow,\n LIST_IMPORT_MAX_CHARACTERS,\n parseCsv,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, HelpTip, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useOrgDataScope, useUser } from '@aglyn/tenant-feature-instance'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n Alert,\n AlertTitle,\n Box,\n Button,\n Chip,\n IconButton,\n LinearProgress,\n MenuItem,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n type ComponentProps,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { importColumns } from '../model/import-column-shapes'\nimport { CrmSitePicker } from './crm-site-picker'\n\n/** A column's target: a standard field, or a custom one as `custom:<key>`. */\nexport type CsvImportTarget<F extends string> = F | `custom:${string}`\n\n/** Column index → target. A column absent from the map is not imported. */\nexport type CsvImportMapping<F extends string> = Record<number, CsvImportTarget<F>>\n\n/** A custom field a column may be mapped to, when the collection has any. */\nexport interface CsvImportCustomField {\n key: string\n label: string\n}\n\n/**\n * Everything one collection's import says for itself. Pure values and pure\n * functions, so a vocabulary is built once outside React and handed in.\n */\nexport interface CsvImportVocabulary<\n F extends string,\n R extends Record<string, unknown>,\n S extends ImportSkippedRow<string>,\n> {\n /** The drawer's title — \"Import contacts from CSV\". */\n title: string\n /**\n * What the import is called where the `importMapping` zone names it —\n * `contacts`, `companies`, `deals`, `leads` (AGL-2917). Absent, the drawer\n * hosts no zone.\n */\n collection?: string\n help: ComponentProps<typeof HelpTip>\n /**\n * What the site picker's answer decides, said under it at the\n * organization level (AGL-2630). Absent, the picker's own sentence.\n */\n sitePickerHelperText?: string\n /** The paragraph under the title. */\n intro: string\n /** The standard targets, in the order the mapping menu lists them. */\n fields: readonly F[]\n fieldLabels: Record<F, string>\n /** Custom targets, offered after the standard ones; none for most collections. */\n customFields?: readonly CsvImportCustomField[]\n /** The one field every row needs, and what the drawer says when it is unmapped. */\n requiredField: F\n requiredWarning: string\n /**\n * Whether a required cell is one the server would refuse — the courtesy\n * count the preview shows. Absent, only a blank cell counts.\n */\n unusable?: (cell: string) => boolean\n /** The preview's sentence about them. */\n unusableNotice: (count: number, total: number) => string\n guessMapping: (columns: readonly string[]) => CsvImportMapping<F>\n mapRow: (cells: readonly string[], mapping: CsvImportMapping<F>) => R\n /** The browser-side URL of the route one chunk is posted to. */\n route: string\n maxRows: number\n chunkSize: number\n previewRows: number\n emptyResult: () => ImportChunkResult<S>\n mergeResults: (\n total: ImportChunkResult<S>,\n chunk: ImportChunkResult<S>,\n offset: number,\n ) => ImportChunkResult<S>\n skipLabels: Record<S['reason'], string>\n skippedCsv: (\n columns: readonly string[],\n entries: readonly { cells: readonly string[]; reason: S['reason'] }[],\n ) => string\n skippedFileName: string\n /** The export's header over no rows. */\n templateCsv: () => string\n templateFileName: string\n /** The result panel's collection-specific lines, when there are any. */\n resultExtras?: (result: ImportChunkResult<S>) => ReactNode\n /**\n * A collection-specific warning about the mapped file, shown above the\n * preview before anything is sent — a value the server will refuse that\n * the operator can fix first. Given every row as `mapRow` reads it;\n * answers nothing when the file is fine.\n */\n rowsNotice?: (rows: readonly R[]) => ReactNode\n}\n\nexport interface CsvImportDrawerProps<\n F extends string,\n R extends Record<string, unknown>,\n S extends ImportSkippedRow<string>,\n> {\n open: boolean\n onClose: () => void\n /**\n * The site the file is imported INTO — what the route files every row\n * under — or `null` at the organization level (AGL-2630), where the\n * drawer asks which site with a picker and holds the import until one is\n * named.\n */\n hostId: string | null\n vocabulary: CsvImportVocabulary<F, R, S>\n}\n\n/** The value the mapping select shows for a column that is not imported. */\nconst SKIP_TARGET = ''\n\n/** What a target is called — a standard field's label, or the custom field's. */\nfunction targetLabel<F extends string>(\n target: string,\n vocabulary: Pick<CsvImportVocabulary<F, never, never>, 'fieldLabels' | 'customFields'>,\n): string {\n const customKey = customImportTargetKey(target)\n if (customKey) {\n return vocabulary.customFields?.find((field) => field.key === customKey)?.label ?? customKey\n }\n return vocabulary.fieldLabels[target as F] ?? target\n}\n\n/** What a dropped-value field is called in the result panel — the label without its hint. */\nfunction droppedFieldLabel<F extends string>(\n field: string,\n vocabulary: Pick<CsvImportVocabulary<F, never, never>, 'fieldLabels' | 'customFields'>,\n): string {\n return targetLabel(field, vocabulary).replace(/ \\(.*\\)$/, '')\n}\n\nexport function CsvImportDrawer<\n F extends string,\n R extends Record<string, unknown>,\n S extends ImportSkippedRow<string>,\n>(props: CsvImportDrawerProps<F, R, S>) {\n const { open, onClose, hostId, vocabulary } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n // The site every row is captured under: the mounted one, or at the\n // organization level the one the picker named — `null` until it has.\n const mount = useCrmOrgMount()\n const createHostId = hostId ?? mount?.createHostId ?? null\n\n const [fileName, setFileName] = useState('')\n const [columns, setColumns] = useState<string[]>([])\n const [rows, setRows] = useState<string[][]>([])\n /** How many rows the file had past the ceiling, so the drawer can say so. */\n const [overCeiling, setOverCeiling] = useState(0)\n const [mapping, setMapping] = useState<CsvImportMapping<F>>({})\n const [result, setResult] = useState<ImportChunkResult<S> | null>(null)\n const [progress, setProgress] = useState<{ sent: number; total: number } | null>(\n null,\n )\n const [busy, setBusy] = useState(false)\n /**\n * Set when the drawer closes mid-run, read by the loop.\n *\n * A ref and not state, for the reason the list import gives: the loop is\n * an `async` function that captured its closure when it started, and a\n * state value it read would be the one from that render.\n */\n const abandoned = useRef(false)\n\n useEffect(() => {\n if (open) abandoned.current = false\n }, [open])\n\n const reset = useCallback(() => {\n setFileName('')\n setColumns([])\n setRows([])\n setOverCeiling(0)\n setMapping({})\n setResult(null)\n setProgress(null)\n }, [])\n\n const handleClose = useCallback(() => {\n abandoned.current = true\n onClose()\n }, [onClose])\n\n const handleFile = useCallback(\n async (file: File | null | undefined) => {\n if (!file) return\n const contents = await file.text().catch(() => '')\n if (contents.length > LIST_IMPORT_MAX_CHARACTERS) {\n return void enqueueSnackbar(\n 'That file is too large to read in one go. Split it and import the ' +\n 'pieces.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n const table = parseCsv(contents).filter((cells) =>\n cells.some((cell) => String(cell ?? '').trim()),\n )\n if (table.length < 2) {\n return void enqueueSnackbar(\n 'That file needs a header row and at least one row under it.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n const header = table[0].map((cell) => String(cell ?? '').trim())\n const body = table.slice(1)\n setFileName(file.name)\n setColumns(header)\n setRows(body.slice(0, vocabulary.maxRows))\n setOverCeiling(Math.max(0, body.length - vocabulary.maxRows))\n setMapping(vocabulary.guessMapping(header))\n setResult(null)\n setProgress(null)\n },\n [enqueueSnackbar, vocabulary],\n )\n\n const requiredMapped = Object.values(mapping).includes(vocabulary.requiredField)\n /** Rows whose required cell the server would refuse. */\n const unusableRows = useMemo(() => {\n const at = Number(\n Object.entries(mapping).find(\n ([, target]) => target === vocabulary.requiredField,\n )?.[0] ?? -1,\n )\n if (at < 0) return rows.length\n const unusable = vocabulary.unusable ?? ((cell: string) => !cell.trim())\n return rows.filter((cells) => unusable(String(cells[at] ?? ''))).length\n }, [mapping, rows, vocabulary])\n\n const preview = useMemo(\n () =>\n rows\n .slice(0, vocabulary.previewRows)\n .map((cells) => vocabulary.mapRow(cells, mapping)),\n [rows, mapping, vocabulary],\n )\n // The collection's own warning over the whole mapped file, when it has one.\n const rowsNotice = useMemo(\n () =>\n vocabulary.rowsNotice && rows.length\n ? vocabulary.rowsNotice(rows.map((cells) => vocabulary.mapRow(cells, mapping)))\n : null,\n [rows, mapping, vocabulary],\n )\n const mappedTargets = useMemo(\n () =>\n Object.entries(mapping)\n .sort(([a], [b]) => Number(a) - Number(b))\n .map(([, target]) => target),\n [mapping],\n )\n\n /*\n * The `importMapping` zone (AGL-2917), hosted through the shell's renderer:\n * a widget there reads each column's header and shape, never a cell, and\n * proposes a matching, which replaces the table's above for the preview to\n * show. A target this import does not offer, or one a column before it\n * already took, is dropped, as the table itself would refuse it.\n */\n const MappingSlot = useConsoleWidgetSlot()\n const { orgId } = useOrgDataScope({ hostId: hostId ?? undefined, orgId: mount?.orgId })\n const shapedColumns = useMemo(() => importColumns(columns, rows), [columns, rows])\n const offeredTargets = useMemo(\n () =>\n new Set<string>([\n ...vocabulary.fields,\n ...(vocabulary.customFields ?? []).map((field) => customImportTarget(field.key)),\n ]),\n [vocabulary],\n )\n const proposeMapping = useCallback(\n (proposed: Readonly<Record<number, string>>) => {\n const taken = new Set<string>()\n const next: CsvImportMapping<F> = {}\n for (const [column, target] of Object.entries(proposed).sort(([a], [b]) => Number(a) - Number(b))) {\n const index = Number(column)\n if (!Number.isInteger(index) || index < 0 || index >= columns.length) continue\n if (!offeredTargets.has(target) || taken.has(target)) continue\n taken.add(target)\n next[index] = target as CsvImportTarget<F>\n }\n setMapping(next)\n },\n [offeredTargets, columns.length],\n )\n\n const handleMap = useCallback((index: number, target: string) => {\n setMapping((previous) => {\n const next: CsvImportMapping<F> = {}\n for (const [column, existing] of Object.entries(previous)) {\n // One column per target: choosing a target already taken moves it\n // here rather than importing one field from two columns.\n if (Number(column) !== index && existing !== target) {\n next[Number(column)] = existing\n }\n }\n if (target !== SKIP_TARGET) next[index] = target as CsvImportTarget<F>\n return next\n })\n }, [])\n\n /**\n * Runs the file, one bounded request at a time.\n *\n * Every chunk's result is folded into the running total as it arrives,\n * so a file that stops halfway — a lost connection, a closed drawer —\n * still reports what it did rather than nothing.\n */\n const handleImport = useCallback(async () => {\n if (busy || !rows.length || !requiredMapped || !createHostId) return\n setBusy(true)\n let total = vocabulary.emptyResult()\n setResult(total)\n setProgress({ sent: 0, total: rows.length })\n try {\n for (let offset = 0; offset < rows.length; offset += vocabulary.chunkSize) {\n if (abandoned.current) return\n const chunk = rows\n .slice(offset, offset + vocabulary.chunkSize)\n .map((cells) => vocabulary.mapRow(cells, mapping))\n const response = await authorizedFetch(user, vocabulary.route, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId: createHostId, rows: chunk }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(\n payload?.error ?? 'The import could not continue.',\n { variant: 'warning', allowDuplicate: true },\n )\n }\n total = vocabulary.mergeResults(total, payload as ImportChunkResult<S>, offset)\n setResult(total)\n setProgress({ sent: Math.min(offset + chunk.length, rows.length), total: rows.length })\n }\n enqueueSnackbar('Import finished', { variant: 'success', persist: false })\n } catch {\n enqueueSnackbar('The import could not continue.', {\n variant: 'warning',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [busy, rows, requiredMapped, mapping, user, createHostId, enqueueSnackbar, vocabulary])\n\n const handleDownloadSkipped = useCallback(() => {\n if (!result?.skipped.length) return\n downloadTextFile(\n vocabulary.skippedFileName,\n 'text/csv',\n vocabulary.skippedCsv(\n columns,\n result.skipped.map((entry) => ({\n cells: rows[entry.index] ?? [],\n reason: entry.reason as S['reason'],\n })),\n ),\n )\n }, [result, columns, rows, vocabulary])\n\n const finished = !!result && !busy\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={handleClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={handleClose}\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 {vocabulary.title}\n </Typography>\n <HelpTip {...vocabulary.help} />\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={handleClose}>\n {finished ? 'Done' : 'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {result ? (\n <ImportResultPanel\n result={result}\n progress={progress}\n busy={busy}\n vocabulary={vocabulary}\n onDownloadSkipped={handleDownloadSkipped}\n onAnother={reset}\n />\n ) : (\n <>\n {/*\n First, because every row is filed under the answer: the site\n the file is imported into. Nothing under a site (AGL-2630).\n */}\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText={vocabulary.sitePickerHelperText}\n />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {vocabulary.intro}\n </Typography>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}>\n <Button variant=\"outlined\" component=\"label\" disabled={busy}>\n {'Choose file'}\n <input\n type=\"file\"\n accept=\".csv,text/csv\"\n hidden\n onChange={(event) => {\n void handleFile(event.target.files?.[0])\n event.target.value = ''\n }}\n />\n </Button>\n {/* The export's own header over no rows, so a sheet filled in\n against it maps itself. */}\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() =>\n downloadTextFile(\n vocabulary.templateFileName,\n 'text/csv',\n vocabulary.templateCsv(),\n )\n }\n >\n {'Download template'}\n </Button>\n {fileName ? (\n <Chip size=\"small\" variant=\"outlined\" label={fileName} />\n ) : null}\n {rows.length ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${rows.length.toLocaleString()} rows`}\n </Typography>\n ) : null}\n </Stack>\n {overCeiling > 0 ? (\n <Alert severity=\"warning\">\n {`This file has ${(rows.length + overCeiling).toLocaleString()} ` +\n `rows. Only the first ${vocabulary.maxRows.toLocaleString()} ` +\n 'are imported — split the file and import the rest ' +\n 'separately.'}\n </Alert>\n ) : null}\n {columns.length ? (\n <ImportMappingTable\n columns={columns}\n sample={rows[0] ?? []}\n mapping={mapping}\n vocabulary={vocabulary}\n onMap={handleMap}\n />\n ) : null}\n {columns.length && MappingSlot && vocabulary.collection ? (\n <MappingSlot\n slot=\"importMapping\"\n hostId={hostId}\n orgId={orgId ?? undefined}\n collection={vocabulary.collection}\n columns={shapedColumns}\n mapping={mapping}\n proposeMapping={(proposed: Readonly<Record<number, string>>) => proposeMapping(proposed)}\n />\n ) : null}\n {columns.length > 0 && !requiredMapped ? (\n <Alert severity=\"warning\">{vocabulary.requiredWarning}</Alert>\n ) : null}\n {columns.length > 0 && requiredMapped ? rowsNotice : null}\n {columns.length > 0 && requiredMapped ? (\n <ImportPreviewTable\n targets={mappedTargets}\n preview={preview}\n total={rows.length}\n unusable={unusableRows}\n vocabulary={vocabulary}\n />\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n <Button\n variant=\"contained\"\n // Held until the capturing site is known.\n disabled={busy || !rows.length || !requiredMapped || !createHostId}\n onClick={() => void handleImport()}\n >\n {rows.length\n ? `Import ${rows.length.toLocaleString()} rows`\n : 'Import'}\n </Button>\n </Stack>\n </>\n )}\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nCsvImportDrawer.displayName = 'CsvImportDrawer'\n\n/**\n * WHICH COLUMN IS WHICH.\n *\n * One row per column in the file: its header, the first row's value under\n * it so the operator can tell \"Title\" the job from \"Title\" the honorific,\n * and a select naming where it goes. The proposal came from the alias\n * table; every row of this table is the operator's to overrule.\n */\nfunction ImportMappingTable<F extends string>(props: {\n columns: readonly string[]\n sample: readonly string[]\n mapping: CsvImportMapping<F>\n vocabulary: Pick<CsvImportVocabulary<F, never, never>, 'fields' | 'fieldLabels' | 'customFields'>\n onMap: (index: number, target: string) => void\n}) {\n const { columns, sample, mapping, vocabulary, onMap } = props\n return (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{'Columns'}</Typography>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Column'}</TableCell>\n <TableCell>{'First row'}</TableCell>\n <TableCell>{'Imports as'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {columns.map((column, index) => (\n <TableRow key={`${column}-${index}`}>\n <TableCell>{column || `Column ${index + 1}`}</TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\" noWrap>\n {String(sample[index] ?? '')}\n </Typography>\n </TableCell>\n <TableCell>\n <TextField\n select\n size=\"small\"\n value={mapping[index] ?? SKIP_TARGET}\n onChange={(event) => onMap(index, event.target.value)}\n sx={{ minWidth: 220 }}\n >\n <MenuItem value={SKIP_TARGET}>{'Do not import'}</MenuItem>\n {vocabulary.fields.map((field) => (\n <MenuItem key={field} value={field}>\n {vocabulary.fieldLabels[field]}\n </MenuItem>\n ))}\n {(vocabulary.customFields ?? []).map((field) => (\n <MenuItem key={field.key} value={customImportTarget(field.key)}>\n {`${field.label} (custom)`}\n </MenuItem>\n ))}\n </TextField>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Stack>\n )\n}\n\n/**\n * The first rows, as the mapping reads them.\n *\n * Verbatim cell values under the fields they were mapped to — not the\n * normalized ones, because normalization is the server's and a preview that\n * showed a cleaned phone number would be promising a result the route may\n * refuse. What it does say is how many rows have no usable required cell,\n * since that is the one refusal the operator can fix before sending.\n */\nfunction ImportPreviewTable<F extends string>(props: {\n targets: readonly string[]\n preview: readonly Record<string, unknown>[]\n total: number\n unusable: number\n vocabulary: Pick<\n CsvImportVocabulary<F, never, never>,\n 'fieldLabels' | 'customFields' | 'previewRows' | 'unusableNotice'\n >\n}) {\n const { targets, preview, total, unusable, vocabulary } = props\n const cell = (row: Record<string, unknown>, target: string) => {\n const customKey = customImportTargetKey(target)\n if (customKey) {\n return String((row['custom'] as Record<string, string> | undefined)?.[customKey] ?? '')\n }\n return String(row[target] ?? '')\n }\n return (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">\n {`Preview — first ${Math.min(preview.length, vocabulary.previewRows)} of ${total.toLocaleString()}`}\n </Typography>\n {unusable > 0 ? (\n <Alert severity=\"info\">{vocabulary.unusableNotice(unusable, total)}</Alert>\n ) : null}\n <Box>\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n {targets.map((target) => (\n <TableCell key={target}>{targetLabel(target, vocabulary)}</TableCell>\n ))}\n </TableRow>\n </TableHead>\n <TableBody>\n {preview.map((row, index) => (\n <TableRow key={index}>\n {targets.map((target) => (\n <TableCell key={target}>\n <Typography variant=\"body2\" noWrap sx={{ maxWidth: 240 }}>\n {cell(row, target)}\n </Typography>\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n </Box>\n </Stack>\n )\n}\n\n/** How far the run has got, and what it did. */\nfunction ImportResultPanel<F extends string, S extends ImportSkippedRow<string>>(props: {\n result: ImportChunkResult<S>\n progress: { sent: number; total: number } | null\n busy: boolean\n vocabulary: Pick<\n CsvImportVocabulary<F, never, S>,\n 'fieldLabels' | 'customFields' | 'skipLabels' | 'resultExtras'\n >\n onDownloadSkipped: () => void\n onAnother: () => void\n}) {\n const { result, progress, busy, vocabulary, onDownloadSkipped, onAnother } = props\n const percent = progress?.total\n ? Math.min((progress.sent / progress.total) * 100, 100)\n : 0\n const skippedByReason = useMemo(() => {\n const counts: Partial<Record<string, number>> = {}\n for (const entry of result.skipped) {\n counts[entry.reason] = (counts[entry.reason] ?? 0) + 1\n }\n return Object.entries(counts) as [S['reason'], number][]\n }, [result.skipped])\n const droppedFields = Object.entries(result.dropped).filter(([, count]) => count > 0)\n return (\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {busy\n ? `${(progress?.sent ?? 0).toLocaleString()} of ${(progress?.total ?? 0).toLocaleString()} rows sent.`\n : `Finished. ${result.created.toLocaleString()} added, ` +\n `${result.merged.toLocaleString()} updated, ` +\n `${result.skipped.length.toLocaleString()} skipped.`}\n </Typography>\n <LinearProgress\n variant=\"determinate\"\n value={percent}\n sx={{ borderRadius: 1 }}\n />\n {skippedByReason.length ? (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell>{'Skipped'}</TableCell>\n <TableCell align=\"right\">{'Rows'}</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {skippedByReason.map(([reason, count]) => (\n <TableRow key={reason}>\n <TableCell>{vocabulary.skipLabels[reason] ?? reason}</TableCell>\n <TableCell align=\"right\">{count}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n ) : null}\n {droppedFields.length ? (\n <Alert severity=\"info\">\n <AlertTitle>{'Some values could not be read'}</AlertTitle>\n {'The rows were imported without them: ' +\n droppedFields\n .map(([field, count]) => `${droppedFieldLabel(field, vocabulary)} (${count})`)\n .join(', ') +\n '.'}\n </Alert>\n ) : null}\n {result.ownersUnresolved.length ? (\n <Alert severity=\"info\">\n {'No team member has these owner addresses, so those rows ' +\n `have no owner: ${result.ownersUnresolved.slice(0, 5).join(', ')}` +\n (result.ownersUnresolved.length > 5\n ? ` and ${result.ownersUnresolved.length - 5} more.`\n : '.')}\n </Alert>\n ) : null}\n {vocabulary.resultExtras?.(result)}\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n {result.skipped.length ? (\n <Button variant=\"outlined\" onClick={onDownloadSkipped}>\n {'Download skipped rows'}\n </Button>\n ) : null}\n {!busy ? (\n <Button variant=\"outlined\" onClick={onAnother}>\n {'Import another file'}\n </Button>\n ) : null}\n </Stack>\n </Stack>\n )\n}\n\n/**\n * An \"Import CSV\" button that mounts its drawer only once it has been\n * opened, so the list pays nothing — no hooks, no listeners — for a\n * feature the operator has not reached for.\n */\nexport function CsvImportButton(props: {\n label?: string\n children: (open: boolean, onClose: () => void) => ReactNode\n}) {\n const { label = 'Import CSV', children } = props\n const [open, setOpen] = useState(false)\n const [mounted, setMounted] = useState(false)\n const handleOpen = useCallback(() => {\n setMounted(true)\n setOpen(true)\n }, [])\n const handleClose = useCallback(() => setOpen(false), [])\n return (\n <>\n <Button size=\"small\" onClick={handleOpen}>\n {label}\n </Button>\n {mounted ? children(open, handleClose) : null}\n </>\n )\n}\nCsvImportButton.displayName = 'CsvImportButton'\n\nexport default CsvImportDrawer\n"],"names":["customImportTarget","customImportTargetKey","LIST_IMPORT_MAX_CHARACTERS","parseCsv","ICON_VARIANT_CLOSE","Container","HelpTip","MdiIcon","SrOnly","NavigationDrawerComponent","ScrollTable","useSnackbar","authorizedFetch","useOrgDataScope","useUser","useConsoleWidgetSlot","Alert","AlertTitle","Box","Button","Chip","IconButton","LinearProgress","MenuItem","Stack","TableBody","TableCell","TableHead","TableRow","TextField","Typography","useCallback","useEffect","useMemo","useRef","useState","useCrmOrgMount","downloadTextFile","importColumns","CrmSitePicker","SKIP_TARGET","targetLabel","target","vocabulary","customKey","customFields","find","field","key","label","fieldLabels","droppedFieldLabel","replace","CsvImportDrawer","props","hostId","rows","open","onClose","data","user","enqueueSnackbar","mount","createHostId","fileName","setFileName","columns","setColumns","setRows","overCeiling","setOverCeiling","mapping","setMapping","result","setResult","progress","setProgress","busy","setBusy","abandoned","current","reset","handleClose","handleFile","file","contents","text","catch","length","variant","allowDuplicate","table","filter","cells","some","cell","String","trim","header","map","body","slice","name","maxRows","Math","max","guessMapping","requiredMapped","Object","values","includes","requiredField","unusableRows","at","Number","entries","unusable","preview","previewRows","mapRow","rowsNotice","mappedTargets","sort","a","b","MappingSlot","orgId","undefined","shapedColumns","offeredTargets","Set","fields","proposeMapping","proposed","taken","next","column","index","isInteger","has","add","handleMap","previous","existing","handleImport","total","emptyResult","sent","offset","chunkSize","chunk","response","route","method","headers","JSON","stringify","payload","json","ok","error","mergeResults","min","persist","handleDownloadSkipped","skipped","skippedFileName","skippedCsv","entry","reason","finished","anchor","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","title","help","appBarRight","gutterY","spacing","ImportResultPanel","onDownloadSkipped","onAnother","disabled","helperText","sitePickerHelperText","intro","direction","alignItems","flexWrap","rowGap","input","type","accept","hidden","onChange","event","files","value","size","templateFileName","templateCsv","toLocaleString","severity","ImportMappingTable","sample","onMap","collection","slot","requiredWarning","ImportPreviewTable","targets","justifyContent","displayName","noWrap","select","minWidth","row","unusableNotice","maxWidth","percent","skippedByReason","counts","droppedFields","dropped","count","created","merged","borderRadius","align","skipLabels","join","ownersUnresolved","resultExtras","CsvImportButton","children","setOpen","mounted","setMounted","handleOpen"],"mappings":"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"}
|
|
@@ -30,6 +30,7 @@ import { useCallback, useState } from "react";
|
|
|
30
30
|
import { useContactFieldDefinitions } from "../hooks/use-contact-field-definitions.js";
|
|
31
31
|
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
32
32
|
import ContactFieldDrawer from "./contact-field-drawer.js";
|
|
33
|
+
import { LeadSourceValuesCard } from "./lead-source-values-card.js";
|
|
33
34
|
import { recomputeAllCrmNextActivity } from "../model/next-activity-api.js";
|
|
34
35
|
import { crmTaskCallScope } from "../model/task-routes.js";
|
|
35
36
|
/** What each tab says the fields are for, above its list. */ const OBJECT_INTRO = {
|
|
@@ -624,6 +625,11 @@ import { crmTaskCallScope } from "../model/task-routes.js";
|
|
|
624
625
|
variant: "caption",
|
|
625
626
|
color: "text.secondary",
|
|
626
627
|
children: `How many ${noun}s carry a value under each field is not ` + `counted — that would read every ${noun} in the organization ` + 'each time this page opened.'
|
|
628
|
+
}) : null,
|
|
629
|
+
object === 'lead' && scope ? /*#__PURE__*/ _jsx(LeadSourceValuesCard, {
|
|
630
|
+
hostId: hostId,
|
|
631
|
+
orgId: orgId,
|
|
632
|
+
createHostId: createHostId
|
|
627
633
|
}) : null
|
|
628
634
|
]
|
|
629
635
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/fields-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 {\n CONTACT_FIELD_TYPE_LABELS,\n type ConsolePluginPageProps,\n createResourceUid,\n CRM_COLLECTIONS,\n CRM_FIELD_OBJECT_LABELS,\n CRM_FIELD_OBJECTS,\n type CrmFieldObject,\n isCrmFieldObject,\n newResourceScopeFields,\n ORG_SCOPE_TOKEN,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport {\n mdiArchiveArrowUpOutline,\n mdiArchiveOutline,\n mdiArrowDown,\n mdiArrowUp,\n mdiDeleteOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n CardDisplay,\n MdiIcon,\n SrOnly,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Chip,\n IconButton,\n Stack,\n Tab,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Tabs,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n deleteDoc,\n doc,\n setDoc,\n updateDoc,\n writeBatch,\n} from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport {\n type ContactFieldDefinitionDoc,\n useContactFieldDefinitions,\n} from '../hooks/use-contact-field-definitions'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport ContactFieldDrawer, { type ContactFieldDraft } from './contact-field-drawer'\nimport { recomputeAllCrmNextActivity } from '../model/next-activity-api'\nimport { crmTaskCallScope } from '../model/task-routes'\n\nexport type ContactsFieldsSectionProps = Pick<ConsolePluginPageProps, 'hostId' | 'org'>\n\n/** What each tab says the fields are for, above its list. */\nconst OBJECT_INTRO: Record<CrmFieldObject, string> = {\n contact:\n 'The custom fields on a contact — text, number, date, choice, ' +\n 'checkbox or link. They show on every contact, as columns on the ' +\n 'list, and a form field can save into one.',\n company:\n 'The custom fields on a company. They show on every company’s page ' +\n 'and its edit form, as columns on the companies list, and a CSV ' +\n 'import can fill them.',\n deal:\n 'The custom fields on a deal. They show on every deal’s page and ' +\n 'its edit form, and as columns on the deals table.',\n lead:\n 'The custom fields on a lead. They show on every lead’s page and the ' +\n 'New lead drawer, and as columns on the leads list. They stay on the ' +\n 'lead when it converts — a contact’s fields are its own.',\n}\n\n/** The noun each tab's empty state and captions use. */\nconst OBJECT_NOUN: Record<CrmFieldObject, string> = {\n contact: 'contact',\n company: 'company',\n deal: 'deal',\n lead: 'lead',\n}\n\n/**\n * `/crm/fields` — the custom fields a holder keeps on a person (AGL-2601),\n * since AGL-2661 on a company and a deal too, and since AGL-3272 on a lead:\n * one tab each, and every CRM record describable in the org's own words.\n *\n * Definitions live in `orgs/{orgId}/contactFields`, one document per field,\n * and the VALUES live under each contact facet's `custom` keyed by the\n * definition's `key`. That split decides everything this section does:\n *\n * - A field is RETIRED, not deleted, while values may exist under it. A\n * retired field leaves every form and every column, but an export can\n * still read what was written, and a restore brings it back intact.\n * Delete is offered only on a retired field, because retiring is the step\n * where the author had to look at what they were losing.\n * - The KEY never changes. It is the map key every value sits under; a\n * rename is a new field and a retire.\n * - Order is a stored `order` on each document, moved with the arrows here,\n * and every reader sorts by it — the profile card, the columns, an export.\n *\n * ## What is NOT on this page\n *\n * How many contacts carry a value under each field. That count is a read of\n * every contact document in the org — the expensive-read shape this codebase\n * has a standing rule against — and it would be paid on every visit to a\n * settings page. The list says so rather than showing a number it cannot\n * afford to keep true.\n *\n * Definitions are ORG-WIDE (`visibleTo: ['org']`): a field is a fact about\n * how this business describes people, and every site in the org files\n * people into the same address book. `hostId` records which site defined it.\n */\nexport function ContactsFieldsSection(props: ContactsFieldsSectionProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The org root from the one scope hook (AGL-2614), which also answers at\n // the organization level (AGL-2630). A definition is org-wide either way;\n // the defining site it records is the mounted one, or the picked one, or\n // none for a field defined over the whole org before any pick.\n const { scope, ready: scopeReady, createHostId } = useCrmScope({ hostId, org })\n const orgId = scope?.[1] ?? null\n /*\n * ONE TAB PER OBJECT (AGL-2661). The list, the drawer and every write\n * below are about the tab's object: the hook narrows the org's one\n * definition list to it, a new field is stamped with it, and the drawer\n * refuses a key only against the keys THIS object already uses — a\n * company and a contact may both define `region`.\n */\n const [object, setObject] = useState<CrmFieldObject>('contact')\n const { definitions, ready, fromCache } = useContactFieldDefinitions(orgId, object)\n const noun = OBJECT_NOUN[object]\n\n /*\n * MAINTENANCE: recompute every record's next activity (AGL-2661). The\n * figure is kept by every task write, so this is for the records written\n * before it existed and for whatever a write that could not reach the\n * route left behind. A console action rather than a script, because the\n * person who notices a stale column is the person who should be able to\n * fix it.\n */\n const { data: user } = useUser()\n const [recomputing, setRecomputing] = useState(false)\n const recomputeNextActivity = useCallback(async () => {\n const callScope = crmTaskCallScope(hostId, orgId)\n if (!callScope || recomputing) return\n const confirmed = await confirm({\n title: 'Recompute next activity?',\n description:\n 'Every contact, company and deal in this organization gets its ' +\n '\"Next activity\" recomputed from its open tasks. Safe to run any time; ' +\n 'it only rewrites a figure the task list already implies.',\n confirmationText: 'Recompute',\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n setRecomputing(true)\n try {\n const result = await recomputeAllCrmNextActivity(user, callScope)\n enqueueSnackbar(\n `Next activity recomputed: ${result.records.toLocaleString()} record${result.records === 1 ? '' : 's'} ` +\n `from ${(result.tasks ?? 0).toLocaleString()} open task${result.tasks === 1 ? '' : 's'}` +\n (result.truncated\n ? ' — the first batch of open tasks only; nothing stale was cleared'\n : ''),\n { variant: 'success' },\n )\n } catch (cause) {\n enqueueSnackbar(\n cause instanceof Error ? cause.message : 'The next activity could not be recomputed.',\n { variant: 'warning' },\n )\n } finally {\n setRecomputing(false)\n }\n }, [hostId, orgId, recomputing, confirm, user, enqueueSnackbar])\n\n const [drawerOpen, setDrawerOpen] = useState(false)\n const [editing, setEditing] = useState<ContactFieldDefinitionDoc | null>(null)\n const [busyId, setBusyId] = useState('')\n\n const fieldRef = useCallback(\n (id: string) => {\n if (!scope) throw new Error('a contact field needs an organization')\n return doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.contactFields, id)\n },\n [firestore, scope],\n )\n\n const openCreate = useCallback(() => {\n setEditing(null)\n setDrawerOpen(true)\n }, [])\n const openEdit = useCallback((definition: ContactFieldDefinitionDoc) => {\n setEditing(definition)\n setDrawerOpen(true)\n }, [])\n\n /**\n * Create, or save the editable half of an existing field.\n *\n * On create the document carries everything a reader expects: the key,\n * an `order` past every existing one so it lands last, an explicit\n * `retiredAt: null` so a `where('retiredAt', '==', null)` can find it, the\n * defining site, and the org-wide scope stamp the rules require. On edit\n * only the label, the choices and the required flag move — the key and\n * the type are the drawer's disabled inputs, and this writer does not\n * take them either.\n */\n const handleSubmit = useCallback(\n async (draft: ContactFieldDraft) => {\n if (!scope) return\n const now = new Date()\n if (editing) {\n const verdict = await writeGuardedBySeed(\n { subject: 'field', fromCache },\n async () => {\n await updateDoc(fieldRef(editing.$id), {\n label: draft.label,\n ...(editing.type === 'select' ? { options: draft.options } : {}),\n required: draft.required,\n updatedAt: now,\n })\n },\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n enqueueSnackbar('Field saved', { variant: 'success', persist: false })\n } else {\n const order =\n definitions.reduce(\n (highest, definition) => Math.max(highest, Number(definition.order) || 0),\n -1,\n ) + 1\n await setDoc(\n doc(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.contactFields),\n createResourceUid(),\n ),\n {\n key: draft.key,\n label: draft.label,\n type: draft.type,\n ...(draft.type === 'select' ? { options: draft.options } : {}),\n required: draft.required,\n order,\n retiredAt: null,\n // Which record the field describes — the tab it was made on.\n object,\n hostId: createHostId,\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n createdAt: now,\n updatedAt: now,\n },\n )\n enqueueSnackbar(`Field \"${draft.label}\" added`, {\n variant: 'success',\n persist: false,\n })\n }\n setDrawerOpen(false)\n setEditing(null)\n },\n [scope, editing, fromCache, fieldRef, enqueueSnackbar, definitions, firestore, createHostId, object],\n )\n\n /**\n * Move a field one place up or down.\n *\n * Writes `order` as the POSITION for every row whose stored order is not\n * already its position, in one batch. Swapping two numbers would be enough\n * on a list whose orders are already distinct, and wrong on one where they\n * are not — every field created before this section existed carries the\n * same `order`, and a swap of two equal numbers moves nothing. Renumbering\n * what is out of place makes the first move on such a list also the one\n * that normalizes it, at the cost of a batch that is at most the list.\n */\n const move = useCallback(\n async (definition: ContactFieldDefinitionDoc, direction: -1 | 1) => {\n if (!scope || busyId) return\n const index = definitions.findIndex((entry) => entry.$id === definition.$id)\n const target = index + direction\n if (index < 0 || target < 0 || target >= definitions.length) return\n const next = [...definitions]\n next[index] = definitions[target]\n next[target] = definitions[index]\n setBusyId(definition.$id)\n try {\n const verdict = await writeGuardedBySeed(\n { subject: 'field order', fromCache },\n async () => {\n const batch = writeBatch(firestore)\n const now = new Date()\n next.forEach((entry, position) => {\n if (entry.order === position) return\n batch.update(fieldRef(entry.$id), { order: position, updatedAt: now })\n })\n await batch.commit()\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyId('')\n }\n },\n [scope, busyId, definitions, fromCache, firestore, fieldRef, enqueueSnackbar],\n )\n\n /**\n * Retire a field, or bring one back.\n *\n * Retiring is asked about because it takes a control off every contact\n * form and a column off the list; restoring is not, because it puts a\n * choice back and is undone by retiring again. Neither touches a value.\n */\n const toggleRetired = useCallback(\n async (definition: ContactFieldDefinitionDoc) => {\n if (!scope || busyId) return\n if (!definition.retiredAt) {\n const accepted = await confirm({\n title: `Retire “${definition.label}”?`,\n description:\n `It leaves every ${noun} form, column and form-field mapping. ` +\n 'Values already saved under it are kept and still export, and ' +\n 'you can restore the field at any time.',\n confirmationText: 'Retire',\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating\n // on the resolved value alone would always fall through.\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n }\n setBusyId(definition.$id)\n try {\n await updateDoc(fieldRef(definition.$id), {\n retiredAt: definition.retiredAt ? null : Date.now(),\n updatedAt: new Date(),\n })\n enqueueSnackbar(definition.retiredAt ? 'Field restored' : 'Field retired', {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyId('')\n }\n },\n [scope, busyId, confirm, fieldRef, enqueueSnackbar, noun],\n )\n\n /**\n * Delete a RETIRED field's definition.\n *\n * The values under its key are not touched — they sit on contact documents\n * this page does not read — so what is destroyed is the ability to show\n * them anywhere. The dialog says exactly that, and the key stays taken for\n * as long as the list can see it, which after this write it cannot; a new\n * field with the same key would read the orphaned values back as its own.\n */\n const handleDelete = useCallback(\n async (definition: ContactFieldDefinitionDoc) => {\n if (!scope || busyId || !definition.retiredAt) return\n const accepted = await confirm({\n title: `Delete “${definition.label}”?`,\n description:\n 'The definition is removed for good. Values saved under its key ' +\n `(${definition.key}) stay on the ${noun}s that carry them but ` +\n 'nothing will show them again — and a new field created with the ' +\n 'same key would read them as its own.',\n confirmationText: 'Delete field',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setBusyId(definition.$id)\n try {\n await deleteDoc(fieldRef(definition.$id))\n enqueueSnackbar('Field deleted', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyId('')\n }\n },\n [scope, busyId, confirm, fieldRef, enqueueSnackbar, noun],\n )\n\n const rowActions = (definition: ContactFieldDefinitionDoc): RowActionsMenuItem[] => [\n {\n key: 'edit',\n label: 'Edit field',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n onClick: () => openEdit(definition),\n },\n {\n key: 'retire',\n label: definition.retiredAt ? 'Restore' : 'Retire',\n icon: (\n <MdiIcon\n path={definition.retiredAt ? mdiArchiveArrowUpOutline.path : mdiArchiveOutline.path}\n size={0.8}\n />\n ),\n destructive: !definition.retiredAt,\n disabled: Boolean(busyId),\n disabledReason: busyId ? 'Another field is being saved' : undefined,\n onClick: () => void toggleRetired(definition),\n },\n ...(definition.retiredAt\n ? [\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: Boolean(busyId),\n disabledReason: busyId ? 'Another field is being saved' : undefined,\n onClick: () => void handleDelete(definition),\n } satisfies RowActionsMenuItem,\n ]\n : []),\n ]\n\n return (\n <CardDisplay\n header={'Fields'}\n help={pluginDocsHelp('contactFields', { anchor: '#define-a-field' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: scope ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button size=\"small\" onClick={() => void recomputeNextActivity()} disabled={recomputing}>\n {recomputing ? 'Recomputing…' : 'Recompute next activity'}\n </Button>\n <Button variant=\"contained\" onClick={openCreate}>\n {'New field'}\n </Button>\n </Stack>\n ) : null,\n }}\n >\n <Stack spacing={2}>\n <Tabs\n value={object}\n onChange={(_event, next) => {\n if (isCrmFieldObject(next)) setObject(next)\n }}\n aria-label=\"Fields by record\"\n >\n {CRM_FIELD_OBJECTS.map((entry) => (\n <Tab key={entry} value={entry} label={CRM_FIELD_OBJECT_LABELS[entry]} />\n ))}\n </Tabs>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${OBJECT_INTRO[object]} Fields are shared across every site in this ` +\n 'organization, like the records themselves.'}\n </Typography>\n {scopeReady && !scope ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`This site has no organization, so it has no ${noun} fields.`}\n </Typography>\n ) : !ready ? null : definitions.length === 0 ? (\n <EmptyStateComponent\n label={`No custom ${noun} fields yet`}\n description={`A field you define here is kept on every ${noun} and shows on its page.`}\n action={\n scope ? (\n <Button variant=\"contained\" onClick={openCreate}>\n {'New field'}\n </Button>\n ) : undefined\n }\n />\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell sx={{ width: 96 }}>{'Order'}</TableCell>\n <TableCell>{'Field'}</TableCell>\n <TableCell>{'Key'}</TableCell>\n <TableCell>{'Type'}</TableCell>\n <TableCell>{'Required'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {definitions.map((definition, index) => (\n <TableRow\n key={definition.$id}\n hover\n sx={definition.retiredAt ? { opacity: 0.6 } : undefined}\n >\n <TableCell>\n <Stack direction=\"row\" spacing={0}>\n <IconButton\n size=\"small\"\n disabled={index === 0 || Boolean(busyId)}\n onClick={() => void move(definition, -1)}\n >\n <MdiIcon path={mdiArrowUp.path} size={0.7} />\n <SrOnly>{`Move ${definition.label} up`}</SrOnly>\n </IconButton>\n <IconButton\n size=\"small\"\n disabled={index === definitions.length - 1 || Boolean(busyId)}\n onClick={() => void move(definition, 1)}\n >\n <MdiIcon path={mdiArrowDown.path} size={0.7} />\n <SrOnly>{`Move ${definition.label} down`}</SrOnly>\n </IconButton>\n </Stack>\n </TableCell>\n <TableCell>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', alignItems: 'center' }}\n >\n <Typography variant=\"body2\">{definition.label}</Typography>\n {definition.retiredAt ? (\n <Chip size=\"small\" variant=\"outlined\" color=\"warning\" label=\"Retired\" />\n ) : null}\n </Stack>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ fontFamily: 'monospace' }}>\n {definition.key}\n </Typography>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\">\n {CONTACT_FIELD_TYPE_LABELS[definition.type] ?? definition.type}\n {definition.type === 'select' && definition.options?.length\n ? ` · ${definition.options.length} choices`\n : ''}\n </Typography>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {definition.required ? 'Yes' : '—'}\n </Typography>\n </TableCell>\n <TableCell align=\"right\" sx={{ width: 56 }}>\n <RowActionsMenu label={definition.label} items={rowActions(definition)} />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n )}\n {definitions.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`How many ${noun}s carry a value under each field is not ` +\n `counted — that would read every ${noun} in the organization ` +\n 'each time this page opened.'}\n </Typography>\n ) : null}\n </Stack>\n <ContactFieldDrawer\n open={drawerOpen}\n onClose={() => setDrawerOpen(false)}\n definition={editing}\n object={object}\n takenKeys={definitions.map((definition) => definition.key)}\n onSubmit={handleSubmit}\n />\n </CardDisplay>\n )\n}\nContactsFieldsSection.displayName = 'ContactsFieldsSection'\n\nexport default ContactsFieldsSection\n"],"names":["CONTACT_FIELD_TYPE_LABELS","createResourceUid","CRM_COLLECTIONS","CRM_FIELD_OBJECT_LABELS","CRM_FIELD_OBJECTS","isCrmFieldObject","newResourceScopeFields","ORG_SCOPE_TOKEN","pluginDocsHelp","EmptyStateComponent","mdiArchiveArrowUpOutline","mdiArchiveOutline","mdiArrowDown","mdiArrowUp","mdiDeleteOutline","mdiPencilOutline","CardDisplay","MdiIcon","SrOnly","useConfirmationContext","RowActionsMenu","ScrollTable","useSnackbar","useFirestore","useUser","writeGuardedBySeed","Button","Chip","IconButton","Stack","Tab","TableBody","TableCell","TableHead","TableRow","Tabs","Typography","collection","deleteDoc","doc","setDoc","updateDoc","writeBatch","useCallback","useState","useContactFieldDefinitions","useCrmScope","ContactFieldDrawer","recomputeAllCrmNextActivity","crmTaskCallScope","OBJECT_INTRO","contact","company","deal","lead","OBJECT_NOUN","ContactsFieldsSection","props","hostId","org","firestore","enqueueSnackbar","confirm","scope","ready","scopeReady","createHostId","orgId","object","setObject","definitions","fromCache","noun","data","user","recomputing","setRecomputing","recomputeNextActivity","callScope","confirmed","title","description","confirmationText","then","catch","result","records","toLocaleString","tasks","truncated","variant","cause","Error","message","drawerOpen","setDrawerOpen","editing","setEditing","busyId","setBusyId","fieldRef","id","contactFields","openCreate","openEdit","definition","handleSubmit","draft","now","Date","verdict","subject","$id","label","type","options","required","updatedAt","ok","persist","order","reduce","highest","Math","max","Number","key","retiredAt","createdAt","move","direction","index","findIndex","entry","target","length","next","batch","forEach","position","update","commit","error","console","allowDuplicate","toggleRetired","accepted","handleDelete","confirmationButtonProps","color","rowActions","icon","path","size","onClick","destructive","disabled","Boolean","disabledReason","undefined","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","spacing","sx","alignItems","value","onChange","_event","aria-label","map","width","align","hover","opacity","flexWrap","fontFamily","items","open","onClose","takenKeys","onSubmit","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/fields-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 {\n CONTACT_FIELD_TYPE_LABELS,\n type ConsolePluginPageProps,\n createResourceUid,\n CRM_COLLECTIONS,\n CRM_FIELD_OBJECT_LABELS,\n CRM_FIELD_OBJECTS,\n type CrmFieldObject,\n isCrmFieldObject,\n newResourceScopeFields,\n ORG_SCOPE_TOKEN,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport {\n mdiArchiveArrowUpOutline,\n mdiArchiveOutline,\n mdiArrowDown,\n mdiArrowUp,\n mdiDeleteOutline,\n mdiPencilOutline,\n} from '@aglyn/shared-data-mdi'\nimport {\n CardDisplay,\n MdiIcon,\n SrOnly,\n useConfirmationContext,\n} from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Chip,\n IconButton,\n Stack,\n Tab,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n Tabs,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n deleteDoc,\n doc,\n setDoc,\n updateDoc,\n writeBatch,\n} from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport {\n type ContactFieldDefinitionDoc,\n useContactFieldDefinitions,\n} from '../hooks/use-contact-field-definitions'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport ContactFieldDrawer, { type ContactFieldDraft } from './contact-field-drawer'\nimport { LeadSourceValuesCard } from './lead-source-values-card'\nimport { recomputeAllCrmNextActivity } from '../model/next-activity-api'\nimport { crmTaskCallScope } from '../model/task-routes'\n\nexport type ContactsFieldsSectionProps = Pick<ConsolePluginPageProps, 'hostId' | 'org'>\n\n/** What each tab says the fields are for, above its list. */\nconst OBJECT_INTRO: Record<CrmFieldObject, string> = {\n contact:\n 'The custom fields on a contact — text, number, date, choice, ' +\n 'checkbox or link. They show on every contact, as columns on the ' +\n 'list, and a form field can save into one.',\n company:\n 'The custom fields on a company. They show on every company’s page ' +\n 'and its edit form, as columns on the companies list, and a CSV ' +\n 'import can fill them.',\n deal:\n 'The custom fields on a deal. They show on every deal’s page and ' +\n 'its edit form, and as columns on the deals table.',\n lead:\n 'The custom fields on a lead. They show on every lead’s page and the ' +\n 'New lead drawer, and as columns on the leads list. They stay on the ' +\n 'lead when it converts — a contact’s fields are its own.',\n}\n\n/** The noun each tab's empty state and captions use. */\nconst OBJECT_NOUN: Record<CrmFieldObject, string> = {\n contact: 'contact',\n company: 'company',\n deal: 'deal',\n lead: 'lead',\n}\n\n/**\n * `/crm/fields` — the custom fields a holder keeps on a person (AGL-2601),\n * since AGL-2661 on a company and a deal too, and since AGL-3272 on a lead:\n * one tab each, and every CRM record describable in the org's own words.\n *\n * Definitions live in `orgs/{orgId}/contactFields`, one document per field,\n * and the VALUES live under each contact facet's `custom` keyed by the\n * definition's `key`. That split decides everything this section does:\n *\n * - A field is RETIRED, not deleted, while values may exist under it. A\n * retired field leaves every form and every column, but an export can\n * still read what was written, and a restore brings it back intact.\n * Delete is offered only on a retired field, because retiring is the step\n * where the author had to look at what they were losing.\n * - The KEY never changes. It is the map key every value sits under; a\n * rename is a new field and a retire.\n * - Order is a stored `order` on each document, moved with the arrows here,\n * and every reader sorts by it — the profile card, the columns, an export.\n *\n * ## What is NOT on this page\n *\n * How many contacts carry a value under each field. That count is a read of\n * every contact document in the org — the expensive-read shape this codebase\n * has a standing rule against — and it would be paid on every visit to a\n * settings page. The list says so rather than showing a number it cannot\n * afford to keep true.\n *\n * Definitions are ORG-WIDE (`visibleTo: ['org']`): a field is a fact about\n * how this business describes people, and every site in the org files\n * people into the same address book. `hostId` records which site defined it.\n */\nexport function ContactsFieldsSection(props: ContactsFieldsSectionProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n // The org root from the one scope hook (AGL-2614), which also answers at\n // the organization level (AGL-2630). A definition is org-wide either way;\n // the defining site it records is the mounted one, or the picked one, or\n // none for a field defined over the whole org before any pick.\n const { scope, ready: scopeReady, createHostId } = useCrmScope({ hostId, org })\n const orgId = scope?.[1] ?? null\n /*\n * ONE TAB PER OBJECT (AGL-2661). The list, the drawer and every write\n * below are about the tab's object: the hook narrows the org's one\n * definition list to it, a new field is stamped with it, and the drawer\n * refuses a key only against the keys THIS object already uses — a\n * company and a contact may both define `region`.\n */\n const [object, setObject] = useState<CrmFieldObject>('contact')\n const { definitions, ready, fromCache } = useContactFieldDefinitions(orgId, object)\n const noun = OBJECT_NOUN[object]\n\n /*\n * MAINTENANCE: recompute every record's next activity (AGL-2661). The\n * figure is kept by every task write, so this is for the records written\n * before it existed and for whatever a write that could not reach the\n * route left behind. A console action rather than a script, because the\n * person who notices a stale column is the person who should be able to\n * fix it.\n */\n const { data: user } = useUser()\n const [recomputing, setRecomputing] = useState(false)\n const recomputeNextActivity = useCallback(async () => {\n const callScope = crmTaskCallScope(hostId, orgId)\n if (!callScope || recomputing) return\n const confirmed = await confirm({\n title: 'Recompute next activity?',\n description:\n 'Every contact, company and deal in this organization gets its ' +\n '\"Next activity\" recomputed from its open tasks. Safe to run any time; ' +\n 'it only rewrites a figure the task list already implies.',\n confirmationText: 'Recompute',\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n setRecomputing(true)\n try {\n const result = await recomputeAllCrmNextActivity(user, callScope)\n enqueueSnackbar(\n `Next activity recomputed: ${result.records.toLocaleString()} record${result.records === 1 ? '' : 's'} ` +\n `from ${(result.tasks ?? 0).toLocaleString()} open task${result.tasks === 1 ? '' : 's'}` +\n (result.truncated\n ? ' — the first batch of open tasks only; nothing stale was cleared'\n : ''),\n { variant: 'success' },\n )\n } catch (cause) {\n enqueueSnackbar(\n cause instanceof Error ? cause.message : 'The next activity could not be recomputed.',\n { variant: 'warning' },\n )\n } finally {\n setRecomputing(false)\n }\n }, [hostId, orgId, recomputing, confirm, user, enqueueSnackbar])\n\n const [drawerOpen, setDrawerOpen] = useState(false)\n const [editing, setEditing] = useState<ContactFieldDefinitionDoc | null>(null)\n const [busyId, setBusyId] = useState('')\n\n const fieldRef = useCallback(\n (id: string) => {\n if (!scope) throw new Error('a contact field needs an organization')\n return doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.contactFields, id)\n },\n [firestore, scope],\n )\n\n const openCreate = useCallback(() => {\n setEditing(null)\n setDrawerOpen(true)\n }, [])\n const openEdit = useCallback((definition: ContactFieldDefinitionDoc) => {\n setEditing(definition)\n setDrawerOpen(true)\n }, [])\n\n /**\n * Create, or save the editable half of an existing field.\n *\n * On create the document carries everything a reader expects: the key,\n * an `order` past every existing one so it lands last, an explicit\n * `retiredAt: null` so a `where('retiredAt', '==', null)` can find it, the\n * defining site, and the org-wide scope stamp the rules require. On edit\n * only the label, the choices and the required flag move — the key and\n * the type are the drawer's disabled inputs, and this writer does not\n * take them either.\n */\n const handleSubmit = useCallback(\n async (draft: ContactFieldDraft) => {\n if (!scope) return\n const now = new Date()\n if (editing) {\n const verdict = await writeGuardedBySeed(\n { subject: 'field', fromCache },\n async () => {\n await updateDoc(fieldRef(editing.$id), {\n label: draft.label,\n ...(editing.type === 'select' ? { options: draft.options } : {}),\n required: draft.required,\n updatedAt: now,\n })\n },\n )\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n enqueueSnackbar('Field saved', { variant: 'success', persist: false })\n } else {\n const order =\n definitions.reduce(\n (highest, definition) => Math.max(highest, Number(definition.order) || 0),\n -1,\n ) + 1\n await setDoc(\n doc(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.contactFields),\n createResourceUid(),\n ),\n {\n key: draft.key,\n label: draft.label,\n type: draft.type,\n ...(draft.type === 'select' ? { options: draft.options } : {}),\n required: draft.required,\n order,\n retiredAt: null,\n // Which record the field describes — the tab it was made on.\n object,\n hostId: createHostId,\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n createdAt: now,\n updatedAt: now,\n },\n )\n enqueueSnackbar(`Field \"${draft.label}\" added`, {\n variant: 'success',\n persist: false,\n })\n }\n setDrawerOpen(false)\n setEditing(null)\n },\n [scope, editing, fromCache, fieldRef, enqueueSnackbar, definitions, firestore, createHostId, object],\n )\n\n /**\n * Move a field one place up or down.\n *\n * Writes `order` as the POSITION for every row whose stored order is not\n * already its position, in one batch. Swapping two numbers would be enough\n * on a list whose orders are already distinct, and wrong on one where they\n * are not — every field created before this section existed carries the\n * same `order`, and a swap of two equal numbers moves nothing. Renumbering\n * what is out of place makes the first move on such a list also the one\n * that normalizes it, at the cost of a batch that is at most the list.\n */\n const move = useCallback(\n async (definition: ContactFieldDefinitionDoc, direction: -1 | 1) => {\n if (!scope || busyId) return\n const index = definitions.findIndex((entry) => entry.$id === definition.$id)\n const target = index + direction\n if (index < 0 || target < 0 || target >= definitions.length) return\n const next = [...definitions]\n next[index] = definitions[target]\n next[target] = definitions[index]\n setBusyId(definition.$id)\n try {\n const verdict = await writeGuardedBySeed(\n { subject: 'field order', fromCache },\n async () => {\n const batch = writeBatch(firestore)\n const now = new Date()\n next.forEach((entry, position) => {\n if (entry.order === position) return\n batch.update(fieldRef(entry.$id), { order: position, updatedAt: now })\n })\n await batch.commit()\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n }\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyId('')\n }\n },\n [scope, busyId, definitions, fromCache, firestore, fieldRef, enqueueSnackbar],\n )\n\n /**\n * Retire a field, or bring one back.\n *\n * Retiring is asked about because it takes a control off every contact\n * form and a column off the list; restoring is not, because it puts a\n * choice back and is undone by retiring again. Neither touches a value.\n */\n const toggleRetired = useCallback(\n async (definition: ContactFieldDefinitionDoc) => {\n if (!scope || busyId) return\n if (!definition.retiredAt) {\n const accepted = await confirm({\n title: `Retire “${definition.label}”?`,\n description:\n `It leaves every ${noun} form, column and form-field mapping. ` +\n 'Values already saved under it are kept and still export, and ' +\n 'you can restore the field at any time.',\n confirmationText: 'Retire',\n })\n // `confirm` resolves with no value and REJECTS on cancel, so gating\n // on the resolved value alone would always fall through.\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n }\n setBusyId(definition.$id)\n try {\n await updateDoc(fieldRef(definition.$id), {\n retiredAt: definition.retiredAt ? null : Date.now(),\n updatedAt: new Date(),\n })\n enqueueSnackbar(definition.retiredAt ? 'Field restored' : 'Field retired', {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyId('')\n }\n },\n [scope, busyId, confirm, fieldRef, enqueueSnackbar, noun],\n )\n\n /**\n * Delete a RETIRED field's definition.\n *\n * The values under its key are not touched — they sit on contact documents\n * this page does not read — so what is destroyed is the ability to show\n * them anywhere. The dialog says exactly that, and the key stays taken for\n * as long as the list can see it, which after this write it cannot; a new\n * field with the same key would read the orphaned values back as its own.\n */\n const handleDelete = useCallback(\n async (definition: ContactFieldDefinitionDoc) => {\n if (!scope || busyId || !definition.retiredAt) return\n const accepted = await confirm({\n title: `Delete “${definition.label}”?`,\n description:\n 'The definition is removed for good. Values saved under its key ' +\n `(${definition.key}) stay on the ${noun}s that carry them but ` +\n 'nothing will show them again — and a new field created with the ' +\n 'same key would read them as its own.',\n confirmationText: 'Delete field',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!accepted) return\n setBusyId(definition.$id)\n try {\n await deleteDoc(fieldRef(definition.$id))\n enqueueSnackbar('Field deleted', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n } finally {\n setBusyId('')\n }\n },\n [scope, busyId, confirm, fieldRef, enqueueSnackbar, noun],\n )\n\n const rowActions = (definition: ContactFieldDefinitionDoc): RowActionsMenuItem[] => [\n {\n key: 'edit',\n label: 'Edit field',\n icon: <MdiIcon path={mdiPencilOutline.path} size={0.8} />,\n onClick: () => openEdit(definition),\n },\n {\n key: 'retire',\n label: definition.retiredAt ? 'Restore' : 'Retire',\n icon: (\n <MdiIcon\n path={definition.retiredAt ? mdiArchiveArrowUpOutline.path : mdiArchiveOutline.path}\n size={0.8}\n />\n ),\n destructive: !definition.retiredAt,\n disabled: Boolean(busyId),\n disabledReason: busyId ? 'Another field is being saved' : undefined,\n onClick: () => void toggleRetired(definition),\n },\n ...(definition.retiredAt\n ? [\n {\n key: 'delete',\n label: 'Delete',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: Boolean(busyId),\n disabledReason: busyId ? 'Another field is being saved' : undefined,\n onClick: () => void handleDelete(definition),\n } satisfies RowActionsMenuItem,\n ]\n : []),\n ]\n\n return (\n <CardDisplay\n header={'Fields'}\n help={pluginDocsHelp('contactFields', { anchor: '#define-a-field' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: scope ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Button size=\"small\" onClick={() => void recomputeNextActivity()} disabled={recomputing}>\n {recomputing ? 'Recomputing…' : 'Recompute next activity'}\n </Button>\n <Button variant=\"contained\" onClick={openCreate}>\n {'New field'}\n </Button>\n </Stack>\n ) : null,\n }}\n >\n <Stack spacing={2}>\n <Tabs\n value={object}\n onChange={(_event, next) => {\n if (isCrmFieldObject(next)) setObject(next)\n }}\n aria-label=\"Fields by record\"\n >\n {CRM_FIELD_OBJECTS.map((entry) => (\n <Tab key={entry} value={entry} label={CRM_FIELD_OBJECT_LABELS[entry]} />\n ))}\n </Tabs>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`${OBJECT_INTRO[object]} Fields are shared across every site in this ` +\n 'organization, like the records themselves.'}\n </Typography>\n {scopeReady && !scope ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {`This site has no organization, so it has no ${noun} fields.`}\n </Typography>\n ) : !ready ? null : definitions.length === 0 ? (\n <EmptyStateComponent\n label={`No custom ${noun} fields yet`}\n description={`A field you define here is kept on every ${noun} and shows on its page.`}\n action={\n scope ? (\n <Button variant=\"contained\" onClick={openCreate}>\n {'New field'}\n </Button>\n ) : undefined\n }\n />\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell sx={{ width: 96 }}>{'Order'}</TableCell>\n <TableCell>{'Field'}</TableCell>\n <TableCell>{'Key'}</TableCell>\n <TableCell>{'Type'}</TableCell>\n <TableCell>{'Required'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {definitions.map((definition, index) => (\n <TableRow\n key={definition.$id}\n hover\n sx={definition.retiredAt ? { opacity: 0.6 } : undefined}\n >\n <TableCell>\n <Stack direction=\"row\" spacing={0}>\n <IconButton\n size=\"small\"\n disabled={index === 0 || Boolean(busyId)}\n onClick={() => void move(definition, -1)}\n >\n <MdiIcon path={mdiArrowUp.path} size={0.7} />\n <SrOnly>{`Move ${definition.label} up`}</SrOnly>\n </IconButton>\n <IconButton\n size=\"small\"\n disabled={index === definitions.length - 1 || Boolean(busyId)}\n onClick={() => void move(definition, 1)}\n >\n <MdiIcon path={mdiArrowDown.path} size={0.7} />\n <SrOnly>{`Move ${definition.label} down`}</SrOnly>\n </IconButton>\n </Stack>\n </TableCell>\n <TableCell>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', alignItems: 'center' }}\n >\n <Typography variant=\"body2\">{definition.label}</Typography>\n {definition.retiredAt ? (\n <Chip size=\"small\" variant=\"outlined\" color=\"warning\" label=\"Retired\" />\n ) : null}\n </Stack>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ fontFamily: 'monospace' }}>\n {definition.key}\n </Typography>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\">\n {CONTACT_FIELD_TYPE_LABELS[definition.type] ?? definition.type}\n {definition.type === 'select' && definition.options?.length\n ? ` · ${definition.options.length} choices`\n : ''}\n </Typography>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {definition.required ? 'Yes' : '—'}\n </Typography>\n </TableCell>\n <TableCell align=\"right\" sx={{ width: 56 }}>\n <RowActionsMenu label={definition.label} items={rowActions(definition)} />\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n )}\n {definitions.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`How many ${noun}s carry a value under each field is not ` +\n `counted — that would read every ${noun} in the organization ` +\n 'each time this page opened.'}\n </Typography>\n ) : null}\n {/*\n Lead source (AGL-3298) — a standard lead field whose CHOICES are the\n org's, Salesforce's Lead Source picklist — kept on the Leads tab\n beside the custom lead fields.\n */}\n {object === 'lead' && scope ? (\n <LeadSourceValuesCard hostId={hostId} orgId={orgId} createHostId={createHostId} />\n ) : null}\n </Stack>\n <ContactFieldDrawer\n open={drawerOpen}\n onClose={() => setDrawerOpen(false)}\n definition={editing}\n object={object}\n takenKeys={definitions.map((definition) => definition.key)}\n onSubmit={handleSubmit}\n />\n </CardDisplay>\n )\n}\nContactsFieldsSection.displayName = 'ContactsFieldsSection'\n\nexport default ContactsFieldsSection\n"],"names":["CONTACT_FIELD_TYPE_LABELS","createResourceUid","CRM_COLLECTIONS","CRM_FIELD_OBJECT_LABELS","CRM_FIELD_OBJECTS","isCrmFieldObject","newResourceScopeFields","ORG_SCOPE_TOKEN","pluginDocsHelp","EmptyStateComponent","mdiArchiveArrowUpOutline","mdiArchiveOutline","mdiArrowDown","mdiArrowUp","mdiDeleteOutline","mdiPencilOutline","CardDisplay","MdiIcon","SrOnly","useConfirmationContext","RowActionsMenu","ScrollTable","useSnackbar","useFirestore","useUser","writeGuardedBySeed","Button","Chip","IconButton","Stack","Tab","TableBody","TableCell","TableHead","TableRow","Tabs","Typography","collection","deleteDoc","doc","setDoc","updateDoc","writeBatch","useCallback","useState","useContactFieldDefinitions","useCrmScope","ContactFieldDrawer","LeadSourceValuesCard","recomputeAllCrmNextActivity","crmTaskCallScope","OBJECT_INTRO","contact","company","deal","lead","OBJECT_NOUN","ContactsFieldsSection","props","hostId","org","firestore","enqueueSnackbar","confirm","scope","ready","scopeReady","createHostId","orgId","object","setObject","definitions","fromCache","noun","data","user","recomputing","setRecomputing","recomputeNextActivity","callScope","confirmed","title","description","confirmationText","then","catch","result","records","toLocaleString","tasks","truncated","variant","cause","Error","message","drawerOpen","setDrawerOpen","editing","setEditing","busyId","setBusyId","fieldRef","id","contactFields","openCreate","openEdit","definition","handleSubmit","draft","now","Date","verdict","subject","$id","label","type","options","required","updatedAt","ok","persist","order","reduce","highest","Math","max","Number","key","retiredAt","createdAt","move","direction","index","findIndex","entry","target","length","next","batch","forEach","position","update","commit","error","console","allowDuplicate","toggleRetired","accepted","handleDelete","confirmationButtonProps","color","rowActions","icon","path","size","onClick","destructive","disabled","Boolean","disabledReason","undefined","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","spacing","sx","alignItems","value","onChange","_event","aria-label","map","width","align","hover","opacity","flexWrap","fontFamily","items","open","onClose","takenKeys","onSubmit","displayName"],"mappings":"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"}
|
|
@@ -9,6 +9,8 @@ export interface LeadImportDrawerProps {
|
|
|
9
9
|
onClose: () => void;
|
|
10
10
|
/** The site the file is filed under, or `null` at the organization level. */
|
|
11
11
|
hostId: string | null;
|
|
12
|
+
/** The org the lead source list is read from (AGL-3298); `null` while it settles. */
|
|
13
|
+
orgId?: string | null;
|
|
12
14
|
}
|
|
13
15
|
export declare function LeadImportDrawer(props: LeadImportDrawerProps): import("react").JSX.Element;
|
|
14
16
|
export declare namespace LeadImportDrawer {
|
|
@@ -17,6 +19,7 @@ export declare namespace LeadImportDrawer {
|
|
|
17
19
|
/** The "Import CSV" action on the leads list, with the drawer it opens. */
|
|
18
20
|
export declare function LeadImportButton(props: {
|
|
19
21
|
hostId: string | null;
|
|
22
|
+
orgId?: string | null;
|
|
20
23
|
}): import("react").JSX.Element;
|
|
21
24
|
export declare namespace LeadImportButton {
|
|
22
25
|
var displayName: string;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
18
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
19
|
/**
|
|
19
20
|
* IMPORTING A LEADS FILE — the lead vocabulary over the shared drawer
|
|
@@ -34,8 +35,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
34
35
|
* drawer's site picker is not a convenience here — it is the only thing
|
|
35
36
|
* that says where the file lands, and the import stays held until it is
|
|
36
37
|
* answered.
|
|
37
|
-
*/ import { normalizeContactEmail, pluginDocsHelp } from "@aglyn/aglyn";
|
|
38
|
-
import {
|
|
38
|
+
*/ import { crmLeadSourceRefusal, normalizeContactEmail, pluginDocsHelp } from "@aglyn/aglyn";
|
|
39
|
+
import { Alert } from "@mui/material";
|
|
40
|
+
import { useMemo } from "react";
|
|
41
|
+
import { useLeadSourcePicklist } from "../hooks/use-lead-source-picklist.js";
|
|
42
|
+
import { LEAD_IMPORT_CHUNK_SIZE, LEAD_IMPORT_FIELD_LABELS, LEAD_IMPORT_FIELDS, LEAD_IMPORT_MAX_ROWS, LEAD_IMPORT_PREVIEW_ROWS, LEAD_IMPORT_SKIP_LABELS, emptyLeadImportResult, guessLeadImportMapping, leadImportSkippedCsv, leadImportUnknownLeadSources, mapLeadImportRow, mergeLeadImportResults } from "../model/crm-lead-import.js";
|
|
39
43
|
import { leadsCsv } from "../model/leads-csv.js";
|
|
40
44
|
import { CsvImportButton, CsvImportDrawer } from "./csv-import-drawer.js";
|
|
41
45
|
/** The browser-side address of the route one chunk is posted to. */ export const LEADS_IMPORT_URL = '/api/crm/leads-import';
|
|
@@ -70,22 +74,41 @@ import { CsvImportButton, CsvImportDrawer } from "./csv-import-drawer.js";
|
|
|
70
74
|
templateFileName: 'leads-template.csv'
|
|
71
75
|
};
|
|
72
76
|
export function LeadImportDrawer(props) {
|
|
73
|
-
const { open, onClose, hostId } = props;
|
|
77
|
+
const { open, onClose, hostId, orgId = null } = props;
|
|
78
|
+
/*
|
|
79
|
+
* THE ORG'S LEAD SOURCES (AGL-3298). The server refuses a row naming a
|
|
80
|
+
* value outside the list; the notice says so before the file is sent,
|
|
81
|
+
* naming the values it holds that the list does not and the ones it
|
|
82
|
+
* allows, so the operator can fix the file or add the value first.
|
|
83
|
+
*/ const { picklist } = useLeadSourcePicklist(orgId);
|
|
84
|
+
const vocabulary = useMemo(()=>_extends({}, LEAD_IMPORT_VOCABULARY, {
|
|
85
|
+
rowsNotice: (rows)=>{
|
|
86
|
+
const unknown = leadImportUnknownLeadSources(rows, picklist);
|
|
87
|
+
if (!unknown.length) return null;
|
|
88
|
+
return /*#__PURE__*/ _jsx(Alert, {
|
|
89
|
+
severity: "warning",
|
|
90
|
+
children: `${unknown.length === 1 ? 'A lead source' : `${unknown.length} lead sources`} in this ` + `file ${unknown.length === 1 ? 'is' : 'are'} not in your list: ` + `${unknown.slice(0, 10).join(', ')}${unknown.length > 10 ? ', …' : ''}. ` + 'Those rows will be skipped. ' + crmLeadSourceRefusal(picklist) + ' Add a value under CRM › Fields › Leads to import it.'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}), [
|
|
94
|
+
picklist
|
|
95
|
+
]);
|
|
74
96
|
return /*#__PURE__*/ _jsx(CsvImportDrawer, {
|
|
75
97
|
open: open,
|
|
76
98
|
onClose: onClose,
|
|
77
99
|
hostId: hostId,
|
|
78
|
-
vocabulary:
|
|
100
|
+
vocabulary: vocabulary
|
|
79
101
|
});
|
|
80
102
|
}
|
|
81
103
|
LeadImportDrawer.displayName = 'LeadImportDrawer';
|
|
82
104
|
/** The "Import CSV" action on the leads list, with the drawer it opens. */ export function LeadImportButton(props) {
|
|
83
|
-
const { hostId } = props;
|
|
105
|
+
const { hostId, orgId = null } = props;
|
|
84
106
|
return /*#__PURE__*/ _jsx(CsvImportButton, {
|
|
85
107
|
children: (open, onClose)=>/*#__PURE__*/ _jsx(LeadImportDrawer, {
|
|
86
108
|
open: open,
|
|
87
109
|
onClose: onClose,
|
|
88
|
-
hostId: hostId
|
|
110
|
+
hostId: hostId,
|
|
111
|
+
orgId: orgId
|
|
89
112
|
})
|
|
90
113
|
});
|
|
91
114
|
}
|