@aglyn/plugins-crm 1.0.0-beta.199 → 1.0.0-beta.200
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/companies-section.d.ts +6 -6
- package/src/lib/components/companies-section.js +54 -84
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +17 -4
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-picker.js +6 -2
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/contact-detail-page.js +14 -2
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -5
- package/src/lib/components/contact-list-columns.js +6 -8
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.js +10 -1
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +115 -163
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.d.ts +4 -2
- package/src/lib/components/crm-next-activity-column.js +6 -3
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +8 -4
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deals-section.d.ts +2 -2
- package/src/lib/components/deals-section.js +59 -84
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.js +11 -3
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +9 -1
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +16 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.d.ts +2 -1
- package/src/lib/components/leads-section.js +110 -148
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/tasks-section.js +128 -121
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +13 -0
- package/src/lib/constants/company-filters.js +47 -17
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +61 -10
- package/src/lib/constants/contact-filters.js +373 -141
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +44 -0
- package/src/lib/constants/deal-filters.js +78 -0
- package/src/lib/constants/deal-filters.js.map +1 -0
- package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
- package/src/lib/hooks/use-crm-list-query.js +182 -0
- package/src/lib/hooks/use-crm-list-query.js.map +1 -0
- package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
- package/src/lib/hooks/use-crm-tasks.js +44 -42
- package/src/lib/hooks/use-crm-tasks.js.map +1 -1
- package/src/lib/hooks/use-deals.d.ts +2 -34
- package/src/lib/hooks/use-deals.js +3 -62
- package/src/lib/hooks/use-deals.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +91 -0
- package/src/lib/model/crm-list-query.js +182 -0
- package/src/lib/model/crm-list-query.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +49 -43
- package/src/lib/model/lead-filters.js +194 -88
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/list-fields-api.d.ts +26 -0
- package/src/lib/model/list-fields-api.js +47 -0
- package/src/lib/model/list-fields-api.js.map +1 -0
- package/src/lib/model/list-fields.d.ts +47 -0
- package/src/lib/model/list-fields.js +39 -0
- package/src/lib/model/list-fields.js.map +1 -0
- package/src/lib/model/task-views.d.ts +15 -10
- package/src/lib/model/task-views.js +68 -9
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/companies-import.js +3 -3
- package/src/lib/server/companies-import.js.map +1 -1
- package/src/lib/server/company-delete.js +3 -1
- package/src/lib/server/company-delete.js.map +1 -1
- package/src/lib/server/consent-group-participant.js +4 -0
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-update.js +14 -8
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.js +4 -2
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/deals-import.js +4 -2
- package/src/lib/server/deals-import.js.map +1 -1
- package/src/lib/server/erase-person.js +2 -1
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/lead-create.js +4 -1
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-source-values.js +9 -5
- package/src/lib/server/lead-source-values.js.map +1 -1
- package/src/lib/server/leads-import.js +4 -1
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server/list-fields-routes.d.ts +29 -0
- package/src/lib/server/list-fields-routes.js +85 -0
- package/src/lib/server/list-fields-routes.js.map +1 -0
- package/src/lib/server/record-email-state.js +5 -2
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-timeline.js +3 -2
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/task-routes.js +6 -3
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server/tasks-import.js +5 -2
- package/src/lib/server/tasks-import.js.map +1 -1
- package/src/lib/server.js +10 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/hooks/use-org-leads.d.ts +0 -50
- package/src/lib/hooks/use-org-leads.js +0 -104
- package/src/lib/hooks/use-org-leads.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-edit-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\nimport {\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n dealHasLineItems,\n findOrgMember,\n nameSearchKey,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteField,\n doc,\n endAt,\n getDocs,\n limit,\n orderBy,\n query,\n startAt,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport {\n type CrmOrgDoc,\n crmScopeListable,\n crmVisibleToClause,\n useCrmScope,\n} from '../hooks/use-crm-scope'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { contactPrimaryGroup } from '../model/contact-record'\nimport {\n type CrmCustomDraft,\n crmCustomDraftDocument,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmSitePicker } from './crm-site-picker'\nimport {\n DEAL_CURRENCIES,\n type DealDoc,\n openStages,\n type PipelineDoc,\n} from '../model/deal-board-model'\nimport {\n type ContactChoice,\n contactChoicesFor,\n DEAL_NOTES_MAX,\n DEAL_TITLE_MAX,\n dealDocumentFromForm,\n dealFormFromDoc,\n dealFormProblem,\n type DealFormValues,\n dealPatchFromForm,\n emptyDealForm,\n} from '../model/deal-form-model'\n\n/**\n * How many recent contacts the picker matches against. The window is the\n * ask — it is read when the drawer opens, not when the section does — and\n * a person outside it is still linkable by pasting their address, which\n * the match tests as a prefix of the email.\n */\nconst CONTACT_WINDOW = 300\n\n/** Companies offered per keystroke; a longer list is a scroll, not a pick. */\nconst COMPANY_MATCHES = 8\n\n/** A company as the picker offers it. */\ninterface CompanyChoice {\n id: string\n name: string\n}\n\nexport interface DealEditDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the drawer is opened under, or `null` at the organization\n * level (AGL-2630), where a NEW deal asks which site captures it and an\n * edit keeps the deal's own.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The deal being edited; absent for a new one. */\n deal?: DealDoc | null\n /** Links and values a new deal opens with — a contact's page passes itself. */\n defaults?: Partial<DealFormValues>\n pipelines: PipelineDoc[]\n /** Where a new deal lands when nobody picks a pipeline. */\n defaultPipeline: PipelineDoc | null\n /** The listener's verdict on the row being edited, for the stale-seed guard. */\n unreadable?: boolean\n fromCache?: boolean\n /** After a save, with the id of the deal written. */\n onSaved?: (dealId: string, mode: 'create' | 'edit') => void\n}\n\n/**\n * The one form a deal is created and edited through (AGL-2598).\n *\n * ## What it writes, and what it does not\n *\n * Client-direct against the rules, like a contact's notes: the title, the\n * amount and its currency, the expected close, the owner, the linked\n * contact and company, and the notes. The stage and the status are shown on\n * a new deal — it has to start somewhere — and never on an existing one,\n * because a stage change is the moment an automation listens for and only\n * the server route can emit that event. A drawer that wrote `stageId`\n * directly would move a deal without anybody hearing it.\n *\n * Nor the amount of a deal that has line items (AGL-2620): it is their\n * sum, the products card on the deal's page owns it, and the field here\n * is read-only with a caption saying so. The pipeline picker on a new\n * deal lists every ACTIVE pipeline (`pipelines`) and opens on the one the\n * caller was looking at.\n *\n * ## The pickers\n *\n * The owner is picked from the org's roster, the company by a server-side\n * prefix on `nameLower` (indexed with the scope), and the contact from a\n * bounded window of the newest contacts matched in memory — see\n * `contactChoicesFor` for why the contact search cannot be a query. Both\n * pickers copy the chosen NAME onto the deal beside the id, so a board of\n * cards can caption itself without a read per card.\n */\nexport function DealEditDrawer(props: DealEditDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n deal,\n defaults,\n pipelines,\n defaultPipeline,\n unreadable = false,\n fromCache = false,\n onSaved,\n } = props\n const mode: 'create' | 'edit' = deal ? 'edit' : 'create'\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const { orgId, consentGroup, visibleTo, createHostId, createTokens } =\n useCrmScope({ hostId, org })\n // The feed the act is logged in is the level it was PERFORMED at\n // (AGL-2738): this site's under a site, the organization's at the org\n // hub. `createHostId` is where the deal is STAMPED, which at the hub is a\n // site the reader picked and may hold no role on — a different question\n // from where the line belongs, and a gate the record write never faced.\n const logActivity = useCrmActivityLogger(hostId)\n const roster = useOrgMemberDirectory(open ? orgId : null)\n\n const [values, setValues] = useState<DealFormValues>(() =>\n emptyDealForm(defaultPipeline, defaults),\n )\n const owner = findOrgMember(roster.members, values.ownerUid)\n const [busy, setBusy] = useState(false)\n /*\n * THE CUSTOM FIELDS (AGL-2661): the org's deal definitions, and the keys\n * the reader has touched over what the deal stores — saved as the\n * difference (`crm-custom-draft`) in the same write as the fixed fields.\n */\n const fields = useContactFieldDefinitions(open ? orgId : null, 'deal')\n const storedCustom = useMemo(() => deal?.custom ?? {}, [deal?.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n // Re-seeded on every open: an existing deal's stored values, or a blank\n // form aimed at the default pipeline with whatever the opener preselected.\n useEffect(() => {\n if (!open) return\n setValues(deal ? dealFormFromDoc(deal) : emptyDealForm(defaultPipeline, defaults))\n setCustom({})\n }, [open, deal, defaultPipeline, defaults])\n\n const update = useCallback(\n (patch: Partial<DealFormValues>) =>\n setValues((current) => ({ ...current, ...patch })),\n [],\n )\n\n const pipeline = useMemo(\n () =>\n pipelines.find((entry) => entry.$id === values.pipelineId) ??\n defaultPipeline,\n [pipelines, values.pipelineId, defaultPipeline],\n )\n const stages = useMemo(() => openStages(pipeline), [pipeline])\n\n /*\n * THE CONTACT WINDOW: the newest contacts this viewer may see, read only\n * while the drawer is open. Scoped and ordered like the contacts list —\n * the same `(visibleTo, updatedAt DESC)` index — and bounded, because the\n * picker's job is to find one person, not to list the audience.\n */\n const { data: contactRows } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n open && orgId && crmScopeListable(visibleTo)\n ? query(\n collection(firestore, 'orgs', orgId, 'contacts'),\n ...crmVisibleToClause(visibleTo),\n orderBy('updatedAt', 'desc'),\n limit(CONTACT_WINDOW),\n )\n : null,\n [firestore, open, orgId, visibleTo],\n { idField: '$id' },\n )\n const [contactQuery, setContactQuery] = useState('')\n // Named through the viewing group under a site; at the organization level\n // through each person's own primary holder.\n const contactChoices = useMemo(\n () =>\n contactChoicesFor(\n contactQuery,\n contactRows ?? [],\n consentGroup?.groupId ??\n ((row) => contactPrimaryGroup(row, org as Record<string, unknown>).groupId),\n ),\n [contactQuery, contactRows, consentGroup, org],\n )\n const contactValue: ContactChoice | null = values.contactId\n ? {\n id: values.contactId,\n name: values.contactName,\n email:\n contactChoices.find((choice) => choice.id === values.contactId)?.email ?? '',\n }\n : null\n\n /*\n * THE COMPANY SEARCH: a prefix on `nameLower` with the scope, which the\n * companies collection indexes. Debounced so a name typed at speed is one\n * query rather than one per letter.\n */\n const [companyQuery, setCompanyQuery] = useState('')\n const [companyChoices, setCompanyChoices] = useState<CompanyChoice[]>([])\n useEffect(() => {\n if (!open || !orgId || !crmScopeListable(visibleTo)) return\n const key = nameSearchKey(companyQuery)\n let active = true\n const timer = setTimeout(() => {\n void getDocs(\n query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n ...(key ? [startAt(key), endAt(`${key}\\uf8ff`)] : []),\n limit(COMPANY_MATCHES),\n ),\n )\n .then((snapshot) => {\n if (!active) return\n setCompanyChoices(\n snapshot.docs.map((entry) => ({\n id: entry.id,\n name: String(entry.get('name') ?? ''),\n })),\n )\n })\n .catch(() => {\n if (active) setCompanyChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, orgId, visibleTo, companyQuery])\n const companyValue: CompanyChoice | null = values.companyId\n ? { id: values.companyId, name: values.companyName }\n : null\n\n const customMissing = crmCustomDraftMissingRequired(fields.active, storedCustom, custom, mode)\n const problem =\n dealFormProblem(values, mode) ??\n (customMissing.length ? `${customMissing.join(', ')} is required.` : null)\n const amountDerived = Boolean(deal && dealHasLineItems(deal))\n\n /*\n * THE RECORDS BAND (AGL-2611), read only while a CREATE is open: a deal\n * is a record of the band the contacts list is banded by, and on an org\n * at a full hard band this drawer refuses the way `upsertHostContact`\n * refuses a capture — same number, same sentence. Memoized, because the\n * tuple is an effect dependency and a fresh one per render would re-read\n * the three aggregates on every keystroke.\n */\n const recordsScope = useMemo(\n () => (open && mode === 'create' && orgId ? (['orgs', orgId] as const) : null),\n [open, mode, orgId],\n )\n const records = useCrmRecordsQuota(recordsScope, org)\n const bandFull = mode === 'create' && records.ready && !records.quota.allowed\n\n const handleSave = useCallback(async () => {\n if (problem || !orgId || !user?.uid) return\n if (bandFull) {\n enqueueSnackbar(CRM_RECORDS_BAND_FULL_MESSAGE, { variant: 'warning', persist: false })\n return\n }\n setBusy(true)\n try {\n const nowMs = Date.now()\n if (deal) {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n const { set, clear } = dealPatchFromForm(values, nowMs, { amountDerived })\n await updateDoc(\n doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id),\n {\n ...set,\n ...Object.fromEntries(clear.map((key) => [key, deleteField()])),\n // The custom keys that changed, merged into the stored map.\n ...crmCustomDraftWrites(storedCustom, custom),\n },\n )\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return\n }\n enqueueSnackbar('Deal saved', { variant: 'success', persist: false })\n onSaved?.(deal.$id, 'edit')\n } else {\n // Held by the button until the capturing site is known; checked\n // again here because a callback can outlive the render that held it.\n if (!createHostId) return\n const customDocument = crmCustomDraftDocument(custom)\n const created = await addDoc(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n {\n ...dealDocumentFromForm(values, {\n visibleTo: [...createTokens],\n hostId: createHostId,\n uid: user.uid,\n nowMs,\n }),\n ...(customDocument ? { custom: customDocument } : {}),\n },\n )\n // Setup → Activity shows CRM work (AGL-2622): the deal is org data,\n // but the act happened in this site's console and belongs in its\n // feed. Creation only — an edit is a save the card reports.\n logActivity('Added deal', { type: 'deal', id: created.id, name: values.title })\n enqueueSnackbar('Deal created', { variant: 'success', persist: false })\n onSaved?.(created.id, 'create')\n }\n onClose()\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [\n problem,\n bandFull,\n orgId,\n user,\n deal,\n values,\n unreadable,\n fromCache,\n firestore,\n createTokens,\n createHostId,\n logActivity,\n enqueueSnackbar,\n onSaved,\n onClose,\n storedCustom,\n custom,\n ])\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={busy ? undefined : onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton color=\"inherit\" edge=\"start\" onClick={onClose} sx={{ mr: 2 }}>\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {deal ? 'Edit deal' : 'New deal'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2} sx={{ width: { xs: '100%', sm: 420 } }}>\n {/*\n First, because the scope of everything below follows from it: at\n the organization level a new deal names the site that captures\n it. Nothing under a site, and nothing on an edit — a deal keeps\n the site it was made on (AGL-2630).\n */}\n {mode === 'create' ? (\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText=\"The site this deal belongs to — it decides which of your sites may see it.\"\n />\n ) : null}\n <TextField\n label=\"Title\"\n value={values.title}\n onChange={(event) => update({ title: event.target.value })}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_TITLE_MAX } }}\n />\n {mode === 'create' ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n select\n label=\"Pipeline\"\n value={pipelines.some((entry) => entry.$id === values.pipelineId) ? values.pipelineId : ''}\n onChange={(event) => {\n const next = pipelines.find(\n (entry) => entry.$id === event.target.value,\n )\n update({\n pipelineId: event.target.value,\n stageId: openStages(next)[0]?.id ?? '',\n })\n }}\n sx={{ flex: 1 }}\n >\n {pipelines.map((entry) => (\n <MenuItem key={entry.$id} value={entry.$id}>\n {entry.name}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n label=\"Stage\"\n value={stages.some((stage) => stage.id === values.stageId) ? values.stageId : ''}\n onChange={(event) => update({ stageId: event.target.value })}\n sx={{ flex: 1 }}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Amount\"\n value={values.amount}\n onChange={(event) => update({ amount: event.target.value })}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n helperText={\n amountDerived\n ? \"The sum of the deal's line items — edit them on the Products card.\"\n : undefined\n }\n slotProps={{\n htmlInput: { inputMode: 'decimal', readOnly: amountDerived },\n }}\n />\n <TextField\n select\n label=\"Currency\"\n value={values.currency}\n onChange={(event) => update({ currency: event.target.value })}\n disabled={amountDerived}\n sx={{ width: 120 }}\n >\n {DEAL_CURRENCIES.map((code) => (\n <MenuItem key={code} value={code}>\n {code.toUpperCase()}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n <TextField\n type=\"date\"\n label=\"Expected close\"\n value={values.expectedClose}\n onChange={(event) => update({ expectedClose: event.target.value })}\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n <TextField\n select\n label=\"Owner\"\n // The stored owner resolved to a member — by uid, or by an\n // address the roster has — so the picker highlights the person\n // and a save writes their uid.\n value={owner ? owner.uid : values.ownerUid ? '__keep' : ''}\n onChange={(event) =>\n update({\n ownerUid: event.target.value === '__keep' ? values.ownerUid : event.target.value,\n })\n }\n fullWidth\n helperText={\n !roster.loading && !roster.members.length\n ? 'The roster could not be read; the owner stays as stored.'\n : undefined\n }\n >\n <MenuItem value=\"\">{'Nobody yet'}</MenuItem>\n {values.ownerUid && !owner ? (\n // The stored owner is not on the roster the route returned —\n // a member who left, or a roster that failed to load. Shown by\n // uid so the field is never blank while the deal has an owner.\n <MenuItem value=\"__keep\">{values.ownerUid}</MenuItem>\n ) : null}\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n <Autocomplete<ContactChoice, false, false, false>\n options={contactChoices}\n value={contactValue}\n inputValue={contactQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setContactQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n contactId: picked?.id ?? '',\n contactName: picked ? picked.name || picked.email : '',\n })\n }\n getOptionLabel={(choice) => choice.name || choice.email}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n // Matching is `contactChoicesFor`'s — the list's grammar over the\n // window — and never MUI's own substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.id}>\n <Stack sx={{ lineHeight: 1.25 }}>\n <Typography variant=\"body2\">{choice.name || choice.email}</Typography>\n {choice.name ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {choice.email}\n </Typography>\n ) : null}\n </Stack>\n </li>\n )}\n noOptionsText=\"No recent contact matches\"\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Contact\"\n placeholder=\"Name or email\"\n helperText=\"Searches your most recent contacts by name or email.\"\n />\n )}\n />\n <Autocomplete<CompanyChoice, false, false, false>\n options={companyChoices}\n value={companyValue}\n inputValue={companyQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setCompanyQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n companyId: picked?.id ?? '',\n companyName: picked?.name ?? '',\n })\n }\n getOptionLabel={(choice) => choice.name}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n filterOptions={(options) => options}\n noOptionsText=\"No company matches\"\n renderInput={(params) => (\n <TextField {...params} label=\"Company\" placeholder=\"Company name\" />\n )}\n />\n <TextField\n label=\"Notes\"\n value={values.notes}\n onChange={(event) => update({ notes: event.target.value })}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_NOTES_MAX } }}\n />\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'Custom fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={busy}\n />\n ))}\n </>\n ) : null}\n {bandFull ? (\n <Alert severity=\"warning\">{CRM_RECORDS_BAND_FULL_MESSAGE}</Alert>\n ) : null}\n {problem && values.title ? <Alert severity=\"warning\">{problem}</Alert> : null}\n <Button\n variant=\"contained\"\n disabled={\n busy ||\n Boolean(problem) ||\n !orgId ||\n bandFull ||\n // A new deal waits for its capturing site.\n (mode === 'create' && !createHostId)\n }\n onClick={() => void handleSave()}\n >\n {deal ? 'Save deal' : 'Create deal'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nDealEditDrawer.displayName = 'DealEditDrawer'\n\nexport default DealEditDrawer\n"],"names":["CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","dealHasLineItems","findOrgMember","nameSearchKey","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","useSnackbar","useFirestore","useFirestoreCollection","useUser","writeGuardedBySeed","Alert","Autocomplete","Button","IconButton","MenuItem","Stack","TextField","Typography","addDoc","collection","deleteField","doc","endAt","getDocs","limit","orderBy","query","startAt","updateDoc","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","useCrmRecordsQuota","crmScopeListable","crmVisibleToClause","useCrmScope","useOrgMemberDirectory","contactPrimaryGroup","crmCustomDraftDocument","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","CrmSitePicker","DEAL_CURRENCIES","openStages","contactChoicesFor","DEAL_NOTES_MAX","DEAL_TITLE_MAX","dealDocumentFromForm","dealFormFromDoc","dealFormProblem","dealPatchFromForm","emptyDealForm","CONTACT_WINDOW","COMPANY_MATCHES","DealEditDrawer","props","contactChoices","open","onClose","hostId","org","deal","defaults","pipelines","defaultPipeline","unreadable","fromCache","onSaved","mode","firestore","enqueueSnackbar","data","user","orgId","consentGroup","visibleTo","createHostId","createTokens","logActivity","roster","values","setValues","owner","members","ownerUid","busy","setBusy","fields","storedCustom","custom","setCustom","update","patch","current","pipeline","find","entry","$id","pipelineId","stages","contactRows","idField","contactQuery","setContactQuery","groupId","row","contactValue","contactId","id","name","contactName","email","choice","companyQuery","setCompanyQuery","companyChoices","setCompanyChoices","key","active","timer","setTimeout","companies","then","snapshot","docs","map","String","get","catch","clearTimeout","companyValue","companyId","companyName","customMissing","problem","length","join","amountDerived","Boolean","recordsScope","records","bandFull","ready","quota","allowed","handleSave","uid","variant","persist","nowMs","Date","now","verdict","subject","set","clear","deals","Object","fromEntries","ok","message","customDocument","created","type","title","error","console","allowDuplicate","anchor","undefined","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","appBarRight","disabled","gutterY","spacing","width","xs","sm","helperText","label","value","onChange","event","target","autoFocus","fullWidth","slotProps","htmlInput","maxLength","direction","select","some","next","stageId","flex","stage","alignItems","amount","placeholder","inputMode","readOnly","currency","code","toUpperCase","expectedClose","inputLabel","shrink","loading","member","options","inputValue","onInputChange","_event","reason","picked","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","lineHeight","noOptionsText","renderInput","params","notes","multiline","minRows","definition","severity","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-edit-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\nimport {\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n crmNewRecordListFields,\n dealHasLineItems,\n findOrgMember,\n nameSearchKey,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteField,\n doc,\n endAt,\n getDocs,\n limit,\n orderBy,\n query,\n startAt,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport {\n type CrmOrgDoc,\n crmScopeListable,\n crmVisibleToClause,\n useCrmScope,\n} from '../hooks/use-crm-scope'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { contactPrimaryGroup } from '../model/contact-record'\nimport {\n type CrmCustomDraft,\n crmCustomDraftDocument,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmSitePicker } from './crm-site-picker'\nimport { CRM_CLIENT_SEARCH_FIELDS, crmClientListFields } from '../model/crm-list-query'\nimport {\n DEAL_CURRENCIES,\n type DealDoc,\n openStages,\n type PipelineDoc,\n} from '../model/deal-board-model'\nimport {\n type ContactChoice,\n contactChoicesFor,\n DEAL_NOTES_MAX,\n DEAL_TITLE_MAX,\n dealDocumentFromForm,\n dealFormFromDoc,\n dealFormProblem,\n type DealFormValues,\n dealPatchFromForm,\n emptyDealForm,\n} from '../model/deal-form-model'\n\n/**\n * How many recent contacts the picker matches against. The window is the\n * ask — it is read when the drawer opens, not when the section does — and\n * a person outside it is still linkable by pasting their address, which\n * the match tests as a prefix of the email.\n */\nconst CONTACT_WINDOW = 300\n\n/** Companies offered per keystroke; a longer list is a scroll, not a pick. */\nconst COMPANY_MATCHES = 8\n\n/** A company as the picker offers it. */\ninterface CompanyChoice {\n id: string\n name: string\n}\n\nexport interface DealEditDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the drawer is opened under, or `null` at the organization\n * level (AGL-2630), where a NEW deal asks which site captures it and an\n * edit keeps the deal's own.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The deal being edited; absent for a new one. */\n deal?: DealDoc | null\n /** Links and values a new deal opens with — a contact's page passes itself. */\n defaults?: Partial<DealFormValues>\n pipelines: PipelineDoc[]\n /** Where a new deal lands when nobody picks a pipeline. */\n defaultPipeline: PipelineDoc | null\n /** The listener's verdict on the row being edited, for the stale-seed guard. */\n unreadable?: boolean\n fromCache?: boolean\n /** After a save, with the id of the deal written. */\n onSaved?: (dealId: string, mode: 'create' | 'edit') => void\n}\n\n/**\n * The one form a deal is created and edited through (AGL-2598).\n *\n * ## What it writes, and what it does not\n *\n * Client-direct against the rules, like a contact's notes: the title, the\n * amount and its currency, the expected close, the owner, the linked\n * contact and company, and the notes. The stage and the status are shown on\n * a new deal — it has to start somewhere — and never on an existing one,\n * because a stage change is the moment an automation listens for and only\n * the server route can emit that event. A drawer that wrote `stageId`\n * directly would move a deal without anybody hearing it.\n *\n * Nor the amount of a deal that has line items (AGL-2620): it is their\n * sum, the products card on the deal's page owns it, and the field here\n * is read-only with a caption saying so. The pipeline picker on a new\n * deal lists every ACTIVE pipeline (`pipelines`) and opens on the one the\n * caller was looking at.\n *\n * ## The pickers\n *\n * The owner is picked from the org's roster, the company by a server-side\n * prefix on `nameLower` (indexed with the scope), and the contact from a\n * bounded window of the newest contacts matched in memory — see\n * `contactChoicesFor` for why the contact search cannot be a query. Both\n * pickers copy the chosen NAME onto the deal beside the id, so a board of\n * cards can caption itself without a read per card.\n */\nexport function DealEditDrawer(props: DealEditDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n deal,\n defaults,\n pipelines,\n defaultPipeline,\n unreadable = false,\n fromCache = false,\n onSaved,\n } = props\n const mode: 'create' | 'edit' = deal ? 'edit' : 'create'\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const { orgId, consentGroup, visibleTo, createHostId, createTokens } =\n useCrmScope({ hostId, org })\n // The feed the act is logged in is the level it was PERFORMED at\n // (AGL-2738): this site's under a site, the organization's at the org\n // hub. `createHostId` is where the deal is STAMPED, which at the hub is a\n // site the reader picked and may hold no role on — a different question\n // from where the line belongs, and a gate the record write never faced.\n const logActivity = useCrmActivityLogger(hostId)\n const roster = useOrgMemberDirectory(open ? orgId : null)\n\n const [values, setValues] = useState<DealFormValues>(() =>\n emptyDealForm(defaultPipeline, defaults),\n )\n const owner = findOrgMember(roster.members, values.ownerUid)\n const [busy, setBusy] = useState(false)\n /*\n * THE CUSTOM FIELDS (AGL-2661): the org's deal definitions, and the keys\n * the reader has touched over what the deal stores — saved as the\n * difference (`crm-custom-draft`) in the same write as the fixed fields.\n */\n const fields = useContactFieldDefinitions(open ? orgId : null, 'deal')\n const storedCustom = useMemo(() => deal?.custom ?? {}, [deal?.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n // Re-seeded on every open: an existing deal's stored values, or a blank\n // form aimed at the default pipeline with whatever the opener preselected.\n useEffect(() => {\n if (!open) return\n setValues(deal ? dealFormFromDoc(deal) : emptyDealForm(defaultPipeline, defaults))\n setCustom({})\n }, [open, deal, defaultPipeline, defaults])\n\n const update = useCallback(\n (patch: Partial<DealFormValues>) =>\n setValues((current) => ({ ...current, ...patch })),\n [],\n )\n\n const pipeline = useMemo(\n () =>\n pipelines.find((entry) => entry.$id === values.pipelineId) ??\n defaultPipeline,\n [pipelines, values.pipelineId, defaultPipeline],\n )\n const stages = useMemo(() => openStages(pipeline), [pipeline])\n\n /*\n * THE CONTACT WINDOW: the newest contacts this viewer may see, read only\n * while the drawer is open. Scoped and ordered like the contacts list —\n * the same `(visibleTo, updatedAt DESC)` index — and bounded, because the\n * picker's job is to find one person, not to list the audience.\n */\n const { data: contactRows } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n open && orgId && crmScopeListable(visibleTo)\n ? query(\n collection(firestore, 'orgs', orgId, 'contacts'),\n ...crmVisibleToClause(visibleTo),\n orderBy('updatedAt', 'desc'),\n limit(CONTACT_WINDOW),\n )\n : null,\n [firestore, open, orgId, visibleTo],\n { idField: '$id' },\n )\n const [contactQuery, setContactQuery] = useState('')\n // Named through the viewing group under a site; at the organization level\n // through each person's own primary holder.\n const contactChoices = useMemo(\n () =>\n contactChoicesFor(\n contactQuery,\n contactRows ?? [],\n consentGroup?.groupId ??\n ((row) => contactPrimaryGroup(row, org as Record<string, unknown>).groupId),\n ),\n [contactQuery, contactRows, consentGroup, org],\n )\n const contactValue: ContactChoice | null = values.contactId\n ? {\n id: values.contactId,\n name: values.contactName,\n email:\n contactChoices.find((choice) => choice.id === values.contactId)?.email ?? '',\n }\n : null\n\n /*\n * THE COMPANY SEARCH: a prefix on `nameLower` with the scope, which the\n * companies collection indexes. Debounced so a name typed at speed is one\n * query rather than one per letter.\n */\n const [companyQuery, setCompanyQuery] = useState('')\n const [companyChoices, setCompanyChoices] = useState<CompanyChoice[]>([])\n useEffect(() => {\n if (!open || !orgId || !crmScopeListable(visibleTo)) return\n const key = nameSearchKey(companyQuery)\n let active = true\n const timer = setTimeout(() => {\n void getDocs(\n query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n ...(key ? [startAt(key), endAt(`${key}\\uf8ff`)] : []),\n limit(COMPANY_MATCHES),\n ),\n )\n .then((snapshot) => {\n if (!active) return\n setCompanyChoices(\n snapshot.docs.map((entry) => ({\n id: entry.id,\n name: String(entry.get('name') ?? ''),\n })),\n )\n })\n .catch(() => {\n if (active) setCompanyChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, orgId, visibleTo, companyQuery])\n const companyValue: CompanyChoice | null = values.companyId\n ? { id: values.companyId, name: values.companyName }\n : null\n\n const customMissing = crmCustomDraftMissingRequired(fields.active, storedCustom, custom, mode)\n const problem =\n dealFormProblem(values, mode) ??\n (customMissing.length ? `${customMissing.join(', ')} is required.` : null)\n const amountDerived = Boolean(deal && dealHasLineItems(deal))\n\n /*\n * THE RECORDS BAND (AGL-2611), read only while a CREATE is open: a deal\n * is a record of the band the contacts list is banded by, and on an org\n * at a full hard band this drawer refuses the way `upsertHostContact`\n * refuses a capture — same number, same sentence. Memoized, because the\n * tuple is an effect dependency and a fresh one per render would re-read\n * the three aggregates on every keystroke.\n */\n const recordsScope = useMemo(\n () => (open && mode === 'create' && orgId ? (['orgs', orgId] as const) : null),\n [open, mode, orgId],\n )\n const records = useCrmRecordsQuota(recordsScope, org)\n const bandFull = mode === 'create' && records.ready && !records.quota.allowed\n\n const handleSave = useCallback(async () => {\n if (problem || !orgId || !user?.uid) return\n if (bandFull) {\n enqueueSnackbar(CRM_RECORDS_BAND_FULL_MESSAGE, { variant: 'warning', persist: false })\n return\n }\n setBusy(true)\n try {\n const nowMs = Date.now()\n if (deal) {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n const { set, clear } = dealPatchFromForm(values, nowMs, { amountDerived })\n await updateDoc(\n doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id),\n {\n ...set,\n ...Object.fromEntries(clear.map((key) => [key, deleteField()])),\n // The custom keys that changed, merged into the stored map.\n ...crmCustomDraftWrites(storedCustom, custom),\n // The title is what the Deals table searches (AGL-3321),\n // from the listener's row with the edit over it.\n ...crmClientListFields(\n 'deals',\n deal as unknown as Record<string, unknown>,\n set,\n CRM_CLIENT_SEARCH_FIELDS,\n ),\n },\n )\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return\n }\n enqueueSnackbar('Deal saved', { variant: 'success', persist: false })\n onSaved?.(deal.$id, 'edit')\n } else {\n // Held by the button until the capturing site is known; checked\n // again here because a callback can outlive the render that held it.\n if (!createHostId) return\n const customDocument = crmCustomDraftDocument(custom)\n const record: Record<string, unknown> = {\n ...dealDocumentFromForm(values, {\n visibleTo: [...createTokens],\n hostId: createHostId,\n uid: user.uid,\n nowMs,\n }),\n ...(customDocument ? { custom: customDocument } : {}),\n }\n const created = await addDoc(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n // What the Deals table searches and filters by (AGL-3321), and\n // nothing scheduled against a deal that did not exist.\n { ...record, ...crmNewRecordListFields('deals', record) },\n )\n // Setup → Activity shows CRM work (AGL-2622): the deal is org data,\n // but the act happened in this site's console and belongs in its\n // feed. Creation only — an edit is a save the card reports.\n logActivity('Added deal', { type: 'deal', id: created.id, name: values.title })\n enqueueSnackbar('Deal created', { variant: 'success', persist: false })\n onSaved?.(created.id, 'create')\n }\n onClose()\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [\n problem,\n bandFull,\n orgId,\n user,\n deal,\n values,\n unreadable,\n fromCache,\n firestore,\n createTokens,\n createHostId,\n logActivity,\n enqueueSnackbar,\n onSaved,\n onClose,\n storedCustom,\n custom,\n ])\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={busy ? undefined : onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton color=\"inherit\" edge=\"start\" onClick={onClose} sx={{ mr: 2 }}>\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {deal ? 'Edit deal' : 'New deal'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2} sx={{ width: { xs: '100%', sm: 420 } }}>\n {/*\n First, because the scope of everything below follows from it: at\n the organization level a new deal names the site that captures\n it. Nothing under a site, and nothing on an edit — a deal keeps\n the site it was made on (AGL-2630).\n */}\n {mode === 'create' ? (\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText=\"The site this deal belongs to — it decides which of your sites may see it.\"\n />\n ) : null}\n <TextField\n label=\"Title\"\n value={values.title}\n onChange={(event) => update({ title: event.target.value })}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_TITLE_MAX } }}\n />\n {mode === 'create' ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n select\n label=\"Pipeline\"\n value={pipelines.some((entry) => entry.$id === values.pipelineId) ? values.pipelineId : ''}\n onChange={(event) => {\n const next = pipelines.find(\n (entry) => entry.$id === event.target.value,\n )\n update({\n pipelineId: event.target.value,\n stageId: openStages(next)[0]?.id ?? '',\n })\n }}\n sx={{ flex: 1 }}\n >\n {pipelines.map((entry) => (\n <MenuItem key={entry.$id} value={entry.$id}>\n {entry.name}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n label=\"Stage\"\n value={stages.some((stage) => stage.id === values.stageId) ? values.stageId : ''}\n onChange={(event) => update({ stageId: event.target.value })}\n sx={{ flex: 1 }}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Amount\"\n value={values.amount}\n onChange={(event) => update({ amount: event.target.value })}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n helperText={\n amountDerived\n ? \"The sum of the deal's line items — edit them on the Products card.\"\n : undefined\n }\n slotProps={{\n htmlInput: { inputMode: 'decimal', readOnly: amountDerived },\n }}\n />\n <TextField\n select\n label=\"Currency\"\n value={values.currency}\n onChange={(event) => update({ currency: event.target.value })}\n disabled={amountDerived}\n sx={{ width: 120 }}\n >\n {DEAL_CURRENCIES.map((code) => (\n <MenuItem key={code} value={code}>\n {code.toUpperCase()}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n <TextField\n type=\"date\"\n label=\"Expected close\"\n value={values.expectedClose}\n onChange={(event) => update({ expectedClose: event.target.value })}\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n <TextField\n select\n label=\"Owner\"\n // The stored owner resolved to a member — by uid, or by an\n // address the roster has — so the picker highlights the person\n // and a save writes their uid.\n value={owner ? owner.uid : values.ownerUid ? '__keep' : ''}\n onChange={(event) =>\n update({\n ownerUid: event.target.value === '__keep' ? values.ownerUid : event.target.value,\n })\n }\n fullWidth\n helperText={\n !roster.loading && !roster.members.length\n ? 'The roster could not be read; the owner stays as stored.'\n : undefined\n }\n >\n <MenuItem value=\"\">{'Nobody yet'}</MenuItem>\n {values.ownerUid && !owner ? (\n // The stored owner is not on the roster the route returned —\n // a member who left, or a roster that failed to load. Shown by\n // uid so the field is never blank while the deal has an owner.\n <MenuItem value=\"__keep\">{values.ownerUid}</MenuItem>\n ) : null}\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n <Autocomplete<ContactChoice, false, false, false>\n options={contactChoices}\n value={contactValue}\n inputValue={contactQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setContactQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n contactId: picked?.id ?? '',\n contactName: picked ? picked.name || picked.email : '',\n })\n }\n getOptionLabel={(choice) => choice.name || choice.email}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n // Matching is `contactChoicesFor`'s — the list's grammar over the\n // window — and never MUI's own substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.id}>\n <Stack sx={{ lineHeight: 1.25 }}>\n <Typography variant=\"body2\">{choice.name || choice.email}</Typography>\n {choice.name ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {choice.email}\n </Typography>\n ) : null}\n </Stack>\n </li>\n )}\n noOptionsText=\"No recent contact matches\"\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Contact\"\n placeholder=\"Name or email\"\n helperText=\"Searches your most recent contacts by name or email.\"\n />\n )}\n />\n <Autocomplete<CompanyChoice, false, false, false>\n options={companyChoices}\n value={companyValue}\n inputValue={companyQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setCompanyQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n companyId: picked?.id ?? '',\n companyName: picked?.name ?? '',\n })\n }\n getOptionLabel={(choice) => choice.name}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n filterOptions={(options) => options}\n noOptionsText=\"No company matches\"\n renderInput={(params) => (\n <TextField {...params} label=\"Company\" placeholder=\"Company name\" />\n )}\n />\n <TextField\n label=\"Notes\"\n value={values.notes}\n onChange={(event) => update({ notes: event.target.value })}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_NOTES_MAX } }}\n />\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'Custom fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={busy}\n />\n ))}\n </>\n ) : null}\n {bandFull ? (\n <Alert severity=\"warning\">{CRM_RECORDS_BAND_FULL_MESSAGE}</Alert>\n ) : null}\n {problem && values.title ? <Alert severity=\"warning\">{problem}</Alert> : null}\n <Button\n variant=\"contained\"\n disabled={\n busy ||\n Boolean(problem) ||\n !orgId ||\n bandFull ||\n // A new deal waits for its capturing site.\n (mode === 'create' && !createHostId)\n }\n onClick={() => void handleSave()}\n >\n {deal ? 'Save deal' : 'Create deal'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nDealEditDrawer.displayName = 'DealEditDrawer'\n\nexport default DealEditDrawer\n"],"names":["CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","crmNewRecordListFields","dealHasLineItems","findOrgMember","nameSearchKey","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","useSnackbar","useFirestore","useFirestoreCollection","useUser","writeGuardedBySeed","Alert","Autocomplete","Button","IconButton","MenuItem","Stack","TextField","Typography","addDoc","collection","deleteField","doc","endAt","getDocs","limit","orderBy","query","startAt","updateDoc","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","useCrmRecordsQuota","crmScopeListable","crmVisibleToClause","useCrmScope","useOrgMemberDirectory","contactPrimaryGroup","crmCustomDraftDocument","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","CrmSitePicker","CRM_CLIENT_SEARCH_FIELDS","crmClientListFields","DEAL_CURRENCIES","openStages","contactChoicesFor","DEAL_NOTES_MAX","DEAL_TITLE_MAX","dealDocumentFromForm","dealFormFromDoc","dealFormProblem","dealPatchFromForm","emptyDealForm","CONTACT_WINDOW","COMPANY_MATCHES","DealEditDrawer","props","contactChoices","open","onClose","hostId","org","deal","defaults","pipelines","defaultPipeline","unreadable","fromCache","onSaved","mode","firestore","enqueueSnackbar","data","user","orgId","consentGroup","visibleTo","createHostId","createTokens","logActivity","roster","values","setValues","owner","members","ownerUid","busy","setBusy","fields","storedCustom","custom","setCustom","update","patch","current","pipeline","find","entry","$id","pipelineId","stages","contactRows","idField","contactQuery","setContactQuery","groupId","row","contactValue","contactId","id","name","contactName","email","choice","companyQuery","setCompanyQuery","companyChoices","setCompanyChoices","key","active","timer","setTimeout","companies","then","snapshot","docs","map","String","get","catch","clearTimeout","companyValue","companyId","companyName","customMissing","problem","length","join","amountDerived","Boolean","recordsScope","records","bandFull","ready","quota","allowed","handleSave","uid","variant","persist","nowMs","Date","now","verdict","subject","set","clear","deals","Object","fromEntries","ok","message","customDocument","record","created","type","title","error","console","allowDuplicate","anchor","undefined","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","appBarRight","disabled","gutterY","spacing","width","xs","sm","helperText","label","value","onChange","event","target","autoFocus","fullWidth","slotProps","htmlInput","maxLength","direction","select","some","next","stageId","flex","stage","alignItems","amount","placeholder","inputMode","readOnly","currency","code","toUpperCase","expectedClose","inputLabel","shrink","loading","member","options","inputValue","onInputChange","_event","reason","picked","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","lineHeight","noOptionsText","renderInput","params","notes","multiline","minRows","definition","severity","displayName"],"mappings":"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"}
|
|
@@ -8,8 +8,8 @@ import { type ConsolePluginPageProps } from '@aglyn/aglyn';
|
|
|
8
8
|
* in one bounded listener over `(visibleTo, pipelineId, status, updatedAt)`
|
|
9
9
|
* and sorted into columns here — a listener per column would be five
|
|
10
10
|
* subscriptions for one board. The TABLE is the same pipeline's deals,
|
|
11
|
-
* paged by
|
|
12
|
-
* is found. The three figures above both come from the board's read, so
|
|
11
|
+
* paged by a query that carries its status, "No next activity" and search
|
|
12
|
+
* word (AGL-3321); it is where the closed history is found. The three figures above both come from the board's read, so
|
|
13
13
|
* they describe what is in play whichever view is showing.
|
|
14
14
|
*
|
|
15
15
|
* ## Which pipeline
|
|
@@ -15,19 +15,21 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
-
import {
|
|
18
|
+
import { CRM_COLLECTIONS, dealStageById, findOrgMember, ORG_SCOPE_TOKEN, pluginDocsHelp } from "@aglyn/aglyn";
|
|
19
19
|
import { mdiCogOutline, mdiPlus, mdiTableLarge, mdiViewColumnOutline } from "@aglyn/shared-data-mdi";
|
|
20
20
|
import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
21
21
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
22
22
|
import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
23
|
+
import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
23
24
|
import { useCrmSavedView } from "../hooks/use-crm-saved-view.js";
|
|
24
25
|
import { useCrmViewGrid } from "../hooks/use-crm-view-grid.js";
|
|
25
26
|
import { CrmListActions, CrmListToolbar } from "./crm-list-toolbar.js";
|
|
26
27
|
import { CRM_LIST_SLOTS, CrmColumnOrderProvider } from "./crm-column-menu.js";
|
|
27
28
|
import { CRM_NEXT_ACTIVITY_FILTER_FIELD, CRM_NEXT_ACTIVITY_FILTER_HEADER, nextActivityColumn } from "./crm-next-activity-column.js";
|
|
29
|
+
import { DEAL_LIST_DECLARATION, DEAL_LIST_FILTER_FIELDS, DEAL_PREFIX_SEARCH, dealPipelineBase } from "../constants/deal-filters.js";
|
|
30
|
+
import { useCrmFoldsScope, useCrmListQuery } from "../hooks/use-crm-list-query.js";
|
|
28
31
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
29
|
-
import { listFilterGridColumns
|
|
30
|
-
import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
|
|
32
|
+
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
31
33
|
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
32
34
|
import CrmViewsControl from "./crm-views-control.js";
|
|
33
35
|
import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.component";
|
|
@@ -38,7 +40,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
38
40
|
import { useContactFieldDefinitions } from "../hooks/use-contact-field-definitions.js";
|
|
39
41
|
import { useCrmScope } from "../hooks/use-crm-scope.js";
|
|
40
42
|
import { useDealStageApi } from "../hooks/use-deal-stage-api.js";
|
|
41
|
-
import { BOARD_CLOSED_LIMIT, BOARD_OPEN_LIMIT,
|
|
43
|
+
import { BOARD_CLOSED_LIMIT, BOARD_OPEN_LIMIT, useDealsByStatus } from "../hooks/use-deals.js";
|
|
42
44
|
import { useOrgMemberDirectory } from "../hooks/use-org-member-directory.js";
|
|
43
45
|
import { usePipeline } from "../hooks/use-pipeline.js";
|
|
44
46
|
import { downloadTextFile } from "../model/contacts-csv.js";
|
|
@@ -53,24 +55,11 @@ import DealsBulkBar from "./deals-bulk-bar.js";
|
|
|
53
55
|
import { LostReasonDialog } from "./lost-reason-dialog.js";
|
|
54
56
|
import { OwnerAvatar } from "./owner-avatar.js";
|
|
55
57
|
import { PipelinesDialog } from "./pipelines-dialog.js";
|
|
56
|
-
/** What the deals table's panel offers: the
|
|
57
|
-
{
|
|
58
|
-
column: 'status',
|
|
59
|
-
kind: 'exact',
|
|
60
|
-
path: 'status',
|
|
61
|
-
operators: [
|
|
62
|
-
'equals'
|
|
63
|
-
]
|
|
64
|
-
},
|
|
65
|
-
CRM_NEXT_ACTIVITY_FILTER_FIELD
|
|
66
|
-
];
|
|
58
|
+
/** What the deals table's panel offers: the status, and "No next activity". */ const DEAL_GRID_FILTER_FIELDS = DEAL_LIST_FILTER_FIELDS;
|
|
67
59
|
const DEAL_GRID_FILTER_HEADERS = {
|
|
68
60
|
status: 'Status',
|
|
69
61
|
[CRM_NEXT_ACTIVITY_FILTER_FIELD.column]: CRM_NEXT_ACTIVITY_FILTER_HEADER
|
|
70
62
|
};
|
|
71
|
-
/** What the table's search reads on a deal. */ const DEAL_SEARCH_FIELDS = [
|
|
72
|
-
'title'
|
|
73
|
-
];
|
|
74
63
|
const DEAL_FILTER_OPTIONS = {
|
|
75
64
|
status: [
|
|
76
65
|
'open',
|
|
@@ -93,8 +82,8 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
93
82
|
* in one bounded listener over `(visibleTo, pipelineId, status, updatedAt)`
|
|
94
83
|
* and sorted into columns here — a listener per column would be five
|
|
95
84
|
* subscriptions for one board. The TABLE is the same pipeline's deals,
|
|
96
|
-
* paged by
|
|
97
|
-
* is found. The three figures above both come from the board's read, so
|
|
85
|
+
* paged by a query that carries its status, "No next activity" and search
|
|
86
|
+
* word (AGL-3321); it is where the closed history is found. The three figures above both come from the board's read, so
|
|
98
87
|
* they describe what is in play whichever view is showing.
|
|
99
88
|
*
|
|
100
89
|
* ## Which pipeline
|
|
@@ -127,7 +116,7 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
127
116
|
* `dealsCsv()`, naming the pipeline, the stage and the owner; the bar
|
|
128
117
|
* writes the same file over the selection.
|
|
129
118
|
*/ export function DealsSection(props) {
|
|
130
|
-
var _ref, _ref1, _ref2, _ref3,
|
|
119
|
+
var _ref, _ref1, _ref2, _ref3, _scope_orgId, _ref4;
|
|
131
120
|
const { hostId, org, basePath } = props;
|
|
132
121
|
const routes = crmRoutes(basePath != null ? basePath : '');
|
|
133
122
|
const router = useRouter();
|
|
@@ -171,8 +160,7 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
171
160
|
}, [
|
|
172
161
|
views.state.filters
|
|
173
162
|
]);
|
|
174
|
-
//
|
|
175
|
-
// it (AGL-2661) narrows the loaded page and survives a status change.
|
|
163
|
+
// Every clause is the query's, the status and "No next activity" alike.
|
|
176
164
|
const viewFilters = views.state.filters;
|
|
177
165
|
useEffect(()=>{
|
|
178
166
|
if (views.currentId) setView('table');
|
|
@@ -185,29 +173,33 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
185
173
|
const closedPipelineId = view === 'board' && closedExpanded ? (_ref2 = pipeline == null ? void 0 : pipeline.$id) != null ? _ref2 : null : null;
|
|
186
174
|
const won = useDealsByStatus(scope.orgId, scope.visibleTo, closedPipelineId, 'won', BOARD_CLOSED_LIMIT);
|
|
187
175
|
const lost = useDealsByStatus(scope.orgId, scope.visibleTo, closedPipelineId, 'lost', BOARD_CLOSED_LIMIT);
|
|
188
|
-
const paged = usePagedDeals(view === 'table' ? scope.orgId : null, scope.visibleTo, statusFilter, (_ref3 = pipeline == null ? void 0 : pipeline.$id) != null ? _ref3 : null);
|
|
189
176
|
/*
|
|
190
|
-
* THE
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
177
|
+
* THE TABLE (AGL-3321): the pipeline's deals, every clause and the search
|
|
178
|
+
* word on one query — the search reads the title's word prefixes every
|
|
179
|
+
* deal writer stamps, so a deal on page four is found by any word of its
|
|
180
|
+
* title — paged by that query, and read only while the table is showing.
|
|
194
181
|
*/ const [searchWords, setSearchWords] = useState([]);
|
|
195
182
|
const searching = searchWords.some((word)=>word.trim());
|
|
196
|
-
const searchWindow = useDealSearchWindow(view === 'table' && searching ? scope.orgId : null, scope.visibleTo, statusFilter, (_ref4 = pipeline == null ? void 0 : pipeline.$id) != null ? _ref4 : null);
|
|
197
183
|
const searchKey = searchWords.join(' ');
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const searchTruncated = searchWindow.data.length > DEAL_SEARCH_WINDOW;
|
|
205
|
-
const [searchPage, setSearchPage] = useState(0);
|
|
206
|
-
const [searchPageSize, setSearchPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
|
|
207
|
-
useEffect(()=>setSearchPage(0), [
|
|
208
|
-
searchKey,
|
|
209
|
-
statusFilter
|
|
184
|
+
const foldsScope = useCrmFoldsScope(scope.orgId, scope.visibleTo);
|
|
185
|
+
const pipelineBase = useMemo(()=>{
|
|
186
|
+
var _ref;
|
|
187
|
+
return dealPipelineBase((_ref = pipeline == null ? void 0 : pipeline.$id) != null ? _ref : null);
|
|
188
|
+
}, [
|
|
189
|
+
pipeline == null ? void 0 : pipeline.$id
|
|
210
190
|
]);
|
|
191
|
+
const paged = useCrmListQuery({
|
|
192
|
+
scope: scope.scope,
|
|
193
|
+
collection: CRM_COLLECTIONS.deals,
|
|
194
|
+
visibleTo: scope.visibleTo,
|
|
195
|
+
foldsScope,
|
|
196
|
+
declaration: DEAL_LIST_DECLARATION,
|
|
197
|
+
clauses: viewFilters,
|
|
198
|
+
search: searchWords,
|
|
199
|
+
base: pipelineBase,
|
|
200
|
+
prefixSearch: DEAL_PREFIX_SEARCH,
|
|
201
|
+
enabled: view === 'table' && Boolean(pipeline)
|
|
202
|
+
});
|
|
211
203
|
const summary = useMemo(()=>boardSummary(open.data, pipeline), [
|
|
212
204
|
open.data,
|
|
213
205
|
pipeline
|
|
@@ -276,7 +268,7 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
276
268
|
// A page or a status is a different set of rows; a selection made on
|
|
277
269
|
// the last one would be a count over rows no longer on screen.
|
|
278
270
|
useEffect(()=>setSelectedIds([]), [
|
|
279
|
-
|
|
271
|
+
paged.plan,
|
|
280
272
|
paged.page
|
|
281
273
|
]);
|
|
282
274
|
const csvOptions = useMemo(()=>({
|
|
@@ -296,12 +288,10 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
296
288
|
pipelineState,
|
|
297
289
|
roster.members
|
|
298
290
|
]);
|
|
299
|
-
// The page on screen
|
|
291
|
+
// The page on screen; Export all on the bulk bar takes the whole list.
|
|
300
292
|
const handleExport = useCallback(()=>{
|
|
301
|
-
downloadTextFile('deals.csv', 'text/csv', dealsCsv(
|
|
293
|
+
downloadTextFile('deals.csv', 'text/csv', dealsCsv(paged.rows, csvOptions));
|
|
302
294
|
}, [
|
|
303
|
-
searching,
|
|
304
|
-
found,
|
|
305
295
|
paged.rows,
|
|
306
296
|
csvOptions
|
|
307
297
|
]);
|
|
@@ -432,23 +422,11 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
432
422
|
dealFields.active,
|
|
433
423
|
nowMs
|
|
434
424
|
]);
|
|
435
|
-
/** The table's rows: the query's page
|
|
436
|
-
found,
|
|
437
|
-
viewFilters
|
|
438
|
-
]);
|
|
439
|
-
const tableRows = useMemo(()=>searching ? searchRows.slice(searchPage * searchPageSize, (searchPage + 1) * searchPageSize) : filterByNextActivity(paged.rows, viewFilters), [
|
|
440
|
-
searching,
|
|
441
|
-
searchRows,
|
|
442
|
-
searchPage,
|
|
443
|
-
searchPageSize,
|
|
444
|
-
paged.rows,
|
|
445
|
-
viewFilters
|
|
446
|
-
]);
|
|
425
|
+
/** The table's rows: the query's page. */ const tableRows = paged.rows;
|
|
447
426
|
/*
|
|
448
427
|
* The table's column and sort models are the view's (AGL-2617); its
|
|
449
|
-
* filters are the grid's own panel over the view's clauses (AGL-3313)
|
|
450
|
-
*
|
|
451
|
-
* "No next activity" stands beside it over the loaded page.
|
|
428
|
+
* filters are the grid's own panel over the view's clauses (AGL-3313),
|
|
429
|
+
* every one of them asked of the query (AGL-3321).
|
|
452
430
|
*/ const filterColumns = useMemo(()=>listFilterGridColumns(columns, DEAL_GRID_FILTER_FIELDS, DEAL_FILTER_OPTIONS, DEAL_GRID_FILTER_HEADERS), [
|
|
453
431
|
columns
|
|
454
432
|
]);
|
|
@@ -459,13 +437,19 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
459
437
|
selectFields: [
|
|
460
438
|
'status'
|
|
461
439
|
],
|
|
462
|
-
single: true,
|
|
463
|
-
keepAlongside: isNoNextActivityClause,
|
|
464
440
|
search: {
|
|
465
441
|
words: searchWords,
|
|
466
442
|
onChange: setSearchWords
|
|
467
443
|
}
|
|
468
444
|
});
|
|
445
|
+
// What the query could not hold, named by the clause the reader set.
|
|
446
|
+
const refused = useMemo(()=>listQueryRefusals(paged.plan.refused, {
|
|
447
|
+
fields: DEAL_GRID_FILTER_FIELDS,
|
|
448
|
+
headers: DEAL_GRID_FILTER_HEADERS,
|
|
449
|
+
options: DEAL_FILTER_OPTIONS
|
|
450
|
+
}), [
|
|
451
|
+
paged.plan.refused
|
|
452
|
+
]);
|
|
469
453
|
const noOrg = scope.ready && !scope.orgId;
|
|
470
454
|
const pipelineLoading = !noOrg && (pipelineState.status === 'loading' || pipelineState.seeding || !pipeline && pipelineState.status !== 'error');
|
|
471
455
|
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
@@ -538,7 +522,7 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
538
522
|
select: true,
|
|
539
523
|
size: "small",
|
|
540
524
|
label: "Pipeline",
|
|
541
|
-
value: (
|
|
525
|
+
value: (_ref3 = pipeline == null ? void 0 : pipeline.$id) != null ? _ref3 : '',
|
|
542
526
|
onChange: (event)=>setPipelineId(event.target.value),
|
|
543
527
|
sx: {
|
|
544
528
|
minWidth: 160
|
|
@@ -673,7 +657,7 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
673
657
|
clauses: viewFilters,
|
|
674
658
|
onChange: views.setFilters,
|
|
675
659
|
options: DEAL_FILTER_OPTIONS,
|
|
676
|
-
|
|
660
|
+
marksServed: false
|
|
677
661
|
}),
|
|
678
662
|
/*#__PURE__*/ _jsx(Stack, {
|
|
679
663
|
sx: {
|
|
@@ -686,11 +670,15 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
686
670
|
/*#__PURE__*/ _jsx(Button, {
|
|
687
671
|
size: "small",
|
|
688
672
|
onClick: handleExport,
|
|
689
|
-
disabled: !
|
|
673
|
+
disabled: !paged.rows.length,
|
|
690
674
|
children: 'Export CSV'
|
|
691
675
|
})
|
|
692
676
|
]
|
|
693
677
|
}),
|
|
678
|
+
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
679
|
+
refused: refused,
|
|
680
|
+
notices: paged.plan.notices
|
|
681
|
+
}),
|
|
694
682
|
/*#__PURE__*/ _jsxs(_Fragment, {
|
|
695
683
|
children: [
|
|
696
684
|
/*#__PURE__*/ _jsx(DealsBulkBar, {
|
|
@@ -715,14 +703,12 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
715
703
|
onChange: setSelectedIds
|
|
716
704
|
},
|
|
717
705
|
onOpen: (_id, row)=>openDeal(row),
|
|
718
|
-
// The panel
|
|
719
|
-
// narrows the loaded rows (AGL-3313); the search runs
|
|
720
|
-
// over every deal in the window (AGL-3315).
|
|
706
|
+
// The panel and the search are the query's (AGL-3321).
|
|
721
707
|
filterMode: "server",
|
|
722
708
|
filterModel: gridFilter.filterModel,
|
|
723
709
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
724
710
|
quickFilter: true,
|
|
725
|
-
loading:
|
|
711
|
+
loading: paged.status === 'loading',
|
|
726
712
|
// An empty table is said inside the grid, so its toolbar
|
|
727
713
|
// stays to change the status that emptied it.
|
|
728
714
|
noRowsLabel: searching ? `No deals match “${searchKey.trim()}”` : statusFilter === 'all' ? 'No deals yet' : `No ${statusFilter} deals`,
|
|
@@ -746,25 +732,14 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
746
732
|
hideFooter: true
|
|
747
733
|
})
|
|
748
734
|
}),
|
|
749
|
-
|
|
750
|
-
page: searchPage,
|
|
751
|
-
pageSize: searchPageSize,
|
|
752
|
-
rowCount: tableRows.length,
|
|
753
|
-
count: searchRows.length,
|
|
754
|
-
onPageChange: setSearchPage,
|
|
755
|
-
onPageSizeChange: setSearchPageSize
|
|
756
|
-
}) : /*#__PURE__*/ _jsx(ListPagination, {
|
|
735
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
757
736
|
page: paged.page,
|
|
758
737
|
pageSize: paged.pageSize,
|
|
759
738
|
rowCount: paged.rows.length,
|
|
760
739
|
hasMore: paged.hasMore,
|
|
761
740
|
onPageChange: paged.setPage,
|
|
762
741
|
onPageSizeChange: paged.setPageSize
|
|
763
|
-
})
|
|
764
|
-
searching && searchTruncated ? /*#__PURE__*/ _jsx(Alert, {
|
|
765
|
-
severity: "info",
|
|
766
|
-
children: `The search reads the ${DEAL_SEARCH_WINDOW.toLocaleString()} most recently ` + 'changed deals; narrow by status to reach older ones.'
|
|
767
|
-
}) : null
|
|
742
|
+
})
|
|
768
743
|
]
|
|
769
744
|
})
|
|
770
745
|
]
|
|
@@ -797,7 +772,7 @@ const DEAL_FILTER_OPTIONS = {
|
|
|
797
772
|
}),
|
|
798
773
|
/*#__PURE__*/ _jsx(LostReasonDialog, {
|
|
799
774
|
open: Boolean(losing),
|
|
800
|
-
dealTitle: (
|
|
775
|
+
dealTitle: (_ref4 = losing == null ? void 0 : losing.title) != null ? _ref4 : '',
|
|
801
776
|
onClose: ()=>setLosing(null),
|
|
802
777
|
onConfirm: confirmLost
|
|
803
778
|
})
|