@aglyn/plugins-crm 1.0.0-beta.219 → 1.0.0-beta.221
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/README.md +2 -0
- package/package.json +14 -15
- package/src/lib/components/company-import-drawer.d.ts +1 -1
- package/src/lib/components/contact-associations-card.js +4 -3
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/crm-booking-zone.d.ts +8 -2
- package/src/lib/components/crm-booking-zone.js.map +1 -1
- package/src/lib/components/crm-glance-card.js +2 -1
- package/src/lib/components/crm-glance-card.js.map +1 -1
- package/src/lib/components/deal-products-card.js +27 -16
- package/src/lib/components/deal-products-card.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.d.ts +4 -4
- package/src/lib/components/lead-campaigns-card.js +9 -8
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +4 -0
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-status-chip.js +3 -1
- package/src/lib/components/lead-status-chip.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +7 -9
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +5 -1
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-lead-drawer.js +5 -4
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/components/reports/activity-card.js +4 -3
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/components/reports/closed-deals-card.js +4 -3
- package/src/lib/components/reports/closed-deals-card.js.map +1 -1
- package/src/lib/components/reports/contacts-mix-card.js +3 -2
- package/src/lib/components/reports/contacts-mix-card.js.map +1 -1
- package/src/lib/components/reports/contacts-trend-card.js +2 -1
- package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
- package/src/lib/components/reports/forecast-card.js +3 -2
- package/src/lib/components/reports/forecast-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +8 -6
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +4 -3
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/report-format.d.ts +1 -1
- package/src/lib/components/reports/report-format.js.map +1 -1
- package/src/lib/components/reports/report-scope.d.ts +1 -1
- package/src/lib/components/reports/report-scope.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +2 -1
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/reports/tasks-card.js +2 -1
- package/src/lib/components/reports/tasks-card.js.map +1 -1
- package/src/lib/components/reports/won-lost-by-owner-card.js +3 -2
- package/src/lib/components/reports/won-lost-by-owner-card.js.map +1 -1
- package/src/lib/components/reports-section.js +4 -4
- package/src/lib/components/reports-section.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +2 -0
- package/src/lib/constants/bundle-common.js +1 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +4 -0
- package/src/lib/declarations.console-server.js +12 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.server.d.ts +6 -0
- package/src/lib/declarations.server.js +51 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-crm-campaigns.d.ts +2 -2
- package/src/lib/hooks/use-crm-campaigns.js +3 -3
- package/src/lib/hooks/use-crm-campaigns.js.map +1 -1
- package/src/lib/model/campaign-filing-activity.d.ts +1 -1
- package/src/lib/model/campaign-filing-activity.js +1 -1
- package/src/lib/model/campaign-filing-activity.js.map +1 -1
- package/src/lib/model/contact-record.js +1 -1
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/crm-digest.d.ts +45 -3
- package/src/lib/model/crm-digest.js +75 -11
- package/src/lib/model/crm-digest.js.map +1 -1
- package/src/lib/model/crm-inbound.d.ts +162 -0
- package/src/lib/model/crm-inbound.js +208 -0
- package/src/lib/model/crm-inbound.js.map +1 -0
- package/src/lib/model/crm-lead-import.d.ts +1 -1
- package/src/lib/model/crm-lead-import.js +4 -4
- package/src/lib/model/crm-lead-import.js.map +1 -1
- package/src/lib/model/crm-reports.d.ts +467 -0
- package/src/lib/model/crm-reports.js +667 -0
- package/src/lib/model/crm-reports.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +4 -3
- package/src/lib/model/lead-filters.js +7 -5
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/plugin.js +36 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/crm-leads.js +1 -0
- package/src/lib/server/api-v1/crm-leads.js.map +1 -1
- package/src/lib/server/api-v1/openapi.js +1 -1
- package/src/lib/server/api-v1/openapi.js.map +1 -1
- package/src/lib/server/campaign-filing-activity.d.ts +1 -1
- package/src/lib/server/campaign-filing-activity.js +7 -11
- package/src/lib/server/campaign-filing-activity.js.map +1 -1
- package/src/lib/server/capture-contact.js +19 -5
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/contact-email-engagement.d.ts +33 -0
- package/src/lib/server/contact-email-engagement.js +146 -0
- package/src/lib/server/contact-email-engagement.js.map +1 -0
- package/src/lib/server/contact-update.js +3 -3
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/container-detach.d.ts +31 -0
- package/src/lib/server/container-detach.js +115 -0
- package/src/lib/server/container-detach.js.map +1 -0
- package/src/lib/server/crm-inbound-email.d.ts +162 -0
- package/src/lib/server/crm-inbound-email.js +259 -0
- package/src/lib/server/crm-inbound-email.js.map +1 -0
- package/src/lib/server/crm-records-meter.d.ts +50 -0
- package/src/lib/server/crm-records-meter.js +126 -0
- package/src/lib/server/crm-records-meter.js.map +1 -0
- package/src/lib/server/daily-digest-route.d.ts +2 -8
- package/src/lib/server/daily-digest-route.js +15 -25
- package/src/lib/server/daily-digest-route.js.map +1 -1
- package/src/lib/server/inbound-address.js +5 -3
- package/src/lib/server/inbound-address.js.map +1 -1
- package/src/lib/server/inbound-route.js +7 -4
- package/src/lib/server/inbound-route.js.map +1 -1
- package/src/lib/server/lead-campaign-carry.js +3 -3
- package/src/lib/server/lead-campaign-carry.js.map +1 -1
- package/src/lib/server/lead-create.d.ts +1 -1
- package/src/lib/server/lead-create.js +10 -15
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-nurturing.d.ts +23 -0
- package/src/lib/server/lead-nurturing.js +82 -0
- package/src/lib/server/lead-nurturing.js.map +1 -0
- package/src/lib/server/leads-import.js +7 -7
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server/record-email-state.js +23 -0
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-timeline.d.ts +12 -0
- package/src/lib/server/record-timeline.js +127 -15
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/task-reminders-route.js +4 -2
- package/src/lib/server/task-reminders-route.js.map +1 -1
- package/src/lib/server/task-routes.d.ts +2 -0
- package/src/lib/server/task-routes.js +22 -3
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/usage-axes.js +8 -1
- package/src/lib/usage-axes.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-products-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n checkEntitlement,\n CRM_COLLECTIONS,\n type CrmDealLineItem,\n DEAL_LINE_ITEM_NAME_MAX,\n DEAL_LINE_ITEM_QUANTITY_MAX,\n DEAL_LINE_ITEMS_MAX,\n lineItemsTotalCents,\n nameSearchToken,\n pluginDocsHelp,\n readDealLineItems,\n} from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Autocomplete,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n IconButton,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n getDocs,\n limit,\n orderBy,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { CrmOrgDoc } from '../hooks/use-crm-scope'\nimport {\n amountInputValue,\n type DealDoc,\n DEFAULT_DEAL_CURRENCY,\n formatMoney,\n parseAmountInput,\n} from '../model/deal-board-model'\n\n/** Catalog matches offered per keystroke; a longer list is a scroll, not a pick. */\nconst CATALOG_MATCHES = 8\n\n/** One thing the catalog search offers: a product, or one variant of it. */\ninterface CatalogChoice {\n key: string\n productId: string\n label: string\n unitAmountCents: number\n}\n\nexport interface DealProductsCardProps {\n deal: DealDoc\n orgId: string\n /**\n * The site whose catalog the search reads — the console's own, or\n * `null` at the organization level (AGL-2630), where the deal's own\n * capturing site is searched; a deal with none takes lines by hand only.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The listener's verdict on the deal, for the stale-seed guard. */\n fromCache: boolean\n unreadable: boolean\n}\n\n/**\n * The products behind a deal's amount (AGL-2620).\n *\n * ## The amount follows the lines\n *\n * Every write here stores `lineItems` AND `amountCents` as their sum, in\n * one update, so the board, the reports and the API keep reading the one\n * field they always read and no reader adds up lines. Once a deal has a\n * line, the drawer's Amount goes read-only and says why; remove the last\n * line and the amount is typed again, keeping the last sum rather than\n * dropping to nothing. The currency is the deal's — a line cannot be in\n * another, because their sum is one number.\n *\n * ## Two doors to a line\n *\n * **From the catalog** searches THIS site's products — a prefix on\n * `nameTokens` over active products, the query the products hub runs and\n * the `(status, nameTokens, nameLower)` index serves — and offers each\n * variant priced. The catalog is USD; a deal in another currency is told\n * so and can edit the unit amount after. **By hand** takes a name and a\n * price with no product behind them, which is also all a plan without\n * commerce gets: the catalog door is not drawn for it, because the site\n * has no catalog to search.\n *\n * Adding opens a dialog rather than a row of fields above the table, and\n * a quantity is edited in place and written on blur.\n */\nexport function DealProductsCard(props: DealProductsCardProps) {\n const { deal, orgId, hostId, org, fromCache, unreadable } = props\n const firestore = useFirestore()\n // The catalog searched: the mounted site's, or the deal's own.\n const catalogHostId = hostId ?? deal.hostId ?? null\n const { enqueueSnackbar } = useSnackbar()\n const currency = String(deal.currency || DEFAULT_DEAL_CURRENCY).toLowerCase()\n const items = useMemo(() => deal.lineItems ?? [], [deal.lineItems])\n const hasCommerce = checkEntitlement(\n (org ?? null) as Partial<AglynOrgBilling> | null,\n 'commerce',\n )\n const [busy, setBusy] = useState(false)\n const [adding, setAdding] = useState(false)\n\n /** The one write: the lines and their sum, together. */\n const save = useCallback(\n async (next: CrmDealLineItem[], done: string) => {\n const read = readDealLineItems(next, currency)\n if ('error' in read) {\n enqueueSnackbar(read.error, { variant: 'warning', persist: false })\n return false\n }\n setBusy(true)\n try {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id), {\n lineItems: read.items,\n // The last sum stays on a deal whose lines were all removed:\n // the forecast should not drop to zero because a quote was\n // cleared to be retyped.\n ...(read.items.length ? { amountCents: lineItemsTotalCents(read.items) } : {}),\n updatedAt: new Date(),\n })\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return false\n }\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n return false\n } finally {\n setBusy(false)\n }\n },\n [currency, enqueueSnackbar, unreadable, fromCache, firestore, orgId, deal.$id],\n )\n\n const handleRemove = useCallback(\n (index: number) =>\n void save(\n items.filter((_item, at) => at !== index),\n 'Line removed',\n ),\n [items, save],\n )\n const handleQuantity = useCallback(\n (index: number, quantity: number) => {\n if (quantity === items[index]?.quantity) return\n void save(\n items.map((item, at) => (at === index ? { ...item, quantity } : item)),\n 'Quantity saved',\n )\n },\n [items, save],\n )\n const handleAdd = useCallback(\n async (line: CrmDealLineItem) => {\n const ok = await save([...items, line], 'Line added')\n if (ok) setAdding(false)\n },\n [items, save],\n )\n\n const total = lineItemsTotalCents(items)\n const full = items.length >= DEAL_LINE_ITEMS_MAX\n\n return (\n <>\n <CardDisplay\n header={'Products'}\n help={pluginDocsHelp('deals', { anchor: '#line-items' })}\n HeaderProps={{\n action: (\n <Tooltip title={full ? `A deal carries at most ${DEAL_LINE_ITEMS_MAX} lines` : ''}>\n <span>\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={busy || full}\n onClick={() => setAdding(true)}\n >\n {'Add line'}\n </Button>\n </span>\n </Tooltip>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n {items.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {hasCommerce\n ? 'No line items yet. Add products from your catalog or by hand; the amount becomes their sum.'\n : 'No line items yet. Add what the deal is for, priced by hand; the amount becomes their sum.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\" aria-label=\"Line items\">\n <TableHead>\n <TableRow>\n <TableCell>{'Item'}</TableCell>\n <TableCell align=\"right\" sx={{ width: 110 }}>\n {'Quantity'}\n </TableCell>\n <TableCell align=\"right\">{'Unit'}</TableCell>\n <TableCell align=\"right\">{'Total'}</TableCell>\n <TableCell sx={{ width: 48 }} />\n </TableRow>\n </TableHead>\n <TableBody>\n {items.map((item, index) => (\n <TableRow key={`${index}:${item.productId ?? item.name}`}>\n <TableCell>\n <Typography variant=\"body2\">{item.name}</Typography>\n {item.productId ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'From the catalog'}\n </Typography>\n ) : null}\n </TableCell>\n <TableCell align=\"right\">\n <QuantityField\n value={item.quantity}\n disabled={busy}\n label={`Quantity of ${item.name}`}\n onCommit={(quantity) => handleQuantity(index, quantity)}\n />\n </TableCell>\n <TableCell align=\"right\">{formatMoney(item.unitAmountCents, currency)}</TableCell>\n <TableCell align=\"right\">\n {formatMoney(item.quantity * item.unitAmountCents, currency)}\n </TableCell>\n <TableCell padding=\"none\" align=\"right\">\n <Tooltip title=\"Remove line\">\n <span>\n <IconButton\n size=\"small\"\n color=\"error\"\n disabled={busy}\n aria-label={`Remove ${item.name}`}\n onClick={() => handleRemove(index)}\n >\n <MdiIcon path={mdiDeleteOutline.path} size={0.8} />\n </IconButton>\n </span>\n </Tooltip>\n </TableCell>\n </TableRow>\n ))}\n <TableRow>\n <TableCell colSpan={3}>\n <Typography variant=\"subtitle2\">{'Amount'}</Typography>\n </TableCell>\n <TableCell align=\"right\">\n <Typography variant=\"subtitle2\" data-testid=\"line-items-total\">\n {formatMoney(total, currency)}\n </Typography>\n </TableCell>\n <TableCell padding=\"none\" />\n </TableRow>\n </TableBody>\n </ScrollTable>\n )}\n </CardDisplay>\n <AddLineDialog\n open={adding}\n onClose={() => setAdding(false)}\n hostId={catalogHostId}\n currency={currency}\n catalog={hasCommerce && Boolean(catalogHostId)}\n busy={busy}\n onAdd={handleAdd}\n />\n </>\n )\n}\nDealProductsCard.displayName = 'DealProductsCard'\n\ninterface QuantityFieldProps {\n value: number\n disabled: boolean\n label: string\n onCommit: (quantity: number) => void\n}\n\n/** A quantity edited in place, committed on blur or Enter, refused below one. */\nfunction QuantityField(props: QuantityFieldProps) {\n const { value, disabled, label, onCommit } = props\n const [draft, setDraft] = useState(String(value))\n useEffect(() => setDraft(String(value)), [value])\n const commit = () => {\n const quantity = Number(draft)\n if (Number.isInteger(quantity) && quantity >= 1 && quantity <= DEAL_LINE_ITEM_QUANTITY_MAX) {\n onCommit(quantity)\n } else {\n setDraft(String(value))\n }\n }\n return (\n <TextField\n size=\"small\"\n type=\"number\"\n value={draft}\n disabled={disabled}\n onChange={(event) => setDraft(event.target.value)}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n ;(event.target as HTMLInputElement).blur()\n }\n }}\n sx={{ width: 88 }}\n slotProps={{\n htmlInput: {\n min: 1,\n max: DEAL_LINE_ITEM_QUANTITY_MAX,\n step: 1,\n 'aria-label': label,\n style: { textAlign: 'right' },\n },\n }}\n />\n )\n}\nQuantityField.displayName = 'QuantityField'\n\ninterface AddLineDialogProps {\n open: boolean\n onClose: () => void\n /** The site whose catalog is searched; `null` draws no catalog door. */\n hostId: string | null\n currency: string\n /** Whether the catalog door is drawn — the plan has commerce. */\n catalog: boolean\n busy: boolean\n onAdd: (line: CrmDealLineItem) => Promise<void>\n}\n\ntype Door = 'catalog' | 'manual'\n\nfunction AddLineDialog(props: AddLineDialogProps) {\n const { open, onClose, hostId, currency, catalog, busy, onAdd } = props\n const firestore = useFirestore()\n const [door, setDoor] = useState<Door>(catalog ? 'catalog' : 'manual')\n const [picked, setPicked] = useState<CatalogChoice | null>(null)\n const [name, setName] = useState('')\n const [quantity, setQuantity] = useState('1')\n const [unit, setUnit] = useState('')\n const [search, setSearch] = useState('')\n const [choices, setChoices] = useState<CatalogChoice[]>([])\n\n useEffect(() => {\n if (!open) return\n setDoor(catalog ? 'catalog' : 'manual')\n setPicked(null)\n setName('')\n setQuantity('1')\n setUnit('')\n setSearch('')\n setChoices([])\n }, [open, catalog])\n\n /*\n * THE CATALOG SEARCH: the first typed word as a prefix token over active\n * products, ordered by name, debounced so a name typed at speed is one\n * query rather than one per letter. Each variant is its own choice,\n * priced from `priceUsd` in cents.\n */\n useEffect(() => {\n if (!open || door !== 'catalog' || !hostId) return\n const token = nameSearchToken(search)\n if (!token) {\n setChoices([])\n return\n }\n let active = true\n const timer = setTimeout(() => {\n void getDocs(\n query(\n collection(firestore, 'hosts', hostId, 'products'),\n where('status', '==', 'active'),\n where('nameTokens', 'array-contains', token),\n orderBy('nameLower'),\n limit(CATALOG_MATCHES),\n ),\n )\n .then((snapshot) => {\n if (!active) return\n const next: CatalogChoice[] = []\n for (const entry of snapshot.docs) {\n const productName = String(entry.get('name') ?? '')\n const variants = (entry.get('variants') as Array<Record<string, unknown>> | undefined) ?? []\n const priced = variants.length\n ? variants\n : [{ id: '', priceUsd: entry.get('priceUsd') }]\n for (const variant of priced) {\n const options = Object.values((variant['options'] as Record<string, string>) ?? {})\n const label = options.length ? `${productName} — ${options.join(' / ')}` : productName\n next.push({\n key: `${entry.id}:${String(variant['id'] ?? '')}`,\n productId: entry.id,\n label,\n unitAmountCents: Math.max(0, Math.round((Number(variant['priceUsd']) || 0) * 100)),\n })\n }\n }\n setChoices(next)\n })\n .catch(() => {\n if (active) setChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, door, hostId, search])\n\n const unitCents = parseAmountInput(unit)\n const quantityNumber = Number(quantity)\n const lineName = (door === 'catalog' ? (name || picked?.label || '') : name).trim()\n const problem = !lineName\n ? door === 'catalog'\n ? 'Pick a product.'\n : 'A line needs a name.'\n : !Number.isInteger(quantityNumber) || quantityNumber < 1\n ? 'The quantity has to be a whole number of one or more.'\n : unitCents === null\n ? 'The unit amount has to be a number of zero or more.'\n : null\n\n const handleAdd = () => {\n if (problem || unitCents === null) return\n void onAdd({\n ...(door === 'catalog' && picked ? { productId: picked.productId } : {}),\n name: lineName.slice(0, DEAL_LINE_ITEM_NAME_MAX),\n quantity: quantityNumber,\n unitAmountCents: unitCents,\n currency,\n })\n }\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} fullWidth maxWidth=\"xs\">\n <DialogTitle>{'Add a line'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n {catalog ? (\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={door}\n onChange={(_event, next) => {\n if (next) setDoor(next as Door)\n }}\n aria-label=\"Where the line comes from\"\n >\n <ToggleButton value=\"catalog\">{'From the catalog'}</ToggleButton>\n <ToggleButton value=\"manual\">{'By hand'}</ToggleButton>\n </ToggleButtonGroup>\n ) : null}\n {door === 'catalog' ? (\n <Autocomplete<CatalogChoice, false, false, false>\n options={choices}\n value={picked}\n inputValue={search}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setSearch(next)\n }}\n onChange={(_event, choice) => {\n setPicked(choice)\n setName(choice?.label ?? '')\n setUnit(choice ? amountInputValue(choice.unitAmountCents) : '')\n }}\n getOptionLabel={(choice) => choice.label}\n isOptionEqualToValue={(option, value) => option.key === value.key}\n // The match is the query's own prefix over `nameTokens`, never\n // MUI's substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.key}>\n <Stack direction=\"row\" spacing={1} sx={{ width: '100%', alignItems: 'baseline' }}>\n <Typography variant=\"body2\" sx={{ flex: 1 }}>\n {choice.label}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {formatMoney(choice.unitAmountCents, 'usd')}\n </Typography>\n </Stack>\n </li>\n )}\n noOptionsText={search.trim() ? 'No active product matches' : 'Type a product name'}\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Product\"\n placeholder=\"Search this site's catalog\"\n autoFocus\n helperText={\n currency !== 'usd'\n ? `Catalog prices are in USD; this deal is in ${currency.toUpperCase()}. Check the unit amount.`\n : undefined\n }\n />\n )}\n />\n ) : (\n <TextField\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_LINE_ITEM_NAME_MAX } }}\n />\n )}\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Quantity\"\n type=\"number\"\n value={quantity}\n onChange={(event) => setQuantity(event.target.value)}\n sx={{ width: 120 }}\n slotProps={{ htmlInput: { min: 1, max: DEAL_LINE_ITEM_QUANTITY_MAX, step: 1 } }}\n />\n <TextField\n label={`Unit amount (${currency.toUpperCase()})`}\n value={unit}\n onChange={(event) => setUnit(event.target.value)}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n </Stack>\n {problem && (lineName || unit) ? (\n <Typography variant=\"caption\" color=\"warning.main\">\n {problem}\n </Typography>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" disabled={busy || Boolean(problem)} onClick={handleAdd}>\n {'Add line'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nAddLineDialog.displayName = 'AddLineDialog'\n\nexport default DealProductsCard\n"],"names":["checkEntitlement","CRM_COLLECTIONS","DEAL_LINE_ITEM_NAME_MAX","DEAL_LINE_ITEM_QUANTITY_MAX","DEAL_LINE_ITEMS_MAX","lineItemsTotalCents","nameSearchToken","pluginDocsHelp","readDealLineItems","mdiDeleteOutline","mdiPlus","CardDisplay","MdiIcon","ScrollTable","useSnackbar","useFirestore","writeGuardedBySeed","Autocomplete","Button","Dialog","DialogActions","DialogContent","DialogTitle","IconButton","Stack","TableBody","TableCell","TableHead","TableRow","TextField","ToggleButton","ToggleButtonGroup","Tooltip","Typography","collection","doc","getDocs","limit","orderBy","query","updateDoc","where","useCallback","useEffect","useMemo","useState","amountInputValue","DEFAULT_DEAL_CURRENCY","formatMoney","parseAmountInput","CATALOG_MATCHES","DealProductsCard","props","hostId","deal","orgId","org","fromCache","unreadable","firestore","catalogHostId","enqueueSnackbar","currency","String","toLowerCase","items","lineItems","hasCommerce","busy","setBusy","adding","setAdding","save","next","done","read","error","variant","persist","verdict","subject","deals","$id","length","amountCents","updatedAt","Date","ok","message","console","allowDuplicate","handleRemove","index","filter","_item","at","handleQuantity","quantity","map","item","handleAdd","line","total","full","header","help","anchor","HeaderProps","action","title","span","size","startIcon","path","disabled","onClick","contentGutterX","contentGutterY","color","aria-label","align","sx","width","name","productId","QuantityField","value","label","onCommit","unitAmountCents","padding","colSpan","data-testid","AddLineDialog","open","onClose","catalog","Boolean","onAdd","displayName","draft","setDraft","commit","Number","isInteger","type","onChange","event","target","onBlur","onKeyDown","key","preventDefault","blur","slotProps","htmlInput","min","max","step","style","textAlign","door","setDoor","picked","setPicked","setName","setQuantity","unit","setUnit","search","setSearch","choices","setChoices","token","active","timer","setTimeout","then","snapshot","entry","docs","productName","get","variants","priced","id","priceUsd","options","Object","values","join","push","Math","round","catch","clearTimeout","unitCents","quantityNumber","lineName","trim","problem","slice","undefined","fullWidth","maxWidth","spacing","pt","exclusive","_event","inputValue","onInputChange","reason","choice","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","direction","alignItems","flex","noOptionsText","renderInput","params","placeholder","autoFocus","helperText","toUpperCase","maxLength","inputMode"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-products-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n checkEntitlement,\n CRM_COLLECTIONS,\n type CrmDealLineItem,\n DEAL_LINE_ITEM_NAME_MAX,\n DEAL_LINE_ITEM_QUANTITY_MAX,\n DEAL_LINE_ITEMS_MAX,\n lineItemsTotalCents,\n pluginDocsHelp,\n readDealLineItems,\n} from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Autocomplete,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n IconButton,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport { doc, getDocs, updateDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { pluginRecordListSource } from '@aglyn/aglyn/plugin-manager/plugin-record-lists'\nimport type { CrmOrgDoc } from '../hooks/use-crm-scope'\nimport {\n amountInputValue,\n type DealDoc,\n DEFAULT_DEAL_CURRENCY,\n formatMoney,\n parseAmountInput,\n} from '../model/deal-board-model'\n\n/** Catalog matches offered per keystroke; a longer list is a scroll, not a pick. */\nconst CATALOG_MATCHES = 8\n\n/** One thing the catalog search offers: a product, or one variant of it. */\ninterface CatalogChoice {\n key: string\n productId: string\n label: string\n unitAmountCents: number\n}\n\nexport interface DealProductsCardProps {\n deal: DealDoc\n orgId: string\n /**\n * The site whose catalog the search reads — the console's own, or\n * `null` at the organization level (AGL-2630), where the deal's own\n * capturing site is searched; a deal with none takes lines by hand only.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The listener's verdict on the deal, for the stale-seed guard. */\n fromCache: boolean\n unreadable: boolean\n}\n\n/**\n * The products behind a deal's amount (AGL-2620).\n *\n * ## The amount follows the lines\n *\n * Every write here stores `lineItems` AND `amountCents` as their sum, in\n * one update, so the board, the reports and the API keep reading the one\n * field they always read and no reader adds up lines. Once a deal has a\n * line, the drawer's Amount goes read-only and says why; remove the last\n * line and the amount is typed again, keeping the last sum rather than\n * dropping to nothing. The currency is the deal's — a line cannot be in\n * another, because their sum is one number.\n *\n * ## Two doors to a line\n *\n * **From the catalog** searches THIS site's products — a prefix on\n * `nameTokens` over active products, the query the products hub runs and\n * the `(status, nameTokens, nameLower)` index serves — and offers each\n * variant priced. The catalog is USD; a deal in another currency is told\n * so and can edit the unit amount after. **By hand** takes a name and a\n * price with no product behind them, which is also all a plan without\n * commerce gets: the catalog door is not drawn for it, because the site\n * has no catalog to search.\n *\n * Adding opens a dialog rather than a row of fields above the table, and\n * a quantity is edited in place and written on blur.\n */\nexport function DealProductsCard(props: DealProductsCardProps) {\n const { deal, orgId, hostId, org, fromCache, unreadable } = props\n const firestore = useFirestore()\n // The catalog searched: the mounted site's, or the deal's own.\n const catalogHostId = hostId ?? deal.hostId ?? null\n const { enqueueSnackbar } = useSnackbar()\n const currency = String(deal.currency || DEFAULT_DEAL_CURRENCY).toLowerCase()\n const items = useMemo(() => deal.lineItems ?? [], [deal.lineItems])\n const hasCommerce = checkEntitlement(\n (org ?? null) as Partial<AglynOrgBilling> | null,\n 'commerce',\n )\n const [busy, setBusy] = useState(false)\n const [adding, setAdding] = useState(false)\n\n /** The one write: the lines and their sum, together. */\n const save = useCallback(\n async (next: CrmDealLineItem[], done: string) => {\n const read = readDealLineItems(next, currency)\n if ('error' in read) {\n enqueueSnackbar(read.error, { variant: 'warning', persist: false })\n return false\n }\n setBusy(true)\n try {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id), {\n lineItems: read.items,\n // The last sum stays on a deal whose lines were all removed:\n // the forecast should not drop to zero because a quote was\n // cleared to be retyped.\n ...(read.items.length ? { amountCents: lineItemsTotalCents(read.items) } : {}),\n updatedAt: new Date(),\n })\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return false\n }\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n return false\n } finally {\n setBusy(false)\n }\n },\n [currency, enqueueSnackbar, unreadable, fromCache, firestore, orgId, deal.$id],\n )\n\n const handleRemove = useCallback(\n (index: number) =>\n void save(\n items.filter((_item, at) => at !== index),\n 'Line removed',\n ),\n [items, save],\n )\n const handleQuantity = useCallback(\n (index: number, quantity: number) => {\n if (quantity === items[index]?.quantity) return\n void save(\n items.map((item, at) => (at === index ? { ...item, quantity } : item)),\n 'Quantity saved',\n )\n },\n [items, save],\n )\n const handleAdd = useCallback(\n async (line: CrmDealLineItem) => {\n const ok = await save([...items, line], 'Line added')\n if (ok) setAdding(false)\n },\n [items, save],\n )\n\n const total = lineItemsTotalCents(items)\n const full = items.length >= DEAL_LINE_ITEMS_MAX\n\n return (\n <>\n <CardDisplay\n header={'Products'}\n help={pluginDocsHelp('deals', { anchor: '#line-items' })}\n HeaderProps={{\n action: (\n <Tooltip title={full ? `A deal carries at most ${DEAL_LINE_ITEMS_MAX} lines` : ''}>\n <span>\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={busy || full}\n onClick={() => setAdding(true)}\n >\n {'Add line'}\n </Button>\n </span>\n </Tooltip>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n {items.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {hasCommerce\n ? 'No line items yet. Add products from your catalog or by hand; the amount becomes their sum.'\n : 'No line items yet. Add what the deal is for, priced by hand; the amount becomes their sum.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\" aria-label=\"Line items\">\n <TableHead>\n <TableRow>\n <TableCell>{'Item'}</TableCell>\n <TableCell align=\"right\" sx={{ width: 110 }}>\n {'Quantity'}\n </TableCell>\n <TableCell align=\"right\">{'Unit'}</TableCell>\n <TableCell align=\"right\">{'Total'}</TableCell>\n <TableCell sx={{ width: 48 }} />\n </TableRow>\n </TableHead>\n <TableBody>\n {items.map((item, index) => (\n <TableRow key={`${index}:${item.productId ?? item.name}`}>\n <TableCell>\n <Typography variant=\"body2\">{item.name}</Typography>\n {item.productId ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'From the catalog'}\n </Typography>\n ) : null}\n </TableCell>\n <TableCell align=\"right\">\n <QuantityField\n value={item.quantity}\n disabled={busy}\n label={`Quantity of ${item.name}`}\n onCommit={(quantity) => handleQuantity(index, quantity)}\n />\n </TableCell>\n <TableCell align=\"right\">{formatMoney(item.unitAmountCents, currency)}</TableCell>\n <TableCell align=\"right\">\n {formatMoney(item.quantity * item.unitAmountCents, currency)}\n </TableCell>\n <TableCell padding=\"none\" align=\"right\">\n <Tooltip title=\"Remove line\">\n <span>\n <IconButton\n size=\"small\"\n color=\"error\"\n disabled={busy}\n aria-label={`Remove ${item.name}`}\n onClick={() => handleRemove(index)}\n >\n <MdiIcon path={mdiDeleteOutline.path} size={0.8} />\n </IconButton>\n </span>\n </Tooltip>\n </TableCell>\n </TableRow>\n ))}\n <TableRow>\n <TableCell colSpan={3}>\n <Typography variant=\"subtitle2\">{'Amount'}</Typography>\n </TableCell>\n <TableCell align=\"right\">\n <Typography variant=\"subtitle2\" data-testid=\"line-items-total\">\n {formatMoney(total, currency)}\n </Typography>\n </TableCell>\n <TableCell padding=\"none\" />\n </TableRow>\n </TableBody>\n </ScrollTable>\n )}\n </CardDisplay>\n <AddLineDialog\n open={adding}\n onClose={() => setAdding(false)}\n hostId={catalogHostId}\n currency={currency}\n catalog={hasCommerce && Boolean(catalogHostId)}\n busy={busy}\n onAdd={handleAdd}\n />\n </>\n )\n}\nDealProductsCard.displayName = 'DealProductsCard'\n\ninterface QuantityFieldProps {\n value: number\n disabled: boolean\n label: string\n onCommit: (quantity: number) => void\n}\n\n/** A quantity edited in place, committed on blur or Enter, refused below one. */\nfunction QuantityField(props: QuantityFieldProps) {\n const { value, disabled, label, onCommit } = props\n const [draft, setDraft] = useState(String(value))\n useEffect(() => setDraft(String(value)), [value])\n const commit = () => {\n const quantity = Number(draft)\n if (Number.isInteger(quantity) && quantity >= 1 && quantity <= DEAL_LINE_ITEM_QUANTITY_MAX) {\n onCommit(quantity)\n } else {\n setDraft(String(value))\n }\n }\n return (\n <TextField\n size=\"small\"\n type=\"number\"\n value={draft}\n disabled={disabled}\n onChange={(event) => setDraft(event.target.value)}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n ;(event.target as HTMLInputElement).blur()\n }\n }}\n sx={{ width: 88 }}\n slotProps={{\n htmlInput: {\n min: 1,\n max: DEAL_LINE_ITEM_QUANTITY_MAX,\n step: 1,\n 'aria-label': label,\n style: { textAlign: 'right' },\n },\n }}\n />\n )\n}\nQuantityField.displayName = 'QuantityField'\n\ninterface AddLineDialogProps {\n open: boolean\n onClose: () => void\n /** The site whose catalog is searched; `null` draws no catalog door. */\n hostId: string | null\n currency: string\n /** Whether the catalog door is drawn — the plan has commerce. */\n catalog: boolean\n busy: boolean\n onAdd: (line: CrmDealLineItem) => Promise<void>\n}\n\ntype Door = 'catalog' | 'manual'\n\nfunction AddLineDialog(props: AddLineDialogProps) {\n const { open, onClose, hostId, currency, catalog, busy, onAdd } = props\n const firestore = useFirestore()\n const [door, setDoor] = useState<Door>(catalog ? 'catalog' : 'manual')\n const [picked, setPicked] = useState<CatalogChoice | null>(null)\n const [name, setName] = useState('')\n const [quantity, setQuantity] = useState('1')\n const [unit, setUnit] = useState('')\n const [search, setSearch] = useState('')\n const [choices, setChoices] = useState<CatalogChoice[]>([])\n\n useEffect(() => {\n if (!open) return\n setDoor(catalog ? 'catalog' : 'manual')\n setPicked(null)\n setName('')\n setQuantity('1')\n setUnit('')\n setSearch('')\n setChoices([])\n }, [open, catalog])\n\n /*\n * THE CATALOG SEARCH: what is typed, matched the way the plugin that keeps\n * products matches it — through the source it publishes (AGL-3080), which\n * offers active products only — debounced so a name typed at speed is one\n * query rather than one per letter. Nothing typed, or no plugin keeping\n * products, matches nothing. Each variant is its own choice, priced from\n * its `priceUsd` in cents.\n */\n useEffect(() => {\n if (!open || door !== 'catalog' || !hostId) return\n const products = pluginRecordListSource('product')?.source ?? null\n const catalogQuery = products?.query(firestore, { hostId, search, limit: CATALOG_MATCHES }) ?? null\n if (!products || !catalogQuery) {\n setChoices([])\n return\n }\n let active = true\n const timer = setTimeout(() => {\n void getDocs(catalogQuery)\n .then((snapshot) => {\n if (!active) return\n const next: CatalogChoice[] = []\n for (const entry of snapshot.docs) {\n const product = products.record(entry.id, entry.data())\n if (!product) continue\n const variants = Array.isArray(product.facts['variants'])\n ? (product.facts['variants'] as Array<Record<string, unknown>>)\n : []\n const priced = variants.length\n ? variants\n : [{ id: '', options: {}, priceUsd: product.facts['priceUsd'] }]\n for (const variant of priced) {\n const options = Object.values((variant['options'] as Record<string, string>) ?? {})\n const label = options.length ? `${product.name} — ${options.join(' / ')}` : product.name\n next.push({\n key: `${product.id}:${String(variant['id'] ?? '')}`,\n productId: product.id,\n label,\n unitAmountCents: Math.max(0, Math.round((Number(variant['priceUsd']) || 0) * 100)),\n })\n }\n }\n setChoices(next)\n })\n .catch(() => {\n if (active) setChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, door, hostId, search])\n\n const unitCents = parseAmountInput(unit)\n const quantityNumber = Number(quantity)\n const lineName = (door === 'catalog' ? (name || picked?.label || '') : name).trim()\n const problem = !lineName\n ? door === 'catalog'\n ? 'Pick a product.'\n : 'A line needs a name.'\n : !Number.isInteger(quantityNumber) || quantityNumber < 1\n ? 'The quantity has to be a whole number of one or more.'\n : unitCents === null\n ? 'The unit amount has to be a number of zero or more.'\n : null\n\n const handleAdd = () => {\n if (problem || unitCents === null) return\n void onAdd({\n ...(door === 'catalog' && picked ? { productId: picked.productId } : {}),\n name: lineName.slice(0, DEAL_LINE_ITEM_NAME_MAX),\n quantity: quantityNumber,\n unitAmountCents: unitCents,\n currency,\n })\n }\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} fullWidth maxWidth=\"xs\">\n <DialogTitle>{'Add a line'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n {catalog ? (\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={door}\n onChange={(_event, next) => {\n if (next) setDoor(next as Door)\n }}\n aria-label=\"Where the line comes from\"\n >\n <ToggleButton value=\"catalog\">{'From the catalog'}</ToggleButton>\n <ToggleButton value=\"manual\">{'By hand'}</ToggleButton>\n </ToggleButtonGroup>\n ) : null}\n {door === 'catalog' ? (\n <Autocomplete<CatalogChoice, false, false, false>\n options={choices}\n value={picked}\n inputValue={search}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setSearch(next)\n }}\n onChange={(_event, choice) => {\n setPicked(choice)\n setName(choice?.label ?? '')\n setUnit(choice ? amountInputValue(choice.unitAmountCents) : '')\n }}\n getOptionLabel={(choice) => choice.label}\n isOptionEqualToValue={(option, value) => option.key === value.key}\n // The match is the query's own prefix over `nameTokens`, never\n // MUI's substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.key}>\n <Stack direction=\"row\" spacing={1} sx={{ width: '100%', alignItems: 'baseline' }}>\n <Typography variant=\"body2\" sx={{ flex: 1 }}>\n {choice.label}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {formatMoney(choice.unitAmountCents, 'usd')}\n </Typography>\n </Stack>\n </li>\n )}\n noOptionsText={search.trim() ? 'No active product matches' : 'Type a product name'}\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Product\"\n placeholder=\"Search this site's catalog\"\n autoFocus\n helperText={\n currency !== 'usd'\n ? `Catalog prices are in USD; this deal is in ${currency.toUpperCase()}. Check the unit amount.`\n : undefined\n }\n />\n )}\n />\n ) : (\n <TextField\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_LINE_ITEM_NAME_MAX } }}\n />\n )}\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Quantity\"\n type=\"number\"\n value={quantity}\n onChange={(event) => setQuantity(event.target.value)}\n sx={{ width: 120 }}\n slotProps={{ htmlInput: { min: 1, max: DEAL_LINE_ITEM_QUANTITY_MAX, step: 1 } }}\n />\n <TextField\n label={`Unit amount (${currency.toUpperCase()})`}\n value={unit}\n onChange={(event) => setUnit(event.target.value)}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n </Stack>\n {problem && (lineName || unit) ? (\n <Typography variant=\"caption\" color=\"warning.main\">\n {problem}\n </Typography>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" disabled={busy || Boolean(problem)} onClick={handleAdd}>\n {'Add line'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nAddLineDialog.displayName = 'AddLineDialog'\n\nexport default DealProductsCard\n"],"names":["checkEntitlement","CRM_COLLECTIONS","DEAL_LINE_ITEM_NAME_MAX","DEAL_LINE_ITEM_QUANTITY_MAX","DEAL_LINE_ITEMS_MAX","lineItemsTotalCents","pluginDocsHelp","readDealLineItems","mdiDeleteOutline","mdiPlus","CardDisplay","MdiIcon","ScrollTable","useSnackbar","useFirestore","writeGuardedBySeed","Autocomplete","Button","Dialog","DialogActions","DialogContent","DialogTitle","IconButton","Stack","TableBody","TableCell","TableHead","TableRow","TextField","ToggleButton","ToggleButtonGroup","Tooltip","Typography","doc","getDocs","updateDoc","useCallback","useEffect","useMemo","useState","pluginRecordListSource","amountInputValue","DEFAULT_DEAL_CURRENCY","formatMoney","parseAmountInput","CATALOG_MATCHES","DealProductsCard","props","hostId","deal","orgId","org","fromCache","unreadable","firestore","catalogHostId","enqueueSnackbar","currency","String","toLowerCase","items","lineItems","hasCommerce","busy","setBusy","adding","setAdding","save","next","done","read","error","variant","persist","verdict","subject","deals","$id","length","amountCents","updatedAt","Date","ok","message","console","allowDuplicate","handleRemove","index","filter","_item","at","handleQuantity","quantity","map","item","handleAdd","line","total","full","header","help","anchor","HeaderProps","action","title","span","size","startIcon","path","disabled","onClick","contentGutterX","contentGutterY","color","aria-label","align","sx","width","name","productId","QuantityField","value","label","onCommit","unitAmountCents","padding","colSpan","data-testid","AddLineDialog","open","onClose","catalog","Boolean","onAdd","displayName","draft","setDraft","commit","Number","isInteger","type","onChange","event","target","onBlur","onKeyDown","key","preventDefault","blur","slotProps","htmlInput","min","max","step","style","textAlign","door","setDoor","picked","setPicked","setName","setQuantity","unit","setUnit","search","setSearch","choices","setChoices","products","source","catalogQuery","query","limit","active","timer","setTimeout","then","snapshot","entry","docs","product","record","id","data","variants","Array","isArray","facts","priced","options","priceUsd","Object","values","join","push","Math","round","catch","clearTimeout","unitCents","quantityNumber","lineName","trim","problem","slice","undefined","fullWidth","maxWidth","spacing","pt","exclusive","_event","inputValue","onInputChange","reason","choice","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","direction","alignItems","flex","noOptionsText","renderInput","params","placeholder","autoFocus","helperText","toUpperCase","maxLength","inputMode"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ContainerPickerOption } from '@aglyn/tenant-feature-instance/components/container-picker';
|
|
2
2
|
import { type FirestoreDocStatus } from '@aglyn/tenant-feature-instance';
|
|
3
3
|
/** The helper line under the picker — the contact card's, word for word. */
|
|
4
4
|
export declare const LEAD_CAMPAIGNS_HELPER_TEXT = "Your own filing. It never adds anyone to a send \u2014 a campaign mails its lists.";
|
|
@@ -11,7 +11,7 @@ export declare const LEAD_CAMPAIGNS_HELPER_TEXT = "Your own filing. It never add
|
|
|
11
11
|
* looks like a fault. The picker below keeps such an id visible as itself,
|
|
12
12
|
* so the filing is never silently lost; the header only ever names.
|
|
13
13
|
*/
|
|
14
|
-
export declare function leadCampaignNames(lead: Record<string, unknown> | null | undefined, options: readonly
|
|
14
|
+
export declare function leadCampaignNames(lead: Record<string, unknown> | null | undefined, options: readonly ContainerPickerOption[]): string[];
|
|
15
15
|
export interface LeadCampaignsCardProps {
|
|
16
16
|
/**
|
|
17
17
|
* The lead's own site, or `null` at the organization level for a lead no
|
|
@@ -26,7 +26,7 @@ export interface LeadCampaignsCardProps {
|
|
|
26
26
|
/** The listener has not confirmed this document with the server yet. */
|
|
27
27
|
fromCache: boolean;
|
|
28
28
|
/** The site's campaigns, read once by the page for this card and the header. */
|
|
29
|
-
options: readonly
|
|
29
|
+
options: readonly ContainerPickerOption[];
|
|
30
30
|
/** The campaign read has answered — false while it settles. */
|
|
31
31
|
optionsReady: boolean;
|
|
32
32
|
/**
|
|
@@ -51,7 +51,7 @@ export interface LeadCampaignsCardProps {
|
|
|
51
51
|
* is the lead's own client door — a one-field `update` on
|
|
52
52
|
* `hosts/{hostId}/leads`, as the properties card writes status and notes —
|
|
53
53
|
* guarded by the seed like every draft edited over a cached read, and
|
|
54
|
-
* `
|
|
54
|
+
* `containerMembershipUnchanged` keeps Save quiet while the picker only
|
|
55
55
|
* reorders what is stored. **Save filing** sits in the card header.
|
|
56
56
|
*
|
|
57
57
|
* It is FILING, not audience: the helper says so, in the sentence the
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
18
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
19
|
+
import { containerMembershipUnchanged, containerMembershipValue, pluginDocsHelp, readContainerIds } from "@aglyn/aglyn";
|
|
20
|
+
import ContainerPicker from "@aglyn/tenant-feature-instance/components/container-picker";
|
|
21
21
|
import { CardDisplay } from "@aglyn/shared-ui-jsx";
|
|
22
22
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
23
23
|
import { useFirestore, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
|
|
@@ -44,7 +44,7 @@ import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
|
|
|
44
44
|
option.value,
|
|
45
45
|
option.label
|
|
46
46
|
]));
|
|
47
|
-
return
|
|
47
|
+
return readContainerIds(lead, 'campaign').map((id)=>labels.get(id)).filter((name)=>Boolean(name));
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).
|
|
@@ -58,7 +58,7 @@ import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
|
|
|
58
58
|
* is the lead's own client door — a one-field `update` on
|
|
59
59
|
* `hosts/{hostId}/leads`, as the properties card writes status and notes —
|
|
60
60
|
* guarded by the seed like every draft edited over a cached read, and
|
|
61
|
-
* `
|
|
61
|
+
* `containerMembershipUnchanged` keeps Save quiet while the picker only
|
|
62
62
|
* reorders what is stored. **Save filing** sits in the card header.
|
|
63
63
|
*
|
|
64
64
|
* It is FILING, not audience: the helper says so, in the sentence the
|
|
@@ -74,7 +74,7 @@ import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
|
|
|
74
74
|
const followUpSharing = useCrmSharingFollowUp(hostId, orgId);
|
|
75
75
|
const firestore = useFirestore();
|
|
76
76
|
const { enqueueSnackbar } = useSnackbar();
|
|
77
|
-
const stored =
|
|
77
|
+
const stored = readContainerIds(lead, 'campaign');
|
|
78
78
|
const [selected, setSelected] = useState(stored);
|
|
79
79
|
const [dirty, setDirty] = useState(false);
|
|
80
80
|
const [saving, setSaving] = useState(false);
|
|
@@ -89,7 +89,7 @@ import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
|
|
|
89
89
|
storedKey,
|
|
90
90
|
dirty
|
|
91
91
|
]);
|
|
92
|
-
const unchanged =
|
|
92
|
+
const unchanged = containerMembershipUnchanged(stored, selected);
|
|
93
93
|
const empty = optionsReady && !options.length;
|
|
94
94
|
const save = async ()=>{
|
|
95
95
|
if (unchanged) return;
|
|
@@ -101,7 +101,7 @@ import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
|
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
setSaving(true);
|
|
104
|
-
const next =
|
|
104
|
+
const next = containerMembershipValue(selected);
|
|
105
105
|
const verdict = await writeGuardedBySeed({
|
|
106
106
|
subject: 'lead',
|
|
107
107
|
fromCache,
|
|
@@ -162,7 +162,8 @@ import { useCrmSharingFollowUp } from "../hooks/use-crm-sharing.js";
|
|
|
162
162
|
},
|
|
163
163
|
children: /*#__PURE__*/ _jsx(Stack, {
|
|
164
164
|
spacing: 2,
|
|
165
|
-
children: /*#__PURE__*/ _jsx(
|
|
165
|
+
children: /*#__PURE__*/ _jsx(ContainerPicker, {
|
|
166
|
+
kind: "campaign",
|
|
166
167
|
options: options,
|
|
167
168
|
value: selected,
|
|
168
169
|
onChange: (next)=>{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-campaigns-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n campaignMembershipUnchanged,\n campaignMembershipValue,\n pluginDocsHelp,\n readCampaignIds,\n} from '@aglyn/aglyn'\nimport CampaignPicker, {\n type CampaignPickerOption,\n} from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack } from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { crmClientListFields } from '../model/crm-list-query'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\n/** The list field a campaign filing moves (AGL-3321). */\nconst LEAD_CAMPAIGN_LIST_FIELDS = ['scopedCampaignIds'] as const\n\n/** The helper line under the picker — the contact card's, word for word. */\nexport const LEAD_CAMPAIGNS_HELPER_TEXT =\n 'Your own filing. It never adds anyone to a send — a campaign mails its lists.'\n\n/**\n * The campaigns a lead is filed under, as NAMES for the record header.\n *\n * Resolved from the site's containers; an id with no live container — a\n * campaign the console deleted, or one still loading — is left out rather\n * than shown, because a document id on a chip tells a reader nothing and\n * looks like a fault. The picker below keeps such an id visible as itself,\n * so the filing is never silently lost; the header only ever names.\n */\nexport function leadCampaignNames(\n lead: Record<string, unknown> | null | undefined,\n options: readonly CampaignPickerOption[],\n): string[] {\n const labels = new Map(options.map((option) => [option.value, option.label]))\n return readCampaignIds(lead)\n .map((id) => labels.get(id))\n .filter((name): name is string => Boolean(name))\n}\n\nexport interface LeadCampaignsCardProps {\n /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Read for the org root alone — the containers\n * themselves are handed down as `options` — so the card serves both\n * levels; the org-level mount answers the same org without a site.\n */\n hostId: string | null\n leadId: string\n lead: Record<string, unknown>\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n /** The site's campaigns, read once by the page for this card and the header. */\n options: readonly CampaignPickerOption[]\n /** The campaign read has answered — false while it settles. */\n optionsReady: boolean\n /**\n * What a saved filing changed, once it has landed: the ids added to and\n * removed from the lead's membership. The page files the activity\n * entries (AGL-3274); the card only knows the document.\n */\n onFiled?: (change: { added: string[]; removed: string[] }) => void\n}\n\n/**\n * WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).\n *\n * The contact's Relationship card has carried this picker since AGL-2596;\n * the lead's page had none, so a lead filed under a campaign by the New\n * lead drawer, the bulk bar or a sequence's enroll showed nothing of it —\n * the one field on the record the reader had no way to see or change.\n *\n * The same control, the same value helper, the same helper line. The save\n * is the lead's own client door — a one-field `update` on\n * `hosts/{hostId}/leads`, as the properties card writes status and notes —\n * guarded by the seed like every draft edited over a cached read, and\n * `campaignMembershipUnchanged` keeps Save quiet while the picker only\n * reorders what is stored. **Save filing** sits in the card header.\n *\n * It is FILING, not audience: the helper says so, in the sentence the\n * contact card uses, because a reader who finds a campaign picker on a\n * person's page assumes the opposite.\n */\nexport function LeadCampaignsCard(props: LeadCampaignsCardProps) {\n const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const stored = readCampaignIds(lead)\n const [selected, setSelected] = useState<string[]>(stored)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n // A newer membership from the server replaces an UNEDITED selection — a\n // sequence's enroll or a bulk add lands while the page is open — and an\n // edited one is the reader's, decided by the guard on save.\n const storedKey = stored.join(',')\n useEffect(() => {\n if (!dirty) setSelected(stored)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storedKey, dirty])\n\n const unchanged = campaignMembershipUnchanged(stored, selected)\n const empty = optionsReady && !options.length\n\n const save = async () => {\n if (unchanged) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setSaving(true)\n const next = campaignMembershipValue(selected)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n campaignIds: next,\n // The Leads list's Campaign filter under a site reads these\n // (AGL-3321), from the lead with its new campaigns.\n ...crmClientListFields('leads', lead, { campaignIds: next }, LEAD_CAMPAIGN_LIST_FIELDS),\n updatedAt: serverTimestamp(),\n })\n },\n )\n setSaving(false)\n // A rule may share by campaign (AGL-3336).\n if (verdict.ok) followUpSharing('leads', [leadId])\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The filing could not be saved.', { variant: 'warning' })\n return\n }\n setDirty(false)\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n onFiled?.({\n added: next.filter((id) => !stored.includes(id)),\n removed: stored.filter((id) => !next.includes(id)),\n })\n }\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: empty ? null : (\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || unchanged}\n onClick={() => void save().catch((error: unknown) => {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n setSaving(false)\n })}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <CampaignPicker\n options={options}\n value={selected}\n onChange={(next) => {\n setSelected(next)\n setDirty(true)\n }}\n label=\"Filed under campaigns\"\n helperText={LEAD_CAMPAIGNS_HELPER_TEXT}\n disabled={saving}\n empty={empty}\n emptyText=\"There are no campaigns to file this lead under yet. Create one from Marketing.\"\n />\n </Stack>\n </CardDisplay>\n )\n}\nLeadCampaignsCard.displayName = 'LeadCampaignsCard'\n\nexport default LeadCampaignsCard\n"],"names":["campaignMembershipUnchanged","campaignMembershipValue","pluginDocsHelp","readCampaignIds","CampaignPicker","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Stack","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","crmClientListFields","useCrmSharingFollowUp","LEAD_CAMPAIGN_LIST_FIELDS","LEAD_CAMPAIGNS_HELPER_TEXT","leadCampaignNames","lead","options","labels","Map","map","option","value","label","id","get","filter","name","Boolean","LeadCampaignsCard","props","hostId","leadId","leadStatus","fromCache","optionsReady","onFiled","orgId","followUpSharing","firestore","enqueueSnackbar","stored","selected","setSelected","dirty","setDirty","saving","setSaving","storedKey","join","unchanged","empty","length","save","variant","persist","next","verdict","subject","unreadable","campaignIds","updatedAt","ok","message","added","includes","removed","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","disabled","onClick","catch","error","console","Error","allowDuplicate","spacing","onChange","helperText","emptyText","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/lead-campaigns-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n containerMembershipUnchanged,\n containerMembershipValue,\n pluginDocsHelp,\n readContainerIds,\n} from '@aglyn/aglyn'\nimport ContainerPicker, {\n type ContainerPickerOption,\n} from '@aglyn/tenant-feature-instance/components/container-picker'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n type FirestoreDocStatus,\n useFirestore,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Stack } from '@mui/material'\nimport { doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useEffect, useState } from 'react'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { crmClientListFields } from '../model/crm-list-query'\nimport { useCrmSharingFollowUp } from '../hooks/use-crm-sharing'\n\n/** The list field a campaign filing moves (AGL-3321). */\nconst LEAD_CAMPAIGN_LIST_FIELDS = ['scopedCampaignIds'] as const\n\n/** The helper line under the picker — the contact card's, word for word. */\nexport const LEAD_CAMPAIGNS_HELPER_TEXT =\n 'Your own filing. It never adds anyone to a send — a campaign mails its lists.'\n\n/**\n * The campaigns a lead is filed under, as NAMES for the record header.\n *\n * Resolved from the site's containers; an id with no live container — a\n * campaign the console deleted, or one still loading — is left out rather\n * than shown, because a document id on a chip tells a reader nothing and\n * looks like a fault. The picker below keeps such an id visible as itself,\n * so the filing is never silently lost; the header only ever names.\n */\nexport function leadCampaignNames(\n lead: Record<string, unknown> | null | undefined,\n options: readonly ContainerPickerOption[],\n): string[] {\n const labels = new Map(options.map((option) => [option.value, option.label]))\n return readContainerIds(lead, 'campaign')\n .map((id) => labels.get(id))\n .filter((name): name is string => Boolean(name))\n}\n\nexport interface LeadCampaignsCardProps {\n /**\n * The lead's own site, or `null` at the organization level for a lead no\n * site captured (AGL-3278). Read for the org root alone — the containers\n * themselves are handed down as `options` — so the card serves both\n * levels; the org-level mount answers the same org without a site.\n */\n hostId: string | null\n leadId: string\n lead: Record<string, unknown>\n leadStatus: FirestoreDocStatus\n /** The listener has not confirmed this document with the server yet. */\n fromCache: boolean\n /** The site's campaigns, read once by the page for this card and the header. */\n options: readonly ContainerPickerOption[]\n /** The campaign read has answered — false while it settles. */\n optionsReady: boolean\n /**\n * What a saved filing changed, once it has landed: the ids added to and\n * removed from the lead's membership. The page files the activity\n * entries (AGL-3274); the card only knows the document.\n */\n onFiled?: (change: { added: string[]; removed: string[] }) => void\n}\n\n/**\n * WHICH CAMPAIGNS THIS LEAD IS FILED UNDER (AGL-3274).\n *\n * The contact's Relationship card has carried this picker since AGL-2596;\n * the lead's page had none, so a lead filed under a campaign by the New\n * lead drawer, the bulk bar or a sequence's enroll showed nothing of it —\n * the one field on the record the reader had no way to see or change.\n *\n * The same control, the same value helper, the same helper line. The save\n * is the lead's own client door — a one-field `update` on\n * `hosts/{hostId}/leads`, as the properties card writes status and notes —\n * guarded by the seed like every draft edited over a cached read, and\n * `containerMembershipUnchanged` keeps Save quiet while the picker only\n * reorders what is stored. **Save filing** sits in the card header.\n *\n * It is FILING, not audience: the helper says so, in the sentence the\n * contact card uses, because a reader who finds a campaign picker on a\n * person's page assumes the opposite.\n */\nexport function LeadCampaignsCard(props: LeadCampaignsCardProps) {\n const { hostId, leadId, lead, leadStatus, fromCache, options, optionsReady, onFiled } = props\n // The lead's collection is the org's now (AGL-3275); the site still names\n // the surface, and the scope hook resolves the org from it.\n const { orgId } = useCrmScope({ hostId })\n const followUpSharing = useCrmSharingFollowUp(hostId, orgId)\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const stored = readContainerIds(lead, 'campaign')\n const [selected, setSelected] = useState<string[]>(stored)\n const [dirty, setDirty] = useState(false)\n const [saving, setSaving] = useState(false)\n // A newer membership from the server replaces an UNEDITED selection — a\n // sequence's enroll or a bulk add lands while the page is open — and an\n // edited one is the reader's, decided by the guard on save.\n const storedKey = stored.join(',')\n useEffect(() => {\n if (!dirty) setSelected(stored)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [storedKey, dirty])\n\n const unchanged = containerMembershipUnchanged(stored, selected)\n const empty = optionsReady && !options.length\n\n const save = async () => {\n if (unchanged) return\n if (!orgId) {\n enqueueSnackbar('Still loading this workspace — try again in a moment.', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n setSaving(true)\n const next = containerMembershipValue(selected)\n const verdict = await writeGuardedBySeed(\n { subject: 'lead', fromCache, unreadable: leadStatus === 'error' },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, 'leads', leadId), {\n campaignIds: next,\n // The Leads list's Campaign filter under a site reads these\n // (AGL-3321), from the lead with its new campaigns.\n ...crmClientListFields('leads', lead, { campaignIds: next }, LEAD_CAMPAIGN_LIST_FIELDS),\n updatedAt: serverTimestamp(),\n })\n },\n )\n setSaving(false)\n // A rule may share by campaign (AGL-3336).\n if (verdict.ok) followUpSharing('leads', [leadId])\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message ?? 'The filing could not be saved.', { variant: 'warning' })\n return\n }\n setDirty(false)\n enqueueSnackbar('Filing saved', { variant: 'success', persist: false })\n onFiled?.({\n added: next.filter((id) => !stored.includes(id)),\n removed: stored.filter((id) => !next.includes(id)),\n })\n }\n\n return (\n <CardDisplay\n header={'Campaigns'}\n help={pluginDocsHelp('crmLeads', { anchor: '#a-leads-page' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: empty ? null : (\n <Button\n size=\"small\"\n variant=\"outlined\"\n disabled={saving || unchanged}\n onClick={() => void save().catch((error: unknown) => {\n console.error(error)\n enqueueSnackbar(\n error instanceof Error && error.message ? error.message : 'An error has occurred',\n { variant: 'error', allowDuplicate: true },\n )\n setSaving(false)\n })}\n >\n {saving ? 'Saving…' : 'Save filing'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <ContainerPicker\n kind=\"campaign\"\n options={options}\n value={selected}\n onChange={(next) => {\n setSelected(next)\n setDirty(true)\n }}\n label=\"Filed under campaigns\"\n helperText={LEAD_CAMPAIGNS_HELPER_TEXT}\n disabled={saving}\n empty={empty}\n emptyText=\"There are no campaigns to file this lead under yet. Create one from Marketing.\"\n />\n </Stack>\n </CardDisplay>\n )\n}\nLeadCampaignsCard.displayName = 'LeadCampaignsCard'\n\nexport default LeadCampaignsCard\n"],"names":["containerMembershipUnchanged","containerMembershipValue","pluginDocsHelp","readContainerIds","ContainerPicker","CardDisplay","useSnackbar","useFirestore","writeGuardedBySeed","Button","Stack","doc","serverTimestamp","updateDoc","useEffect","useState","useCrmScope","crmClientListFields","useCrmSharingFollowUp","LEAD_CAMPAIGN_LIST_FIELDS","LEAD_CAMPAIGNS_HELPER_TEXT","leadCampaignNames","lead","options","labels","Map","map","option","value","label","id","get","filter","name","Boolean","LeadCampaignsCard","props","hostId","leadId","leadStatus","fromCache","optionsReady","onFiled","orgId","followUpSharing","firestore","enqueueSnackbar","stored","selected","setSelected","dirty","setDirty","saving","setSaving","storedKey","join","unchanged","empty","length","save","variant","persist","next","verdict","subject","unreadable","campaignIds","updatedAt","ok","message","added","includes","removed","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","size","disabled","onClick","catch","error","console","Error","allowDuplicate","spacing","kind","onChange","helperText","emptyText","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SACEA,4BAA4B,EAC5BC,wBAAwB,EACxBC,cAAc,EACdC,gBAAgB,QACX,eAAc;AACrB,OAAOC,qBAEA,6DAA4D;AACnE,SAASC,WAAW,QAAQ,uBAAsB;AAClD,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAEEC,YAAY,EACZC,kBAAkB,QACb,iCAAgC;AACvC,SAASC,MAAM,EAAEC,KAAK,QAAQ,gBAAe;AAC7C,SAASC,GAAG,EAAEC,eAAe,EAAEC,SAAS,QAAQ,qBAAoB;AACpE,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAC3C,SAASC,WAAW,QAAQ,4BAAwB;AACpD,SAASC,mBAAmB,QAAQ,6BAAyB;AAC7D,SAASC,qBAAqB,QAAQ,8BAA0B;AAEhE,uDAAuD,GACvD,MAAMC,4BAA4B;IAAC;CAAoB;AAEvD,0EAA0E,GAC1E,OAAO,MAAMC,6BACX,gFAA+E;AAEjF;;;;;;;;CAQC,GACD,OAAO,SAASC,kBACdC,IAAgD,EAChDC,OAAyC;IAEzC,MAAMC,SAAS,IAAIC,IAAIF,QAAQG,GAAG,CAAC,CAACC,SAAW;YAACA,OAAOC,KAAK;YAAED,OAAOE,KAAK;SAAC;IAC3E,OAAO1B,iBAAiBmB,MAAM,YAC3BI,GAAG,CAAC,CAACI,KAAON,OAAOO,GAAG,CAACD,KACvBE,MAAM,CAAC,CAACC,OAAyBC,QAAQD;AAC9C;AA2BA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASE,kBAAkBC,KAA6B;IAC7D,MAAM,EAAEC,MAAM,EAAEC,MAAM,EAAEhB,IAAI,EAAEiB,UAAU,EAAEC,SAAS,EAAEjB,OAAO,EAAEkB,YAAY,EAAEC,OAAO,EAAE,GAAGN;IACxF,0EAA0E;IAC1E,4DAA4D;IAC5D,MAAM,EAAEO,KAAK,EAAE,GAAG3B,YAAY;QAAEqB;IAAO;IACvC,MAAMO,kBAAkB1B,sBAAsBmB,QAAQM;IACtD,MAAME,YAAYtC;IAClB,MAAM,EAAEuC,eAAe,EAAE,GAAGxC;IAC5B,MAAMyC,SAAS5C,iBAAiBmB,MAAM;IACtC,MAAM,CAAC0B,UAAUC,YAAY,GAAGlC,SAAmBgC;IACnD,MAAM,CAACG,OAAOC,SAAS,GAAGpC,SAAS;IACnC,MAAM,CAACqC,QAAQC,UAAU,GAAGtC,SAAS;IACrC,wEAAwE;IACxE,wEAAwE;IACxE,4DAA4D;IAC5D,MAAMuC,YAAYP,OAAOQ,IAAI,CAAC;IAC9BzC,UAAU;QACR,IAAI,CAACoC,OAAOD,YAAYF;IACxB,uDAAuD;IACzD,GAAG;QAACO;QAAWJ;KAAM;IAErB,MAAMM,YAAYxD,6BAA6B+C,QAAQC;IACvD,MAAMS,QAAQhB,gBAAgB,CAAClB,QAAQmC,MAAM;IAE7C,MAAMC,OAAO;QACX,IAAIH,WAAW;QACf,IAAI,CAACb,OAAO;YACVG,gBAAgB,yDAAyD;gBACvEc,SAAS;gBACTC,SAAS;YACX;YACA;QACF;QACAR,UAAU;QACV,MAAMS,OAAO7D,yBAAyB+C;QACtC,MAAMe,UAAU,MAAMvD,mBACpB;YAAEwD,SAAS;YAAQxB;YAAWyB,YAAY1B,eAAe;QAAQ,GACjE;YACE,MAAM1B,UAAUF,IAAIkC,WAAW,QAAQF,OAAO,SAASL,SAAS;gBAC9D4B,aAAaJ;eAGV7C,oBAAoB,SAASK,MAAM;gBAAE4C,aAAaJ;YAAK,GAAG3C;gBAC7DgD,WAAWvD;;QAEf;QAEFyC,UAAU;QACV,2CAA2C;QAC3C,IAAIU,QAAQK,EAAE,EAAExB,gBAAgB,SAAS;YAACN;SAAO;QACjD,IAAI,CAACyB,QAAQK,EAAE,EAAE;gBACCL;YAAhBjB,iBAAgBiB,mBAAAA,QAAQM,OAAO,YAAfN,mBAAmB,kCAAkC;gBAAEH,SAAS;YAAU;YAC1F;QACF;QACAT,SAAS;QACTL,gBAAgB,gBAAgB;YAAEc,SAAS;YAAWC,SAAS;QAAM;QACrEnB,2BAAAA,QAAU;YACR4B,OAAOR,KAAK9B,MAAM,CAAC,CAACF,KAAO,CAACiB,OAAOwB,QAAQ,CAACzC;YAC5C0C,SAASzB,OAAOf,MAAM,CAAC,CAACF,KAAO,CAACgC,KAAKS,QAAQ,CAACzC;QAChD;IACF;IAEA,qBACE,KAACzB;QACCoE,QAAQ;QACRC,MAAMxE,eAAe,YAAY;YAAEyE,QAAQ;QAAgB;QAC3DC,cAAc;QACdC,cAAc;QACdC,aAAa;YACXC,QAAQtB,QAAQ,qBACd,KAAChD;gBACCuE,MAAK;gBACLpB,SAAQ;gBACRqB,UAAU7B,UAAUI;gBACpB0B,SAAS,IAAM,KAAKvB,OAAOwB,KAAK,CAAC,CAACC;wBAChCC,QAAQD,KAAK,CAACA;wBACdtC,gBACEsC,iBAAiBE,SAASF,MAAMf,OAAO,GAAGe,MAAMf,OAAO,GAAG,yBAC1D;4BAAET,SAAS;4BAAS2B,gBAAgB;wBAAK;wBAE3ClC,UAAU;oBACZ;0BAECD,SAAS,YAAY;;QAG5B;kBAEA,cAAA,KAAC1C;YAAM8E,SAAS;sBACd,cAAA,KAACpF;gBACCqF,MAAK;gBACLlE,SAASA;gBACTK,OAAOoB;gBACP0C,UAAU,CAAC5B;oBACTb,YAAYa;oBACZX,SAAS;gBACX;gBACAtB,OAAM;gBACN8D,YAAYvE;gBACZ6D,UAAU7B;gBACVK,OAAOA;gBACPmC,WAAU;;;;AAKpB;AACAzD,kBAAkB0D,WAAW,GAAG;AAEhC,eAAe1D,kBAAiB"}
|
|
@@ -499,6 +499,10 @@ const TEXT_MAX = Aglyn.CRM_LEAD_TEXT_MAX;
|
|
|
499
499
|
value: "new",
|
|
500
500
|
children: Aglyn.CRM_LEAD_STATUS_LABELS.new
|
|
501
501
|
}),
|
|
502
|
+
/*#__PURE__*/ _jsx(MenuItem, {
|
|
503
|
+
value: "nurturing",
|
|
504
|
+
children: Aglyn.CRM_LEAD_STATUS_LABELS.nurturing
|
|
505
|
+
}),
|
|
502
506
|
/*#__PURE__*/ _jsx(MenuItem, {
|
|
503
507
|
value: "working",
|
|
504
508
|
children: Aglyn.CRM_LEAD_STATUS_LABELS.working
|