@aglyn/plugins-crm 1.0.0-beta.184 → 1.0.0-beta.186

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-crm",
3
- "version": "1.0.0-beta.184",
3
+ "version": "1.0.0-beta.186",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,18 +25,18 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.184",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.184",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.184",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.184",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.184",
33
- "@aglyn/shared-ui-next": "1.0.0-beta.184",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.184",
35
- "@aglyn/shared-util-email": "1.0.0-beta.184",
36
- "@aglyn/shared-util-http": "1.0.0-beta.184",
37
- "@aglyn/tenant-data-admin": "1.0.0-beta.184",
38
- "@aglyn/tenant-feature-instance": "1.0.0-beta.184",
39
- "@aglyn/tenant-runtime": "1.0.0-beta.184",
28
+ "@aglyn/aglyn": "1.0.0-beta.186",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.186",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.186",
31
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.186",
32
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.186",
33
+ "@aglyn/shared-ui-next": "1.0.0-beta.186",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.186",
35
+ "@aglyn/shared-util-email": "1.0.0-beta.186",
36
+ "@aglyn/shared-util-http": "1.0.0-beta.186",
37
+ "@aglyn/tenant-data-admin": "1.0.0-beta.186",
38
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.186",
39
+ "@aglyn/tenant-runtime": "1.0.0-beta.186",
40
40
  "@dnd-kit/core": "^6.3.1",
41
41
  "@dnd-kit/utilities": "^3.2.2",
42
42
  "@swc/helpers": "0.5.23",
@@ -886,11 +886,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
886
886
  byForm && contactsStatus === 'error' ? /*#__PURE__*/ _jsx(Alert, {
887
887
  severity: "info",
888
888
  children: 'The contacts this form captured could not be listed. Your ' + 'access is limited to specific sites, and a form filter ' + 'cannot be narrowed to them — an organization administrator ' + 'can see it.'
889
- }) : contacts.length === 0 && filter ? /*#__PURE__*/ _jsx(Typography, {
890
- variant: "body2",
891
- color: "text.secondary",
892
- children: contactsStatus === 'loading' ? 'Loading…' : 'No contacts match this filter.'
893
- }) : contacts.length === 0 ? /*#__PURE__*/ _jsx(EmptyStateComponent, {
889
+ }) : contacts.length === 0 && !views.state.filters.length ? /*#__PURE__*/ _jsx(EmptyStateComponent, {
894
890
  label: contactsStatus === 'loading' ? 'Loading contacts…' : 'No contacts yet',
895
891
  description: contactsStatus === 'loading' ? undefined : suiteIncluded ? 'Form submissions, member sign-ups, orders and bookings become contacts on their own; add one by hand or bring a list in from a CSV.' : 'Form submissions, member sign-ups, orders and bookings become contacts on their own.',
896
892
  action: contactsStatus === 'loading' ? undefined : !suiteIncluded ? /*#__PURE__*/ _jsxs(Stack, {
@@ -969,6 +965,9 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
969
965
  onFilterModelChange: gridFilter.onFilterModelChange,
970
966
  quickFilter: true,
971
967
  onPreferencePanelOpen: ()=>setFilterOpened(true),
968
+ // A filter that matched nothing is said inside the grid,
969
+ // so the toolbar that set it stays to change it.
970
+ noRowsLabel: contactsStatus === 'loading' ? 'Loading…' : 'No contacts match these filters',
972
971
  columnVisibilityModel: grid.columnVisibilityModel,
973
972
  onColumnVisibilityModelChange: grid.onColumnVisibilityModelChange,
974
973
  sortModel: grid.sortModel,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contacts-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n CONTACT_SOURCE_LABELS,\n CRM_CONTACT_VIEW_FIELDS,\n type CrmViewFilterClause,\n crmContactCustomColumn,\n crmViewFiltersFromSegment,\n crmViewSegmentFilters,\n newResourceScopeFields,\n ORG_SCOPE_TOKEN,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { type ConsolePluginPageProps } from '@aglyn/aglyn'\nimport {\n hiddenFilterVisibility,\n matchListFilter,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n CONTACT_LIST_FILTER_FIELDS,\n CONTACT_LIST_FILTER_HEADERS,\n contactCustomFilterFields,\n contactCustomFilterHeaders,\n} from '../constants/contact-filters'\nimport {\n type ContactRecord,\n contactPrimaryGroup,\n contactRecordFromDoc,\n} from '../model/contact-record'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { crmVisibleToClause, useCrmScope } from '../hooks/use-crm-scope'\nimport { contactsListSeed } from '../model/contacts-list-seed'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n type ContactCsvOptions,\n contactsCsv,\n downloadTextFile,\n} from '../model/contacts-csv'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport ContactsBulkBar from './contacts-bulk-bar'\nimport { ContactImportButton } from './contact-import-drawer'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n listFilterPlan,\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteDoc,\n doc,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport RecentActivityFeed from './recent-activity-feed'\nimport {\n CONTACT_FILTER_COLUMNS,\n CONTACT_OPTIONAL_COLUMNS,\n contactListColumns,\n} from './contact-list-columns'\nimport NewContactDrawer, { type NewContactValues } from './new-contact-drawer'\nimport { useCrmApi } from './use-crm-api'\nimport { useOrgMembers } from './use-org-members'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { customFieldColumns } from './contact-custom-columns'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport { useCompanyOptions } from './company-picker'\nimport CrmFilterBar from './crm-filter-bar'\nimport {\n type CrmFilterOption,\n crmFilterColumns,\n crmRowMatchesSearch,\n} from '../model/crm-grid-filter'\nimport { useCrmGridFilter } from '../hooks/use-crm-grid-filter'\nimport CrmViewsControl, { type CrmViewPreset } from './crm-views-control'\nimport { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from './crm-suite-lock'\n\n/**\n * The shared labels, under the name this file has always called them.\n *\n * The map lives beside the `ContactSource` union so the dynamic-list rule\n * editor and this filter cannot disagree about what `order` is called.\n */\nconst SOURCE_LABELS = CONTACT_SOURCE_LABELS\n\n/** What the grid's quick search reads on a contact: who they are, and their tags. */\nconst CONTACT_SEARCH_FIELDS = ['name', 'email', 'phone', 'company', 'tags'] as const\n\n/**\n * What the list keeps out of sight until a view says otherwise: the\n * filter-only columns, which never show, and the optional columns\n * (AGL-2616), which start hidden and come back through the column menu or\n * a saved view's column list.\n */\nconst HIDDEN_COLUMNS: Readonly<Record<string, boolean>> = {\n ...hiddenFilterVisibility(CONTACT_LIST_FILTER_FIELDS, CONTACT_FILTER_COLUMNS),\n ...Object.fromEntries(CONTACT_OPTIONAL_COLUMNS.map((field) => [field, false])),\n}\n\n/**\n * Why a refund found no contact to record itself against (AGL-2329).\n *\n * The three reasons `recordContactRefund` distinguishes, in the operator's\n * language rather than the writer's enum. They are not interchangeable: two\n * of them are things the merchant can act on and one is a deletion nothing\n * should undo, and collapsing them into \"unmatched\" would turn an actionable\n * fact into a statistic.\n */\nconst UNMATCHED_REFUND_REASON: Record<string, string> = {\n 'no-email': 'the order carried no email address',\n 'no-contact': 'no contact record matched the buyer',\n 'contact-deleted': 'the contact was deleted between the sale and the refund',\n}\n\n/**\n * Contacts CRM (AGL-198): the unified people list fed by AGL-197's\n * ingestion — search, source badges, a profile drawer with the\n * interaction timeline plus tags/notes editing, and CSV export. Part of\n * the CRM, included from Starter (AGL-2851); the contactsPerHost band\n * counts the records in it.\n */\n/**\n * Contacts CRM (AGL-109 → AGL-395): the unified contacts list, segments,\n * and profile drawer, owned by the contacts plugin and rendered by the\n * shell's generic plugin route. The shell applies the `release_crm`\n * gate (via the nav tab) and passes the resolved `org` doc for the\n * `contactsPerHost` quota check.\n *\n * Since AGL-2595 this is the `people` SECTION of the Contacts hub rather\n * than the whole page: `crm-console-page.tsx` routes to it, and the\n * companies, deals, tasks, reports and fields sections sit beside it. The\n * body is the v1 page unchanged, and it takes the shell's full prop bag so\n * nothing the v1 page read has to be threaded through the hub by name.\n */\nexport function ContactsPeopleSection(props: ConsolePluginPageProps) {\n const { hostId, org, releaseFlag, basePath } = props\n /*\n * A row is a LINK now (AGL-2596): opening a person navigates to their own\n * page rather than a drawer over the list, so the address can be pasted\n * and every other CRM record can point at it. `basePath` is what the hub\n * hands every section; the empty fallback only exists for a direct mount\n * without the shell, where there is nowhere to navigate to anyway.\n */\n const routes = crmRoutes(basePath ?? '')\n const router = useRouter()\n // Whether the audience overage on this page is actually INVOICED\n // (AGL-1662), and whether that question has been answered yet.\n //\n // AGL-1604 stopped the usage cron putting `contactsOverageUsd` into\n // `billedCents` while `release_crm` is off for the org; `db5ecdf2b`\n // taught the console billing page's caption the same thing. This page's own\n // alert still quoted the dollar figure with no flag check — and this is the\n // surface a staff member reaches with the flag OFF, because the shell's\n // `FeatureGate` admits them on `visible` (`released || isStaff`). Support\n // then reads that number back to a customer whose invoice will not carry it.\n //\n // The shell resolves this from `released`, never `visible`: staff opening a\n // page does not put a line on the customer's bill.\n //\n // Both default to the WITHHELD answer when the prop is absent, which only\n // happens in a direct mount — the shell always supplies it for a surface\n // with a nav-tab flag. A caller that has not resolved the verdict has not\n // earned the right to quote a charge.\n const contactsBilled = releaseFlag?.released ?? false\n const releaseFlagsReady = releaseFlag?.ready ?? false\n // Org-shared data root (AGL-237). Null until the org lookup settles\n // (AGL-1061), and for a host with no owning org — the pre-migration host\n // path this used to fall back to is gone (AGL-1050), so the CRM lists\n // nothing rather than listing somewhere else. Resolved by the one scope\n // hook (AGL-2614), which also answers for the organization-level mount\n // (AGL-2630): there `hostId` is null, the org comes from the hub's mount,\n // and the viewing group and the scope clause are both `null`.\n const crmScope = useCrmScope({ hostId, org })\n const { scope: dataScope, orgId, consentGroup, visibleTo: visibleToTokens } = crmScope\n /*\n * THE CRM (AGL-2788). This list is the CRM's, and the shell mounts no CRM\n * page for a plan without it (AGL-2851). The locks below — adding a person\n * by hand, importing a file, a saved view, the owner, stage and company of\n * a selection — draw only for an org whose plan lacks the CRM. The shell\n * mounts this page only once the org has settled, so the plan read here\n * is an answer.\n */\n const suiteIncluded = crmSuiteIncluded(org)\n // The org's site list, at the organization level only — what names the\n // sites in the \"Known by\" column (AGL-2630).\n const mount = useCrmOrgMount()\n // The org's custom fields, for the optional columns below (AGL-2601).\n const customFields = useContactFieldDefinitions(dataScope?.[1] ?? null)\n /*==========================================\n * THE CONTROLLER THIS PAGE IS SHOWING.\n *\n * A contact document is shared by every site in the org — one human who\n * touched two sites is one row, which is the dedupe the shared address book\n * exists for and the reason the org is billed once for them. Almost nothing\n * ON that row is shared: the notes, tags, timeline and lifetime value are\n * the holder's own business records, and while they lived at the top of the\n * document every site in an agency's account could read every client's.\n *\n * So the page resolves the group it is being viewed as — the sites declared\n * to be one sender, or this site alone — and reads that group's facet.\n * Pure, from the org document the shell already passed, so it costs no\n * read. `consentGroup` above is that group; at the ORGANIZATION level it\n * is `null`, and each row is flattened through the person's own primary\n * holder instead (`contactPrimaryGroup`), so an org-wide member reads the\n * profile the first capturing site keeps — and the \"Known by\" column says\n * which other sites hold one.\n *=========================================*/\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n\n /*\n * WHAT THE LIST WAS OPENED FOR (AGL-2612).\n *\n * Two surfaces address this list with a question rather than a record: a\n * form's own page (\"who came in through this form\") and an Inbox\n * submission row (\"open the person with this address\"). Both arrive as\n * query keys, parsed once into the same filter the grid's panel would\n * produce, so the seeded query IS a typed query and nothing here is a\n * second predicate.\n */\n const searchParams = useSearchParams()\n const seed = useMemo(() => contactsListSeed(searchParams), [searchParams])\n /*\n * The seed as CLAUSES, in front of everything else (AGL-2617).\n *\n * A view holds a list of clauses in the grammar the grid's panel speaks,\n * and the seed is two of them — the form's captures and the address, plus\n * the source the form link names. They lead every state the list works\n * from, whichever view is open, so the query serves the seed (it reaches\n * the whole collection, as it did before views) and a view narrows what\n * comes back. The controller keeps them out of what a view SAVES.\n */\n const seedFilters = useMemo<CrmViewFilterClause[]>(\n () => [\n ...(seed.filter ? [seed.filter] : []),\n ...(seed.source\n ? [{ field: CRM_CONTACT_VIEW_FIELDS.source, op: 'equals', value: seed.source }]\n : []),\n ],\n [seed],\n )\n const viewInitial = useMemo(() => ({ filters: seedFilters }), [seedFilters])\n const views = useCrmSavedView({\n section: 'contacts',\n hostId,\n org,\n basePath: basePath ?? '',\n initial: viewInitial,\n })\n /*\n * The grammar this list speaks: the declared fields, and one field per\n * custom contact field the org has defined — so a view can filter on\n * \"plan is enterprise\" the way it filters on a stage.\n */\n const filterFields = useMemo(\n () => [...CONTACT_LIST_FILTER_FIELDS, ...contactCustomFilterFields(customFields.active)],\n [customFields.active],\n )\n const filterHeaders = useMemo(\n () => ({\n ...CONTACT_LIST_FILTER_HEADERS,\n ...contactCustomFilterHeaders(customFields.active),\n }),\n [customFields.active],\n )\n /*\n * ONE clause on the query, the rest over the window.\n *\n * Firestore takes one array clause per query and composes the rest only\n * through indexes nobody built for every pair of columns, so the first\n * clause the translator can serve is the one that reaches every contact\n * and every other narrows the thousand the query returned. The caption\n * under the bar says which is which; the chips mark the served one.\n */\n const plan = useMemo(\n () => listFilterPlan(filterFields, views.state.filters),\n [filterFields, views.state.filters],\n )\n const filter = plan.served\n /**\n * The one filter that cannot carry the scope clause.\n *\n * `formIds` is an `array-contains` on the mirror, and Firestore takes one\n * array clause per query — so this listener drops its `visibleTo`\n * predicate for it, exactly as the company contacts card does for\n * `companyIds`, and the rules then admit the read to an org-wide member\n * only. A scoped member's listener errors instead of listing, and the\n * alert below says so rather than showing an empty form.\n */\n const byForm = filter?.field === 'formIds'\n const {\n data: contactDocs,\n status: contactsStatus,\n /**\n * The rows the profile drawer is seeded from are unconfirmed by the\n * server (AGL-1358). This payload is narrower than most sites in this\n * issue — `email`, `sources` and `interactions` are not in it — but the\n * two fields that are, `tags` and `notes`, are BOTH written on every\n * save and both come off the seed. Edit the notes against a cached read\n * and the tags go back with them, and tags are what\n * `contactMatchesSegment` runs on: a saved segment is a campaign\n * audience, so a rollback here silently changes who gets emailed.\n */\n fromCache: contactsFromCache,\n } = useFirestoreCollection<any>(\n () => {\n if (!dataScope) return null\n /*\n * ORDERED, and filtered by the QUERY (AGL-2501, AGL-2292).\n *\n * Two bugs shared this one line. `limit(1000)` with no `orderBy` returns\n * documents in ID order — contacts are created with `.add()` and\n * `createResourceUid()`, so that is a pseudo-random SAMPLE of a thousand,\n * and the client `.sort()` below made it look reliably newest-first. An\n * org with forty thousand contacts saw a thousand arbitrary ones,\n * convincingly sorted.\n *\n * The search then ran over that sample, so a name on the wrong side of\n * the cap answered \"no contacts match\" — the answer a search must never\n * give wrongly, on the list a merchant uses to find one person.\n *\n * The cap STAYS: nobody needs forty thousand rows streamed into a table,\n * and the head-count has been a server aggregate since AGL-1706. What\n * changes is that the thousand are now the newest thousand, and that a\n * filter reaches the whole collection before the cap applies.\n */\n const constraints = plan.constraints\n /*\n * SCOPED, and this is the leak it closes.\n *\n * The listener had no `where()` at all: `hostId` reached it only to\n * resolve which ORG owns the data, so every site in the account listed\n * every contact in the account. An agency's client opened Contacts and\n * read the other clients' customers.\n *\n * `array-contains-any` over the group's tokens is the same predicate\n * the rules evaluate with `hasAny`, so a filtered query is provable\n * per-document and an UNFILTERED one is now permission-denied rather\n * than quietly returning everything.\n */\n return query(\n collection(firestore, dataScope[0], dataScope[1], 'contacts'),\n // Dropped for the form mirror alone — see `byForm` — and absent at\n // the organization level, where the tokens are `null` (AGL-2630).\n ...(byForm ? [] : crmVisibleToClause(visibleToTokens)),\n ...(constraints ?? [orderBy('updatedAt', 'desc')]),\n limit(1000),\n )\n },\n [firestore, dataScope, plan.constraints, byForm, visibleToTokens],\n { idField: '$id' },\n )\n /*\n * OPENED FOR ONE ADDRESS: move straight on to the record (AGL-2612).\n *\n * The Inbox links here with an email because a contact's id is minted at\n * capture and nothing outside the CRM holds it; the list is the lookup.\n * Once the seeded `email equals` query has answered with exactly one row,\n * that row is the person and the list is a detour — `replace`, so Back\n * returns to the Inbox rather than to this redirect. Nothing else\n * navigates: no match leaves the filtered list on screen, which is the\n * honest answer for a submission whose contact the band dropped, and a\n * filter the reader has since changed is theirs.\n */\n const openedByEmail = useRef(false)\n useEffect(() => {\n if (openedByEmail.current || !seed.openEmail) return\n if (contactsStatus !== 'success') return\n if (filter?.field !== 'email' || filter.value !== seed.openEmail) return\n const only = (contactDocs ?? []).length === 1 ? contactDocs?.[0] : null\n if (!only) return\n openedByEmail.current = true\n router.replace(routes.contact(String(only.$id)))\n }, [seed.openEmail, contactsStatus, filter, contactDocs, router, routes])\n /**\n * Every row, flattened through THIS group's facet.\n *\n * One projection rather than a facet read at each of the nine places that\n * touch `tags`, `notes`, `name` or `interactions`: a surface that reads one\n * of them off the top of the document is a surface showing another\n * holder's records, and nine chances to forget is nine leaks.\n *\n * The canonical `name` is the fallback and the shared identity; a holder's\n * own override wins, so one business renaming a person cannot change what\n * an unrelated business sees.\n */\n const contacts: ContactRecord[] = useMemo(\n () =>\n (contactDocs ?? []).map((row) =>\n contactRecordFromDoc(\n row,\n consentGroup ?? contactPrimaryGroup(row, org as Record<string, unknown>),\n ),\n ),\n [contactDocs, consentGroup, org],\n )\n /*\n * The roster, for the Owner column — read only once a loaded row actually\n * carries an owner. A list of people nobody has assigned never pays for\n * the team list, and the create drawer asks for it in its own right.\n */\n const [createOpen, setCreateOpen] = useState(false)\n /*\n * The filter bar's pickers name people and companies, so the roster is\n * also read once the reader has reached for a filter, or is looking at a\n * view that names an owner or a company (AGL-2617).\n */\n const [filterOpened, setFilterOpened] = useState(false)\n const namesFilter = views.state.filters.some(\n (clause) =>\n clause.field === CRM_CONTACT_VIEW_FIELDS.owner ||\n clause.field === CRM_CONTACT_VIEW_FIELDS.company,\n )\n const members = useOrgMembers(orgId, {\n enabled:\n createOpen ||\n filterOpened ||\n namesFilter ||\n contacts.some((contact) => Boolean(contact.ownerUid)),\n })\n /**\n * The HEAD-COUNT, read as a server-side aggregate (AGL-1706).\n *\n * The listener above is `limit(1000)` and always will be — nobody needs\n * 40,000 rows streamed into a table. What it must not do is answer \"how\n * many contacts does this org have\", and it did: `contacts.length`\n * saturated at 1,000, which is *exactly* the smallest paid included band\n * (`starter`). So `overageContacts = max(0, used − included)` was 0 on\n * every stock plan and the alert below could not render at all.\n *\n * Worse than the dead alert, the same capped number fed the readout in the\n * toolbar. An org with 40,000 contacts on Pro read \"1,000 contacts ·\n * 10,000 included\" — a page whose job is telling a customer where they sit\n * in their band, telling them they have room they do not have. The console\n * billing page read the truth from `getCountFromServer` on this very\n * collection, so the two surfaces disagreed about the same org's audience.\n *\n * THE LIST AND THE COUNT ARE DIFFERENT QUESTIONS and now have different\n * answers: one aggregate read per mount, the same call\n * `billing-usage.component.tsx` already makes against the same path.\n *\n * The counting RULE is untouched — `checkCrmRecordsQuota` is an\n * entitlement input and the usage cron is what bills from it. Only this\n * page's input stopped being a saturated one.\n *\n * THE BAND IS THE RECORDS BAND (AGL-2611): contacts, companies and deals\n * together, so the alert below is fed the sum of three aggregates while\n * the readout beside the toolbar keeps the people count — a workspace\n * whose hundred records are ninety companies is told it holds ten\n * contacts and that its band is full, which are both true. The three\n * reads and the fallback rule live in `useCrmRecordsQuota`, shared with\n * the company and deal drawers so every door on this hub refuses on the\n * same number.\n */\n const records = useCrmRecordsQuota(dataScope, org, {\n contactsFallback: contacts.length,\n })\n // Pending or denied, the listener length stands in. It can only UNDERSTATE\n // (it is the same collection, capped), never overstate, so no alert this\n // number gates can fire on a count larger than the truth.\n const contactCount = records.contactsCount ?? contacts.length\n // Records bands (AGL-890): a plan with an overage rate meters past the\n // included count instead of blocking; a plan without one hard-bands\n // (quota.allowed = false).\n const quota = records.quota\n // Signups whose CRM record was dropped at a hard band (AGL-891), Free's\n // included: capture still fills that band on a plan without the CRM.\n // Written by upsert-contact, host-scoped.\n // Host-scoped, so absent at the organization level (AGL-2630): the\n // counter is a fact about one site's capture, and summing thirty of them\n // would be a figure about nothing in particular.\n const { data: droppedCounter } = useFirestoreDoc<any>(\n () => (hostId ? doc(firestore, 'hosts', hostId, 'counters', 'contactsDropped') : null),\n [firestore, hostId],\n )\n const droppedTotal = Number(droppedCounter?.['total'] ?? 0)\n\n /*==========================================\n * REFUNDS THAT REACHED NO CONTACT (AGL-2329).\n *\n * `recordContactRefund` refuses to create a contact for a refund — a\n * contact holding a refund and no purchase is a phantom record with\n * negative lifetime value — and increments\n * `hosts/{hostId}/counters/contactRefundsUnmatched` instead. Its own\n * comment says the shape mirrors `contactsDropped` \"so an operator…\", and\n * there the sentence stops: `contactsDropped` had this reader and the\n * refund counter had none, so a refund that reached no contact record\n * incremented a number nobody could see.\n *\n * It sits beside the dropped-signup alert because they are the same kind\n * of fact — something that happened to this host's CRM and left no row —\n * and an operator reconciling their contact list needs both or neither.\n *=========================================*/\n const { data: unmatchedRefundCounter } = useFirestoreDoc<any>(\n () =>\n hostId\n ? doc(firestore, 'hosts', hostId, 'counters', 'contactRefundsUnmatched')\n : null,\n [firestore, hostId],\n )\n const unmatchedRefundTotal = Number(unmatchedRefundCounter?.['total'] ?? 0)\n const unmatchedRefundReason = String(\n unmatchedRefundCounter?.['lastReason'] ?? '',\n )\n\n // Saved segments (AGL-199): reusable audience filters.\n const { data: segmentDocs } = useFirestoreCollection<any>(\n () =>\n dataScope\n ? query(\n collection(\n firestore,\n dataScope[0],\n dataScope[1],\n 'contactSegments',\n ),\n limit(50),\n )\n : null,\n [firestore, dataScope],\n { idField: '$id' },\n )\n const segments = useMemo(\n () =>\n [...(segmentDocs ?? [])].sort((a, b) =>\n String(a.name ?? '').localeCompare(String(b.name ?? '')),\n ),\n [segmentDocs],\n )\n /*\n * Every saved segment, offered in the views menu as a view (AGL-2617):\n * choosing one puts its tag and source clauses on the filter bar. A\n * segment can only have come FROM an org scope, so the delete is\n * unreachable without one in practice — but the scope is nullable\n * (AGL-1050) and an unguarded deref would be a crash rather than a no-op\n * if that ever stopped being true.\n */\n const segmentPresets = useMemo<CrmViewPreset[]>(\n () =>\n segments.map((segment: any) => ({\n id: String(segment.$id),\n label: String(segment.name ?? ''),\n filters: crmViewFiltersFromSegment({\n tags: segment.tags ?? [],\n sources: segment.sources ?? [],\n }),\n onRemove: dataScope\n ? () =>\n deleteDoc(\n doc(\n firestore,\n dataScope[0],\n dataScope[1],\n 'contactSegments',\n String(segment.$id),\n ),\n )\n : undefined,\n })),\n [segments, dataScope, firestore],\n )\n\n /*\n * THE WINDOW CLAUSES narrow in the browser, and the caption says so.\n *\n * An owner, a stage, a source, a company and a custom value all live on\n * the viewing group's FACET, and a facet path is per group, so none of\n * them is a field the query grammar can reach without an index per\n * group; a second tag clause, or \"any of these tags\", is an array clause\n * the scope predicate already occupies. So every clause the plan could\n * not serve refines the ordered window rather than the collection —\n * through the same matcher the grammar declares, so a chip and the rows\n * under it agree. Ordinary orgs load their whole list into the window\n * anyway; the caption below says what these narrow for the ones that do\n * not.\n */\n const uid = user?.uid ?? ''\n const windowClauses = plan.window\n // The quick search is the grid's box, answered here over the same window.\n const [searchWords, setSearchWords] = useState<string[]>([])\n const searchKey = searchWords.join(' ')\n const visible = useMemo(\n () =>\n windowClauses.length || searchWords.length\n ? contacts.filter(\n (contact) =>\n windowClauses.every((clause) =>\n matchListFilter(contact, filterFields, clause),\n ) && crmRowMatchesSearch(contact, CONTACT_SEARCH_FIELDS, searchWords),\n )\n : contacts,\n // `searchKey` stands for the words, which are a new array each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [contacts, windowClauses, filterFields, searchKey],\n )\n /*\n * The choices the filter bar picks from. The companies are read only once\n * the reader has reached for a filter or is looking at a view that names\n * one — a read a list nobody is narrowing should not pay for, the same\n * bargain the roster gets above. A stage and a source are fixed lists and\n * cost nothing; a custom `select` field offers its own options.\n */\n const companies = useCompanyOptions({\n hostId,\n org,\n enabled: filterOpened || namesFilter,\n })\n const filterOptions = useMemo<Record<string, readonly CrmFilterOption[]>>(\n () => ({\n [CRM_CONTACT_VIEW_FIELDS.owner]: [\n ...(uid ? [{ value: uid, label: 'Me' }] : []),\n ...members.options\n .filter((option) => option.uid !== uid)\n .map((option) => ({ value: option.uid, label: Aglyn.crmMemberPickerLabel(option) })),\n ],\n [CRM_CONTACT_VIEW_FIELDS.stage]: CONTACT_LIFECYCLE_STAGES.map((stage) => ({\n value: stage,\n label: CONTACT_LIFECYCLE_STAGE_LABELS[stage],\n })),\n [CRM_CONTACT_VIEW_FIELDS.source]: Object.entries(SOURCE_LABELS).map(\n ([value, label]) => ({ value, label }),\n ),\n [CRM_CONTACT_VIEW_FIELDS.company]: companies.options.map((company) => ({\n value: company.id,\n label: company.name,\n })),\n // The verdict on the address (AGL-3245): \"Email is bounced\".\n emailState: Aglyn.EMAIL_STATE_STATUSES.map((status) => ({\n value: status,\n label: Aglyn.EMAIL_STATE_LABELS[status],\n })),\n ...Object.fromEntries(\n customFields.active\n .filter((definition) => definition.type === 'select' && definition.options?.length)\n .map((definition) => [\n crmContactCustomColumn(definition.key),\n (definition.options ?? []).map((option) => ({ value: option, label: option })),\n ]),\n ),\n }),\n [uid, members.options, companies.options, customFields.active],\n )\n\n /* One row grammar, the console's (AGL-2501) — the same table everywhere. */\n const contactColumns = useMemo(\n () => [\n ...contactListColumns({\n memberName: members.memberName,\n // \"Known by\", at the organization level only (AGL-2630).\n siteName: mount ? mount.siteName : undefined,\n }),\n // The org's custom fields as optional columns (AGL-2601).\n ...customFieldColumns(customFields.active),\n ],\n [members.memberName, mount, customFields.active],\n )\n /*\n * The grid's column and sort models are the VIEW'S (AGL-2617). The\n * filter-only hidden columns — the declared fields that are not table\n * columns, never the custom ones, which are shown — stay hidden whatever\n * a view says, so a saved arrangement cannot unhide a strip of blank\n * cells.\n */\n /*\n * The grid's own Filters panel and quick search edit the view's clauses\n * (AGL-3313). The picked fields — owner, stage, source, company, email\n * verdict, a custom select — are select columns over the same choices the\n * chips name them by; the rest keep the operators the grammar declares.\n */\n const filterColumns = useMemo(\n () => crmFilterColumns(contactColumns, filterFields, filterOptions, filterHeaders),\n [contactColumns, filterFields, filterOptions, filterHeaders],\n )\n const gridFilter = useCrmGridFilter({\n clauses: views.state.filters,\n onChange: views.setFilters,\n selectFields: Object.keys(filterOptions),\n search: { words: searchWords, onChange: setSearchWords },\n })\n const grid = useCrmViewGrid(views, filterColumns, HIDDEN_COLUMNS)\n\n /*\n * A SEGMENT IS A VIEW'S TAG AND SOURCE CLAUSES, kept where a campaign\n * can reach them (AGL-2617).\n *\n * The segment stays: it is what a campaign audience and a dynamic list\n * resolve, and `contactMatchesSegment` is the one predicate both the\n * console and the sender read. What changed is where it comes from — the\n * filter bar's tag and source clauses, read back through\n * `crmViewSegmentFilters`, so \"Save as segment\" is offered exactly when\n * the working filters hold something a segment can hold. Each saved\n * segment is offered in the views menu as a preset, and applying one\n * puts its clauses on the bar, where they can be narrowed further and\n * saved as a view.\n */\n const segmentFilters = useMemo(\n () => crmViewSegmentFilters(views.state.filters),\n [views.state.filters],\n )\n const [segmentName, setSegmentName] = useState<string | null>(null)\n const handleSaveSegment = useCallback(async () => {\n const name = (segmentName ?? '').trim().slice(0, 60)\n // No org, no place to put it (AGL-1050). The button is disabled in\n // this state; the guard is here so the callback cannot outlive it.\n if (!name || !segmentFilters || !dataScope) return\n setSegmentName(null)\n try {\n await addDoc(\n collection(firestore, dataScope[0], dataScope[1], 'contactSegments'),\n {\n name,\n tags: segmentFilters.tags ?? [],\n sources: segmentFilters.sources ?? [],\n // `contactSegments` is in SCOPED_COLLECTIONS and was the one\n // member of it with no `array-contains-any` reader and no rules\n // `hasAny` — the rules gate it on `isOrgWideMember()` and\n // `campaign-send` checks it with `visibleToHost`, which passes on\n // a missing field. So nothing broke, which is precisely why this\n // went unnoticed (AGL-1478): a collection listed as scoped that\n // nothing enforces is a trap set for whoever wires up the first\n // scoped read, and they would inherit every segment ever saved\n // already broken. Stamped at creation instead, like its siblings.\n //\n // Org-wide, unconditionally: `useOrgDataScope` resolves to\n // `['orgs', orgId]` or to null, AGL-1061 having removed the\n // `hosts/{hostId}` branch after counting zero documents there.\n // A segment is a saved filter over org contacts, so it is exactly\n // as visible as they are.\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n createdAt: new Date(),\n },\n )\n enqueueSnackbar(\n `Segment \"${name}\" saved — usable as a campaign audience`,\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [segmentName, segmentFilters, firestore, dataScope, enqueueSnackbar])\n\n /** The rows ticked for a bulk action (AGL-2603); the bar above the table acts on them. */\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n /*==========================================\n * ADDING ONE PERSON BY HAND (AGL-2596).\n *\n * Through the plugin's server route rather than a client write, because\n * creating a contact is the one act on this surface the rules cannot fully\n * judge: the dedupe against every holder's rows is a lookup the capturing\n * site may not read, and the audience band is a count the browser cannot\n * take. The route answers `{ contactId, created }`; a merge onto somebody\n * already in the address book is not an error, and the page says which\n * happened before it opens the record.\n *=========================================*/\n // The site the route resolves the org from and stamps the record with: the\n // mounted site, or at the organization level the one the drawer's picker\n // named (AGL-2630) — the drawer holds its submit until it is known.\n const crmApi = useCrmApi(crmScope.createHostId)\n const [createBusy, setCreateBusy] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const handleCreate = useCallback(\n async (values: NewContactValues) => {\n setCreateBusy(true)\n setCreateError(null)\n try {\n const { response, payload } = await crmApi('contacts-create', {\n email: values.email,\n ...(values.name ? { name: values.name } : {}),\n ...(values.phone ? { phone: values.phone } : {}),\n ...(values.jobTitle ? { jobTitle: values.jobTitle } : {}),\n ...(values.companyName ? { companyName: values.companyName } : {}),\n ...(values.companyId ? { companyId: values.companyId } : {}),\n ...(values.address ? { address: values.address } : {}),\n ...(values.ownerUid ? { ownerUid: values.ownerUid } : {}),\n ...(values.lifecycleStage\n ? { lifecycleStage: values.lifecycleStage }\n : {}),\n ...(values.tags.length ? { tags: values.tags } : {}),\n marketingConsent: values.marketingConsent,\n })\n if (!response.ok) {\n // The route's own sentence, shown above the form unchanged: the\n // band refusal is the list's wording, and a field refusal names\n // the field.\n setCreateError(\n String(payload['error'] ?? 'The contact could not be added.'),\n )\n return\n }\n const contactId = String(payload['contactId'] ?? '')\n // The activity entry is the route's (AGL-2622): it verified the\n // caller and performed the write, so it is the one writer that\n // cannot record an add that did not happen — and a second entry\n // from here would put two rows on one act.\n enqueueSnackbar(\n payload['created']\n ? 'Contact added'\n : 'Already a contact — what you entered was merged into their record',\n { variant: 'success', persist: false },\n )\n setCreateOpen(false)\n if (contactId) router.push(routes.contact(contactId))\n } catch (error) {\n console.error(error)\n setCreateError('The contact could not be added.')\n } finally {\n setCreateBusy(false)\n }\n },\n [crmApi, enqueueSnackbar, router, routes],\n )\n\n /*\n * The file is `contactsCsv()`'s — every CRM column, the owner by address,\n * one column per custom field — and the same options reach the bulk bar,\n * so the table's export and the selection's are one file over two row\n * sets (AGL-2621).\n */\n const csvOptions: ContactCsvOptions = useMemo(\n () => ({ ownerEmail: members.memberEmail, customFields: customFields.active }),\n [members.memberEmail, customFields.active],\n )\n const handleExport = useCallback(() => {\n downloadTextFile('contacts.csv', 'text/csv', contactsCsv(visible, csvOptions))\n }, [visible, csvOptions])\n\n return (\n <>\n <CardDisplay\n header={'Contacts'}\n help={pluginDocsHelp('contacts', { anchor: '#the-contacts-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ flex: 1 }}>\n {/* The org's audience, not the page's row count (AGL-1706) —\n these two stopped being the same number the moment the\n listener grew a `limit(1000)`. */}\n {`${contactCount.toLocaleString()} contacts · ${\n Number.isFinite(quota.included)\n ? `${quota.used.toLocaleString()} of ${quota.included.toLocaleString()} CRM records`\n : `${quota.used.toLocaleString()} CRM records`\n }`}\n </Typography>\n {suiteIncluded ? (\n <ContactImportButton hostId={hostId} org={org} />\n ) : (\n <CrmSuiteLockedButton>{'Import CSV'}</CrmSuiteLockedButton>\n )}\n <Button\n size=\"small\"\n onClick={handleExport}\n disabled={!visible.length}\n >\n {'Export CSV'}\n </Button>\n {/* A button that opens a drawer — never a create form above the\n list. Disabled until the org has resolved, because the route\n resolves the org from the site and a click before that has\n nowhere to write. */}\n {suiteIncluded ? (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={!dataScope}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {'New contact'}\n </Button>\n ) : (\n <CrmSuiteLockedButton variant=\"contained\" color=\"primary\">\n {'New contact'}\n </CrmSuiteLockedButton>\n )}\n </Stack>\n {suiteIncluded ? null : (\n <CrmSuiteNotice>\n {'Contacts arrive here on their own from your forms, sign-ups, ' +\n 'orders and bookings. Adding one by hand, importing a CSV, ' +\n \"saved views, and a contact's owner, stage and company are \" +\n 'part of the CRM.'}\n </CrmSuiteNotice>\n )}\n {/*\n The view this list is showing, and the clauses narrowing it\n (AGL-2617). The control names the view and holds everything a\n reader does to one; the bar holds the clauses, the seed's\n included, with the one the query serves marked. Segments are\n offered in the control's menu and saved from it.\n */}\n <Stack spacing={1}>\n <CrmViewsControl\n controller={views}\n allLabel=\"All contacts\"\n suiteLocked={!suiteIncluded}\n presets={segmentPresets}\n onSaveAsSegment={\n segmentFilters && dataScope ? () => setSegmentName('') : null\n }\n />\n <CrmFilterBar\n fields={filterFields}\n headers={filterHeaders}\n clauses={views.state.filters}\n onChange={views.setFilters}\n options={filterOptions}\n servedField={plan.served?.field ?? null}\n />\n </Stack>\n {!quota.allowed ? (\n <Alert severity=\"warning\">\n {'CRM records limit reached — new visitors are no longer ' +\n 'captured' +\n (droppedTotal > 0\n ? ` (${droppedTotal.toLocaleString()} missed so far)`\n : '') +\n '. Upgrade in Billing to keep collecting.'}\n </Alert>\n ) : quota.overageRecords > 0 &&\n quota.overageRateUsd != null &&\n // No claim about money until the verdict that decides it has\n // settled (AGL-1662). Before Remote Config activation the flag\n // reads its registry default, which is no org's verdict, so an\n // ungated alert could assert the wrong wording for one paint.\n releaseFlagsReady ? (\n <Alert severity=\"info\">\n {contactsBilled\n ? // Same sentence as the billing page's caption, and the same\n // basis (AGL-2399): the count here is LIVE, the invoice\n // charges the last reading before the month closes, so the\n // dollar figure is a projection until the month ends. Staff\n // and customer must not read different sentences about the\n // same org's money — that applies to WHEN it is measured as\n // much as to how much.\n `${quota.overageRecords.toLocaleString()} CRM records over ` +\n `your plan's included ${quota.included.toLocaleString()} — ` +\n `metered at $${quota.overageRateUsd}/1,000 per month ` +\n `(≈$${quota.overageMonthlyUsd.toFixed(2)} if your list ends ` +\n 'the month at this size). ' +\n 'Upgrade in Billing for a larger included audience.'\n : // The wording `db5ecdf2b` put on the billing page, which is\n // itself the wording `1a2aed5cb` published to\n // `billing-and-plans/overview.md` (AGL-1601/1603). Staff and\n // customer must not read different sentences about the same\n // org's money.\n //\n // THE COUNT STAYS, THE TOTAL GOES: the head-count is real —\n // ingestion captured those records — and is not a claim\n // about money. The upgrade nudge goes with the total, since\n // it prompts a purchase premised on a charge that is not\n // happening.\n `${quota.overageRecords.toLocaleString()} CRM records over ` +\n `your plan's included ${quota.included.toLocaleString()} — ` +\n 'not billed while the CRM is unavailable. ' +\n `The $${quota.overageRateUsd}/1,000 rate applies once ` +\n 'Contacts opens.'}\n </Alert>\n ) : droppedTotal > 0 ? (\n <Alert severity=\"info\">\n {`${droppedTotal.toLocaleString()} earlier visitor${\n droppedTotal === 1 ? ' was' : 's were'\n } not captured while your CRM records band was full.`}\n </Alert>\n ) : null}\n {/*\n Unlike the band alert above, this one is NOT exclusive with the\n others: a host can be over its band AND have refunds that landed\n nowhere, and the two have different remedies. Chaining it into the\n same ternary would hide whichever fact came second.\n */}\n {unmatchedRefundTotal > 0 ? (\n <Alert severity=\"warning\">\n {`${unmatchedRefundTotal.toLocaleString()} refund${\n unmatchedRefundTotal === 1 ? '' : 's'\n } could not be recorded against a contact${\n UNMATCHED_REFUND_REASON[unmatchedRefundReason]\n ? ` — most recently because ${UNMATCHED_REFUND_REASON[unmatchedRefundReason]}`\n : ''\n }. The money moved; the customer's timeline does not show it.`}\n </Alert>\n ) : null}\n {byForm && contactsStatus === 'error' ? (\n <Alert severity=\"info\">\n {'The contacts this form captured could not be listed. Your ' +\n 'access is limited to specific sites, and a form filter ' +\n 'cannot be narrowed to them — an organization administrator ' +\n 'can see it.'}\n </Alert>\n ) : contacts.length === 0 && filter ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {contactsStatus === 'loading'\n ? 'Loading…'\n : 'No contacts match this filter.'}\n </Typography>\n ) : contacts.length === 0 ? (\n <EmptyStateComponent\n label={contactsStatus === 'loading' ? 'Loading contacts…' : 'No contacts yet'}\n description={\n contactsStatus === 'loading'\n ? undefined\n : suiteIncluded\n ? 'Form submissions, member sign-ups, orders and bookings become contacts on their own; add one by hand or bring a list in from a CSV.'\n : 'Form submissions, member sign-ups, orders and bookings become contacts on their own.'\n }\n action={\n contactsStatus === 'loading' ? undefined : !suiteIncluded ? (\n <Stack direction=\"row\" spacing={1}>\n <CrmSuiteLockedButton variant=\"contained\" color=\"primary\">\n {'New contact'}\n </CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Import CSV'}</CrmSuiteLockedButton>\n </Stack>\n ) : (\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={!dataScope}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {'New contact'}\n </Button>\n <ContactImportButton hostId={hostId} org={org} />\n </Stack>\n )\n }\n />\n ) : (\n <>\n {/* Which clauses reached the whole collection and which\n narrowed the loaded window. Said out loud, because a control\n that narrows less than it looks like it does is the thing\n this page has been fixing. */}\n {windowClauses.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {(windowClauses.length === 1\n ? `${filterHeaders[windowClauses[0].field] ?? windowClauses[0].field} narrows `\n : `${windowClauses.length} of the filters narrow `) +\n 'the loaded window — the newest 1,000 contacts' +\n (plan.served\n ? ` matching ${filterHeaders[plan.served.field] ?? plan.served.field}, which reached every contact.`\n : '.') +\n ' Sorting reorders that window.'}\n </Typography>\n ) : null}\n <ContactsBulkBar hostId={hostId} org={org} scope={dataScope} consentGroup={consentGroup} rows={visible} selected={selectedIds} onSelectedChange={setSelectedIds} csv={csvOptions} suiteLocked={!suiteIncluded} />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={visible}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n onOpen={(id) => router.push(routes.contact(id))}\n /*\n * The grid's Filters panel and quick search edit the view's\n * clauses; the list answers them, onto the query where it\n * can and over the loaded window where it cannot, so the\n * grid must not filter again itself (AGL-3313). Opening the\n * panel is what reads the roster and the companies its\n * pickers name. Columns and sort are the view's, controlled\n * so a saved arrangement is what the grid shows.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n onPreferencePanelOpen={() => setFilterOpened(true)}\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={grid.onColumnVisibilityModelChange}\n sortModel={grid.sortModel}\n onSortModelChange={grid.onSortModelChange}\n />\n </CrmColumnOrderProvider>\n </>\n )}\n <RecentActivityFeed hostId={hostId} org={org} basePath={props.basePath} />\n </Stack>\n </CardDisplay>\n {/*\n Naming a segment is a dialog, never a form above the list. Open\n while the name is a string; `null` is closed.\n */}\n <Dialog\n open={segmentName !== null}\n onClose={() => setSegmentName(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{'Save as segment'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The tag and source filters, kept as a segment an email campaign or a dynamic list can send to. The other filters stay on this view only.'}\n </Typography>\n <TextField\n autoFocus\n size=\"small\"\n label=\"Segment name\"\n value={segmentName ?? ''}\n onChange={(event) => setSegmentName(event.target.value.slice(0, 60))}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n void handleSaveSegment()\n }\n }}\n slotProps={{ htmlInput: { maxLength: 60 } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setSegmentName(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n disabled={!(segmentName ?? '').trim() || !dataScope}\n onClick={() => void handleSaveSegment()}\n >\n {'Save segment'}\n </Button>\n </DialogActions>\n </Dialog>\n {/*\n Mounted only while open, so a page that never reaches for it never\n renders a drawer's chrome, and every opening starts from a blank form.\n */}\n {createOpen ? (\n <NewContactDrawer\n open\n onClose={() => setCreateOpen(false)}\n hostId={hostId}\n org={org}\n busy={createBusy}\n error={createError}\n owners={members.options}\n ownersReady={members.ready}\n onSubmit={(values) => void handleCreate(values)}\n />\n ) : null}\n </>\n )\n}\nContactsPeopleSection.displayName = 'ContactsPeopleSection'\n\nexport default ContactsPeopleSection\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","CONTACT_SOURCE_LABELS","CRM_CONTACT_VIEW_FIELDS","crmContactCustomColumn","crmViewFiltersFromSegment","crmViewSegmentFilters","newResourceScopeFields","ORG_SCOPE_TOKEN","pluginDocsHelp","hiddenFilterVisibility","matchListFilter","ListTable","CONTACT_LIST_FILTER_FIELDS","CONTACT_LIST_FILTER_HEADERS","contactCustomFilterFields","contactCustomFilterHeaders","contactPrimaryGroup","contactRecordFromDoc","useCrmOrgMount","crmVisibleToClause","useCrmScope","contactsListSeed","crmRoutes","contactsCsv","downloadTextFile","useCrmRecordsQuota","CardDisplay","EmptyStateComponent","ContactsBulkBar","ContactImportButton","useSnackbar","listFilterPlan","useFirestore","useFirestoreCollection","useFirestoreDoc","useUser","Alert","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","addDoc","collection","deleteDoc","doc","limit","orderBy","query","useRouter","useSearchParams","useCallback","useEffect","useMemo","useRef","useState","RecentActivityFeed","CONTACT_FILTER_COLUMNS","CONTACT_OPTIONAL_COLUMNS","contactListColumns","NewContactDrawer","useCrmApi","useOrgMembers","useContactFieldDefinitions","customFieldColumns","useCrmSavedView","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","useCompanyOptions","CrmFilterBar","crmFilterColumns","crmRowMatchesSearch","useCrmGridFilter","CrmViewsControl","CrmSuiteLockedButton","CrmSuiteNotice","crmSuiteIncluded","SOURCE_LABELS","CONTACT_SEARCH_FIELDS","HIDDEN_COLUMNS","Object","fromEntries","map","field","UNMATCHED_REFUND_REASON","ContactsPeopleSection","props","records","filterHeaders","plan","hostId","org","releaseFlag","basePath","routes","router","contactsBilled","released","releaseFlagsReady","ready","crmScope","scope","dataScope","orgId","consentGroup","visibleTo","visibleToTokens","suiteIncluded","mount","customFields","firestore","enqueueSnackbar","data","user","searchParams","seed","seedFilters","filter","source","op","value","viewInitial","filters","views","section","initial","filterFields","active","state","served","byForm","contactDocs","status","contactsStatus","fromCache","contactsFromCache","constraints","idField","openedByEmail","current","openEmail","only","length","replace","contact","String","$id","contacts","row","createOpen","setCreateOpen","filterOpened","setFilterOpened","namesFilter","some","clause","owner","company","members","enabled","Boolean","ownerUid","contactsFallback","contactCount","contactsCount","quota","droppedCounter","droppedTotal","Number","unmatchedRefundCounter","unmatchedRefundTotal","unmatchedRefundReason","segmentDocs","segments","sort","a","b","name","localeCompare","segmentPresets","segment","id","label","tags","sources","onRemove","undefined","uid","windowClauses","window","searchWords","setSearchWords","searchKey","join","visible","every","companies","filterOptions","options","option","crmMemberPickerLabel","stage","entries","emailState","EMAIL_STATE_STATUSES","EMAIL_STATE_LABELS","definition","type","key","contactColumns","memberName","siteName","filterColumns","gridFilter","clauses","onChange","setFilters","selectFields","keys","search","words","grid","segmentFilters","segmentName","setSegmentName","handleSaveSegment","trim","slice","createdAt","Date","variant","persist","error","console","allowDuplicate","selectedIds","setSelectedIds","crmApi","createHostId","createBusy","setCreateBusy","createError","setCreateError","handleCreate","values","payload","response","email","phone","jobTitle","companyName","companyId","address","lifecycleStage","marketingConsent","ok","contactId","push","csvOptions","ownerEmail","memberEmail","handleExport","header","help","anchor","contentGutterX","contentGutterY","spacing","direction","sx","alignItems","flexWrap","rowGap","color","flex","toLocaleString","isFinite","included","used","size","onClick","disabled","controller","allLabel","suiteLocked","presets","onSaveAsSegment","fields","headers","servedField","allowed","severity","overageRecords","overageRateUsd","overageMonthlyUsd","toFixed","description","action","rows","selected","onSelectedChange","csv","columnOrder","columns","slots","selectable","onOpen","filterMode","filterModel","onFilterModelChange","quickFilter","onPreferencePanelOpen","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","open","onClose","maxWidth","fullWidth","pt","autoFocus","event","target","onKeyDown","preventDefault","slotProps","htmlInput","maxLength","busy","owners","ownersReady","onSubmit","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,YAAYA,WAAW,eAAc;AACrC,SACEC,8BAA8B,EAC9BC,wBAAwB,EACxBC,qBAAqB,EACrBC,uBAAuB,EAEvBC,sBAAsB,EACtBC,yBAAyB,EACzBC,qBAAqB,EACrBC,sBAAsB,EACtBC,eAAe,EACfC,cAAc,QACT,eAAc;AAErB,SACEC,sBAAsB,EACtBC,eAAe,QACV,yCAAwC;AAC/C,SAASC,SAAS,QAAQ,uDAAsD;AAChF,SACEC,0BAA0B,EAC1BC,2BAA2B,EAC3BC,yBAAyB,EACzBC,0BAA0B,QACrB,kCAA8B;AACrC,SAEEC,mBAAmB,EACnBC,oBAAoB,QACf,6BAAyB;AAChC,SAASC,cAAc,QAAQ,gCAA4B;AAC3D,SAASC,kBAAkB,EAAEC,WAAW,QAAQ,4BAAwB;AACxE,SAASC,gBAAgB,QAAQ,iCAA6B;AAC9D,SAASC,SAAS,QAAQ,yBAAqB;AAC/C,SAEEC,WAAW,EACXC,gBAAgB,QACX,2BAAuB;AAC9B,SAASC,kBAAkB,QAAQ,oCAAgC;AACnE,SAASC,WAAW,QAAQ,uBAAsB;AAClD,OAAOC,yBAAyB,wDAAuD;AACvF,OAAOC,qBAAqB,yBAAqB;AACjD,SAASC,mBAAmB,QAAQ,6BAAyB;AAC7D,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,cAAc,EACdC,YAAY,EACZC,sBAAsB,EACtBC,eAAe,EACfC,OAAO,QACF,iCAAgC;AACvC,SACEC,KAAK,EACLC,MAAM,EACNC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,WAAW,EACXC,KAAK,EACLC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SACEC,MAAM,EACNC,UAAU,EACVC,SAAS,EACTC,GAAG,EACHC,KAAK,EACLC,OAAO,EACPC,KAAK,QAEA,qBAAoB;AAC3B,SAASC,SAAS,EAAEC,eAAe,QAAQ,kBAAiB;AAC5D,SAASC,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AACzE,OAAOC,wBAAwB,4BAAwB;AACvD,SACEC,sBAAsB,EACtBC,wBAAwB,EACxBC,kBAAkB,QACb,4BAAwB;AAC/B,OAAOC,sBAAiD,0BAAsB;AAC9E,SAASC,SAAS,QAAQ,mBAAe;AACzC,SAASC,aAAa,QAAQ,uBAAmB;AACjD,SAASC,0BAA0B,QAAQ,4CAAwC;AACnF,SAASC,kBAAkB,QAAQ,8BAA0B;AAC7D,SAASC,eAAe,QAAQ,iCAA6B;AAC7D,SAASC,cAAc,QAAQ,gCAA4B;AAC3D,SAASC,cAAc,EAAEC,sBAAsB,QAAQ,uBAAmB;AAC1E,SAASC,iBAAiB,QAAQ,sBAAkB;AACpD,OAAOC,kBAAkB,sBAAkB;AAC3C,SAEEC,gBAAgB,EAChBC,mBAAmB,QACd,8BAA0B;AACjC,SAASC,gBAAgB,QAAQ,kCAA8B;AAC/D,OAAOC,qBAA6C,yBAAqB;AACzE,SAASC,oBAAoB,EAAEC,cAAc,EAAEC,gBAAgB,QAAQ,sBAAkB;AAEzF;;;;;CAKC,GACD,MAAMC,gBAAgBhF;AAEtB,mFAAmF,GACnF,MAAMiF,wBAAwB;IAAC;IAAQ;IAAS;IAAS;IAAW;CAAO;AAE3E;;;;;CAKC,GACD,MAAMC,iBAAoD,aACrD1E,uBAAuBG,4BAA4BgD,yBACnDwB,OAAOC,WAAW,CAACxB,yBAAyByB,GAAG,CAAC,CAACC,QAAU;QAACA;QAAO;KAAM;AAG9E;;;;;;;;CAQC,GACD,MAAMC,0BAAkD;IACtD,YAAY;IACZ,cAAc;IACd,mBAAmB;AACrB;AAEA;;;;;;CAMC,GACD;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,sBAAsBC,KAA6B;4BAuU5CC,2DAklBEC,qCAIYA;QA1IVC;IAlxBzB,MAAM,EAAEC,MAAM,EAAEC,GAAG,EAAEC,WAAW,EAAEC,QAAQ,EAAE,GAAGP;IAC/C;;;;;;GAMC,GACD,MAAMQ,SAAS5E,UAAU2E,mBAAAA,WAAY;IACrC,MAAME,SAAS/C;IACf,iEAAiE;IACjE,+DAA+D;IAC/D,EAAE;IACF,oEAAoE;IACpE,oEAAoE;IACpE,4EAA4E;IAC5E,4EAA4E;IAC5E,wEAAwE;IACxE,0EAA0E;IAC1E,6EAA6E;IAC7E,EAAE;IACF,4EAA4E;IAC5E,mDAAmD;IACnD,EAAE;IACF,0EAA0E;IAC1E,yEAAyE;IACzE,0EAA0E;IAC1E,sCAAsC;IACtC,MAAMgD,yBAAiBJ,+BAAAA,YAAaK,QAAQ,mBAAI;IAChD,MAAMC,6BAAoBN,+BAAAA,YAAaO,KAAK,oBAAI;IAChD,oEAAoE;IACpE,yEAAyE;IACzE,sEAAsE;IACtE,wEAAwE;IACxE,uEAAuE;IACvE,0EAA0E;IAC1E,8DAA8D;IAC9D,MAAMC,WAAWpF,YAAY;QAAE0E;QAAQC;IAAI;IAC3C,MAAM,EAAEU,OAAOC,SAAS,EAAEC,KAAK,EAAEC,YAAY,EAAEC,WAAWC,eAAe,EAAE,GAAGN;IAC9E;;;;;;;GAOC,GACD,MAAMO,gBAAgB/B,iBAAiBe;IACvC,uEAAuE;IACvE,6CAA6C;IAC7C,MAAMiB,QAAQ9F;IACd,sEAAsE;IACtE,MAAM+F,eAAe/C,oCAA2BwC,6BAAAA,SAAW,CAAC,EAAE,oBAAI;IAClE;;;;;;;;;;;;;;;;;;6CAkB2C,GAC3C,MAAMQ,YAAYlF;IAClB,MAAM,EAAEmF,eAAe,EAAE,GAAGrF;IAC5B,MAAM,EAAEsF,MAAMC,IAAI,EAAE,GAAGlF;IAEvB;;;;;;;;;GASC,GACD,MAAMmF,eAAejE;IACrB,MAAMkE,OAAO/D,QAAQ,IAAMnC,iBAAiBiG,eAAe;QAACA;KAAa;IACzE;;;;;;;;;GASC,GACD,MAAME,cAAchE,QAClB,IAAM;eACA+D,KAAKE,MAAM,GAAG;gBAACF,KAAKE,MAAM;aAAC,GAAG,EAAE;eAChCF,KAAKG,MAAM,GACX;gBAAC;oBAAEnC,OAAOrF,wBAAwBwH,MAAM;oBAAEC,IAAI;oBAAUC,OAAOL,KAAKG,MAAM;gBAAC;aAAE,GAC7E,EAAE;SACP,EACD;QAACH;KAAK;IAER,MAAMM,cAAcrE,QAAQ,IAAO,CAAA;YAAEsE,SAASN;QAAY,CAAA,GAAI;QAACA;KAAY;IAC3E,MAAMO,QAAQ3D,gBAAgB;QAC5B4D,SAAS;QACTlC;QACAC;QACAE,QAAQ,EAAEA,mBAAAA,WAAY;QACtBgC,SAASJ;IACX;IACA;;;;GAIC,GACD,MAAMK,eAAe1E,QACnB,IAAM;eAAI5C;eAA+BE,0BAA0BmG,aAAakB,MAAM;SAAE,EACxF;QAAClB,aAAakB,MAAM;KAAC;IAEvB,MAAMvC,gBAAgBpC,QACpB,IAAO,aACF3C,6BACAE,2BAA2BkG,aAAakB,MAAM,IAEnD;QAAClB,aAAakB,MAAM;KAAC;IAEvB;;;;;;;;GAQC,GACD,MAAMtC,OAAOrC,QACX,IAAMzB,eAAemG,cAAcH,MAAMK,KAAK,CAACN,OAAO,GACtD;QAACI;QAAcH,MAAMK,KAAK,CAACN,OAAO;KAAC;IAErC,MAAML,SAAS5B,KAAKwC,MAAM;IAC1B;;;;;;;;;GASC,GACD,MAAMC,SAASb,CAAAA,0BAAAA,OAAQlC,KAAK,MAAK;IACjC,MAAM,EACJ6B,MAAMmB,WAAW,EACjBC,QAAQC,cAAc,EACtB;;;;;;;;;KASC,GACDC,WAAWC,iBAAiB,EAC7B,GAAG1G,uBACF;QACE,IAAI,CAACyE,WAAW,OAAO;QACvB;;;;;;;;;;;;;;;;;;OAkBC,GACD,MAAMkC,cAAc/C,KAAK+C,WAAW;QACpC;;;;;;;;;;;;OAYC,GACD,OAAOzF,MACLL,WAAWoE,WAAWR,SAAS,CAAC,EAAE,EAAEA,SAAS,CAAC,EAAE,EAAE,aAClD,mEAAmE;QACnE,kEAAkE;WAC9D4B,SAAS,EAAE,GAAGnH,mBAAmB2F,qBACjC8B,sBAAAA,cAAe;YAAC1F,QAAQ,aAAa;SAAQ,EACjDD,MAAM;IAEV,GACA;QAACiE;QAAWR;QAAWb,KAAK+C,WAAW;QAAEN;QAAQxB;KAAgB,EACjE;QAAE+B,SAAS;IAAM;IAEnB;;;;;;;;;;;GAWC,GACD,MAAMC,gBAAgBrF,OAAO;IAC7BF,UAAU;QACR,IAAIuF,cAAcC,OAAO,IAAI,CAACxB,KAAKyB,SAAS,EAAE;QAC9C,IAAIP,mBAAmB,WAAW;QAClC,IAAIhB,CAAAA,0BAAAA,OAAQlC,KAAK,MAAK,WAAWkC,OAAOG,KAAK,KAAKL,KAAKyB,SAAS,EAAE;QAClE,MAAMC,OAAO,CAACV,sBAAAA,cAAe,EAAE,EAAEW,MAAM,KAAK,IAAIX,+BAAAA,WAAa,CAAC,EAAE,GAAG;QACnE,IAAI,CAACU,MAAM;QACXH,cAAcC,OAAO,GAAG;QACxB5C,OAAOgD,OAAO,CAACjD,OAAOkD,OAAO,CAACC,OAAOJ,KAAKK,GAAG;IAC/C,GAAG;QAAC/B,KAAKyB,SAAS;QAAEP;QAAgBhB;QAAQc;QAAapC;QAAQD;KAAO;IACxE;;;;;;;;;;;GAWC,GACD,MAAMqD,WAA4B/F,QAChC,IACE,CAAC+E,sBAAAA,cAAe,EAAE,EAAEjD,GAAG,CAAC,CAACkE,MACvBvI,qBACEuI,KACA5C,uBAAAA,eAAgB5F,oBAAoBwI,KAAKzD,QAG/C;QAACwC;QAAa3B;QAAcb;KAAI;IAElC;;;;GAIC,GACD,MAAM,CAAC0D,YAAYC,cAAc,GAAGhG,SAAS;IAC7C;;;;GAIC,GACD,MAAM,CAACiG,cAAcC,gBAAgB,GAAGlG,SAAS;IACjD,MAAMmG,cAAc9B,MAAMK,KAAK,CAACN,OAAO,CAACgC,IAAI,CAC1C,CAACC,SACCA,OAAOxE,KAAK,KAAKrF,wBAAwB8J,KAAK,IAC9CD,OAAOxE,KAAK,KAAKrF,wBAAwB+J,OAAO;IAEpD,MAAMC,UAAUjG,cAAc0C,OAAO;QACnCwD,SACEV,cACAE,gBACAE,eACAN,SAASO,IAAI,CAAC,CAACV,UAAYgB,QAAQhB,QAAQiB,QAAQ;IACvD;IACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCC,GACD,MAAM1E,UAAUlE,mBAAmBiF,WAAWX,KAAK;QACjDuE,kBAAkBf,SAASL,MAAM;IACnC;IACA,2EAA2E;IAC3E,yEAAyE;IACzE,0DAA0D;IAC1D,MAAMqB,gBAAe5E,yBAAAA,QAAQ6E,aAAa,YAArB7E,yBAAyB4D,SAASL,MAAM;IAC7D,uEAAuE;IACvE,oEAAoE;IACpE,2BAA2B;IAC3B,MAAMuB,QAAQ9E,QAAQ8E,KAAK;IAC3B,wEAAwE;IACxE,qEAAqE;IACrE,0CAA0C;IAC1C,mEAAmE;IACnE,yEAAyE;IACzE,iDAAiD;IACjD,MAAM,EAAErD,MAAMsD,cAAc,EAAE,GAAGxI,gBAC/B,IAAO4D,SAAS9C,IAAIkE,WAAW,SAASpB,QAAQ,YAAY,qBAAqB,MACjF;QAACoB;QAAWpB;KAAO;IAErB,MAAM6E,eAAeC,gBAAOF,kCAAAA,cAAgB,CAAC,QAAQ,oBAAI;IAEzD;;;;;;;;;;;;;;;6CAe2C,GAC3C,MAAM,EAAEtD,MAAMyD,sBAAsB,EAAE,GAAG3I,gBACvC,IACE4D,SACI9C,IAAIkE,WAAW,SAASpB,QAAQ,YAAY,6BAC5C,MACN;QAACoB;QAAWpB;KAAO;IAErB,MAAMgF,uBAAuBF,gBAAOC,0CAAAA,sBAAwB,CAAC,QAAQ,oBAAI;IACzE,MAAME,wBAAwB1B,gBAC5BwB,0CAAAA,sBAAwB,CAAC,aAAa,oBAAI;IAG5C,uDAAuD;IACvD,MAAM,EAAEzD,MAAM4D,WAAW,EAAE,GAAG/I,uBAC5B,IACEyE,YACIvD,MACEL,WACEoE,WACAR,SAAS,CAAC,EAAE,EACZA,SAAS,CAAC,EAAE,EACZ,oBAEFzD,MAAM,OAER,MACN;QAACiE;QAAWR;KAAU,EACtB;QAAEmC,SAAS;IAAM;IAEnB,MAAMoC,WAAWzH,QACf,IACE;eAAKwH,sBAAAA,cAAe,EAAE;SAAE,CAACE,IAAI,CAAC,CAACC,GAAGC;gBACzBD,SAAmCC;mBAA1C/B,QAAO8B,UAAAA,EAAEE,IAAI,YAANF,UAAU,IAAIG,aAAa,CAACjC,QAAO+B,UAAAA,EAAEC,IAAI,YAAND,UAAU;YAExD;QAACJ;KAAY;IAEf;;;;;;;GAOC,GACD,MAAMO,iBAAiB/H,QACrB,IACEyH,SAAS3F,GAAG,CAAC,CAACkG;gBAEEA,eAENA,eACGA;mBALmB;gBAC9BC,IAAIpC,OAAOmC,QAAQlC,GAAG;gBACtBoC,OAAOrC,QAAOmC,gBAAAA,QAAQH,IAAI,YAAZG,gBAAgB;gBAC9B1D,SAAS1H,0BAA0B;oBACjCuL,IAAI,GAAEH,gBAAAA,QAAQG,IAAI,YAAZH,gBAAgB,EAAE;oBACxBI,OAAO,GAAEJ,mBAAAA,QAAQI,OAAO,YAAfJ,mBAAmB,EAAE;gBAChC;gBACAK,UAAUnF,YACN,IACE3D,UACEC,IACEkE,WACAR,SAAS,CAAC,EAAE,EACZA,SAAS,CAAC,EAAE,EACZ,mBACA2C,OAAOmC,QAAQlC,GAAG,MAGxBwC;YACN;YACF;QAACb;QAAUvE;QAAWQ;KAAU;IAGlC;;;;;;;;;;;;;GAaC,GACD,MAAM6E,eAAM1E,wBAAAA,KAAM0E,GAAG,oBAAI;IACzB,MAAMC,gBAAgBnG,KAAKoG,MAAM;IACjC,0EAA0E;IAC1E,MAAM,CAACC,aAAaC,eAAe,GAAGzI,SAAmB,EAAE;IAC3D,MAAM0I,YAAYF,YAAYG,IAAI,CAAC;IACnC,MAAMC,UAAU9I,QACd,IACEwI,cAAc9C,MAAM,IAAIgD,YAAYhD,MAAM,GACtCK,SAAS9B,MAAM,CACb,CAAC2B,UACC4C,cAAcO,KAAK,CAAC,CAACxC,SACnBrJ,gBAAgB0I,SAASlB,cAAc6B,YACpCpF,oBAAoByE,SAASlE,uBAAuBgH,gBAE7D3C,UACN,uEAAuE;IACvE,uDAAuD;IACvD;QAACA;QAAUyC;QAAe9D;QAAckE;KAAU;IAEpD;;;;;;GAMC,GACD,MAAMI,YAAYhI,kBAAkB;QAClCsB;QACAC;QACAoE,SAASR,gBAAgBE;IAC3B;IACA,MAAM4C,gBAAgBjJ,QACpB,IAAO;YACL,CAACtD,wBAAwB8J,KAAK,CAAC,EAAE;mBAC3B+B,MAAM;oBAAC;wBAAEnE,OAAOmE;wBAAKL,OAAO;oBAAK;iBAAE,GAAG,EAAE;mBACzCxB,QAAQwC,OAAO,CACfjF,MAAM,CAAC,CAACkF,SAAWA,OAAOZ,GAAG,KAAKA,KAClCzG,GAAG,CAAC,CAACqH,SAAY,CAAA;wBAAE/E,OAAO+E,OAAOZ,GAAG;wBAAEL,OAAO5L,MAAM8M,oBAAoB,CAACD;oBAAQ,CAAA;aACpF;YACD,CAACzM,wBAAwB2M,KAAK,CAAC,EAAE7M,yBAAyBsF,GAAG,CAAC,CAACuH,QAAW,CAAA;oBACxEjF,OAAOiF;oBACPnB,OAAO3L,8BAA8B,CAAC8M,MAAM;gBAC9C,CAAA;YACA,CAAC3M,wBAAwBwH,MAAM,CAAC,EAAEtC,OAAO0H,OAAO,CAAC7H,eAAeK,GAAG,CACjE,CAAC,CAACsC,OAAO8D,MAAM,GAAM,CAAA;oBAAE9D;oBAAO8D;gBAAM,CAAA;YAEtC,CAACxL,wBAAwB+J,OAAO,CAAC,EAAEuC,UAAUE,OAAO,CAACpH,GAAG,CAAC,CAAC2E,UAAa,CAAA;oBACrErC,OAAOqC,QAAQwB,EAAE;oBACjBC,OAAOzB,QAAQoB,IAAI;gBACrB,CAAA;YACA,6DAA6D;YAC7D0B,YAAYjN,MAAMkN,oBAAoB,CAAC1H,GAAG,CAAC,CAACkD,SAAY,CAAA;oBACtDZ,OAAOY;oBACPkD,OAAO5L,MAAMmN,kBAAkB,CAACzE,OAAO;gBACzC,CAAA;WACGpD,OAAOC,WAAW,CACnB4B,aAAakB,MAAM,CAChBV,MAAM,CAAC,CAACyF;gBAA+CA;mBAAhCA,WAAWC,IAAI,KAAK,cAAYD,sBAAAA,WAAWR,OAAO,qBAAlBQ,oBAAoBhE,MAAM;WACjF5D,GAAG,CAAC,CAAC4H;gBAEHA;mBAFkB;gBACnB/M,uBAAuB+M,WAAWE,GAAG;gBACrC,EAACF,sBAAAA,WAAWR,OAAO,YAAlBQ,sBAAsB,EAAE,EAAE5H,GAAG,CAAC,CAACqH,SAAY,CAAA;wBAAE/E,OAAO+E;wBAAQjB,OAAOiB;oBAAO,CAAA;aAC5E;cAGP;QAACZ;QAAK7B,QAAQwC,OAAO;QAAEF,UAAUE,OAAO;QAAEzF,aAAakB,MAAM;KAAC;IAGhE,0EAA0E,GAC1E,MAAMkF,iBAAiB7J,QACrB,IAAM;eACDM,mBAAmB;gBACpBwJ,YAAYpD,QAAQoD,UAAU;gBAC9B,yDAAyD;gBACzDC,UAAUvG,QAAQA,MAAMuG,QAAQ,GAAGzB;YACrC;YACA,0DAA0D;eACvD3H,mBAAmB8C,aAAakB,MAAM;SAC1C,EACD;QAAC+B,QAAQoD,UAAU;QAAEtG;QAAOC,aAAakB,MAAM;KAAC;IAElD;;;;;;GAMC,GACD;;;;;GAKC,GACD,MAAMqF,gBAAgBhK,QACpB,IAAMkB,iBAAiB2I,gBAAgBnF,cAAcuE,eAAe7G,gBACpE;QAACyH;QAAgBnF;QAAcuE;QAAe7G;KAAc;IAE9D,MAAM6H,aAAa7I,iBAAiB;QAClC8I,SAAS3F,MAAMK,KAAK,CAACN,OAAO;QAC5B6F,UAAU5F,MAAM6F,UAAU;QAC1BC,cAAczI,OAAO0I,IAAI,CAACrB;QAC1BsB,QAAQ;YAAEC,OAAO9B;YAAayB,UAAUxB;QAAe;IACzD;IACA,MAAM8B,OAAO5J,eAAe0D,OAAOyF,eAAerI;IAElD;;;;;;;;;;;;;GAaC,GACD,MAAM+I,iBAAiB1K,QACrB,IAAMnD,sBAAsB0H,MAAMK,KAAK,CAACN,OAAO,GAC/C;QAACC,MAAMK,KAAK,CAACN,OAAO;KAAC;IAEvB,MAAM,CAACqG,aAAaC,eAAe,GAAG1K,SAAwB;IAC9D,MAAM2K,oBAAoB/K,YAAY;QACpC,MAAM+H,OAAO,CAAC8C,sBAAAA,cAAe,IAAIG,IAAI,GAAGC,KAAK,CAAC,GAAG;QACjD,mEAAmE;QACnE,mEAAmE;QACnE,IAAI,CAAClD,QAAQ,CAAC6C,kBAAkB,CAACxH,WAAW;QAC5C0H,eAAe;QACf,IAAI;gBAKQF,sBACGA;YALb,MAAMrL,OACJC,WAAWoE,WAAWR,SAAS,CAAC,EAAE,EAAEA,SAAS,CAAC,EAAE,EAAE,oBAClD;gBACE2E;gBACAM,IAAI,GAAEuC,uBAAAA,eAAevC,IAAI,YAAnBuC,uBAAuB,EAAE;gBAC/BtC,OAAO,GAAEsC,0BAAAA,eAAetC,OAAO,YAAtBsC,0BAA0B,EAAE;eAgBlC5N,uBAAuB;gBAACC;aAAgB;gBAC3CiO,WAAW,IAAIC;;YAGnBtH,gBACE,CAAC,SAAS,EAAEkE,KAAK,uCAAuC,CAAC,EACzD;gBAAEqD,SAAS;gBAAWC,SAAS;YAAM;QAEzC,EAAE,OAAOC,OAAO;YACdC,QAAQD,KAAK,CAACA;YACdzH,gBAAgB,yBAAyB;gBACvCuH,SAAS;gBACTI,gBAAgB;YAClB;QACF;IACF,GAAG;QAACX;QAAaD;QAAgBhH;QAAWR;QAAWS;KAAgB;IAEvE,wFAAwF,GACxF,MAAM,CAAC4H,aAAaC,eAAe,GAAGtL,SAAmB,EAAE;IAC3D;;;;;;;;;;6CAU2C,GAC3C,2EAA2E;IAC3E,yEAAyE;IACzE,oEAAoE;IACpE,MAAMuL,SAASjL,UAAUwC,SAAS0I,YAAY;IAC9C,MAAM,CAACC,YAAYC,cAAc,GAAG1L,SAAS;IAC7C,MAAM,CAAC2L,aAAaC,eAAe,GAAG5L,SAAwB;IAC9D,MAAM6L,eAAejM,YACnB,OAAOkM;QACLJ,cAAc;QACdE,eAAe;QACf,IAAI;gBAyBuBG;YAxBzB,MAAM,EAAEC,QAAQ,EAAED,OAAO,EAAE,GAAG,MAAMR,OAAO,mBAAmB;gBAC5DU,OAAOH,OAAOG,KAAK;eACfH,OAAOnE,IAAI,GAAG;gBAAEA,MAAMmE,OAAOnE,IAAI;YAAC,IAAI,CAAC,GACvCmE,OAAOI,KAAK,GAAG;gBAAEA,OAAOJ,OAAOI,KAAK;YAAC,IAAI,CAAC,GAC1CJ,OAAOK,QAAQ,GAAG;gBAAEA,UAAUL,OAAOK,QAAQ;YAAC,IAAI,CAAC,GACnDL,OAAOM,WAAW,GAAG;gBAAEA,aAAaN,OAAOM,WAAW;YAAC,IAAI,CAAC,GAC5DN,OAAOO,SAAS,GAAG;gBAAEA,WAAWP,OAAOO,SAAS;YAAC,IAAI,CAAC,GACtDP,OAAOQ,OAAO,GAAG;gBAAEA,SAASR,OAAOQ,OAAO;YAAC,IAAI,CAAC,GAChDR,OAAOnF,QAAQ,GAAG;gBAAEA,UAAUmF,OAAOnF,QAAQ;YAAC,IAAI,CAAC,GACnDmF,OAAOS,cAAc,GACrB;gBAAEA,gBAAgBT,OAAOS,cAAc;YAAC,IACxC,CAAC,GACDT,OAAO7D,IAAI,CAACzC,MAAM,GAAG;gBAAEyC,MAAM6D,OAAO7D,IAAI;YAAC,IAAI,CAAC;gBAClDuE,kBAAkBV,OAAOU,gBAAgB;;YAE3C,IAAI,CAACR,SAASS,EAAE,EAAE;oBAKPV;gBAJT,gEAAgE;gBAChE,gEAAgE;gBAChE,aAAa;gBACbH,eACEjG,QAAOoG,iBAAAA,OAAO,CAAC,QAAQ,YAAhBA,iBAAoB;gBAE7B;YACF;YACA,MAAMW,YAAY/G,QAAOoG,qBAAAA,OAAO,CAAC,YAAY,YAApBA,qBAAwB;YACjD,gEAAgE;YAChE,+DAA+D;YAC/D,gEAAgE;YAChE,2CAA2C;YAC3CtI,gBACEsI,OAAO,CAAC,UAAU,GACd,kBACA,qEACJ;gBAAEf,SAAS;gBAAWC,SAAS;YAAM;YAEvCjF,cAAc;YACd,IAAI0G,WAAWjK,OAAOkK,IAAI,CAACnK,OAAOkD,OAAO,CAACgH;QAC5C,EAAE,OAAOxB,OAAO;YACdC,QAAQD,KAAK,CAACA;YACdU,eAAe;QACjB,SAAU;YACRF,cAAc;QAChB;IACF,GACA;QAACH;QAAQ9H;QAAiBhB;QAAQD;KAAO;IAG3C;;;;;GAKC,GACD,MAAMoK,aAAgC9M,QACpC,IAAO,CAAA;YAAE+M,YAAYrG,QAAQsG,WAAW;YAAEvJ,cAAcA,aAAakB,MAAM;QAAC,CAAA,GAC5E;QAAC+B,QAAQsG,WAAW;QAAEvJ,aAAakB,MAAM;KAAC;IAE5C,MAAMsI,eAAenN,YAAY;QAC/B9B,iBAAiB,gBAAgB,YAAYD,YAAY+K,SAASgE;IACpE,GAAG;QAAChE;QAASgE;KAAW;IAExB,qBACE;;0BACE,KAAC5O;gBACCgP,QAAQ;gBACRC,MAAMnQ,eAAe,YAAY;oBAAEoQ,QAAQ;gBAAqB;gBAChEC,cAAc;gBACdC,cAAc;0BAEd,cAAA,MAACpO;oBAAMqO,SAAS;;sCACd,MAACrO;4BACCsO,WAAU;4BACVD,SAAS;4BACTE,IAAI;gCAAEC,YAAY;gCAAUC,UAAU;gCAAQC,QAAQ;4BAAE;;8CAExD,KAACxO;oCAAW8L,SAAQ;oCAAQ2C,OAAM;oCAAiBJ,IAAI;wCAAEK,MAAM;oCAAE;8CAI9D,GAAG/G,aAAagH,cAAc,GAAG,YAAY,EAC5C3G,OAAO4G,QAAQ,CAAC/G,MAAMgH,QAAQ,IAC1B,GAAGhH,MAAMiH,IAAI,CAACH,cAAc,GAAG,IAAI,EAAE9G,MAAMgH,QAAQ,CAACF,cAAc,GAAG,YAAY,CAAC,GAClF,GAAG9G,MAAMiH,IAAI,CAACH,cAAc,GAAG,YAAY,CAAC,EAChD;;gCAEHxK,8BACC,KAAClF;oCAAoBiE,QAAQA;oCAAQC,KAAKA;mDAE1C,KAACjB;8CAAsB;;8CAEzB,KAACzC;oCACCsP,MAAK;oCACLC,SAASnB;oCACToB,UAAU,CAACvF,QAAQpD,MAAM;8CAExB;;gCAMFnC,8BACC,KAAC1E;oCACCsP,MAAK;oCACLjD,SAAQ;oCACR2C,OAAM;oCACNQ,UAAU,CAACnL;oCACXkL,SAAS;wCACPtC,eAAe;wCACf5F,cAAc;oCAChB;8CAEC;mDAGH,KAAC5E;oCAAqB4J,SAAQ;oCAAY2C,OAAM;8CAC7C;;;;wBAINtK,gBAAgB,qBACf,KAAChC;sCACE,kEACC,+DACA,+DACA;;sCAUN,MAACrC;4BAAMqO,SAAS;;8CACd,KAAClM;oCACCiN,YAAY/J;oCACZgK,UAAS;oCACTC,aAAa,CAACjL;oCACdkL,SAAS1G;oCACT2G,iBACEhE,kBAAkBxH,YAAY,IAAM0H,eAAe,MAAM;;8CAG7D,KAAC3J;oCACC0N,QAAQjK;oCACRkK,SAASxM;oCACT8H,SAAS3F,MAAMK,KAAK,CAACN,OAAO;oCAC5B6F,UAAU5F,MAAM6F,UAAU;oCAC1BlB,SAASD;oCACT4F,WAAW,YAAExM,eAAAA,KAAKwC,MAAM,qBAAXxC,aAAaN,KAAK,oBAAI;;;;wBAGtC,CAACkF,MAAM6H,OAAO,iBACb,KAAClQ;4BAAMmQ,UAAS;sCACb,4DACC,aACC5H,CAAAA,eAAe,IACZ,CAAC,EAAE,EAAEA,aAAa4G,cAAc,GAAG,eAAe,CAAC,GACnD,EAAC,IACL;6BAEF9G,MAAM+H,cAAc,GAAG,KACzB/H,MAAMgI,cAAc,IAAI,QACxB,6DAA6D;wBAC7D,+DAA+D;wBAC/D,+DAA+D;wBAC/D,8DAA8D;wBAC9DnM,kCACA,KAAClE;4BAAMmQ,UAAS;sCACbnM,iBAEG,wDAAwD;4BACxD,2DAA2D;4BAC3D,4DAA4D;4BAC5D,2DAA2D;4BAC3D,4DAA4D;4BAC5D,uBAAuB;4BACvB,GAAGqE,MAAM+H,cAAc,CAACjB,cAAc,GAAG,kBAAkB,CAAC,GAC5D,CAAC,qBAAqB,EAAE9G,MAAMgH,QAAQ,CAACF,cAAc,GAAG,GAAG,CAAC,GAC5D,CAAC,YAAY,EAAE9G,MAAMgI,cAAc,CAAC,iBAAiB,CAAC,GACtD,CAAC,GAAG,EAAEhI,MAAMiI,iBAAiB,CAACC,OAAO,CAAC,GAAG,mBAAmB,CAAC,GAC7D,8BACA,uDAEA,8CAA8C;4BAC9C,6DAA6D;4BAC7D,4DAA4D;4BAC5D,eAAe;4BACf,EAAE;4BACF,4DAA4D;4BAC5D,wDAAwD;4BACxD,4DAA4D;4BAC5D,yDAAyD;4BACzD,aAAa;4BACb,GAAGlI,MAAM+H,cAAc,CAACjB,cAAc,GAAG,kBAAkB,CAAC,GAC5D,CAAC,qBAAqB,EAAE9G,MAAMgH,QAAQ,CAACF,cAAc,GAAG,GAAG,CAAC,GAC5D,8CACA,CAAC,KAAK,EAAE9G,MAAMgI,cAAc,CAAC,yBAAyB,CAAC,GACvD;6BAEJ9H,eAAe,kBACjB,KAACvI;4BAAMmQ,UAAS;sCACb,GAAG5H,aAAa4G,cAAc,GAAG,gBAAgB,EAChD5G,iBAAiB,IAAI,SAAS,SAC/B,mDAAmD,CAAC;6BAErD;wBAOHG,uBAAuB,kBACtB,KAAC1I;4BAAMmQ,UAAS;sCACb,GAAGzH,qBAAqByG,cAAc,GAAG,OAAO,EAC/CzG,yBAAyB,IAAI,KAAK,IACnC,wCAAwC,EACvCtF,uBAAuB,CAACuF,sBAAsB,GAC1C,CAAC,yBAAyB,EAAEvF,uBAAuB,CAACuF,sBAAsB,EAAE,GAC5E,GACL,4DAA4D,CAAC;6BAE9D;wBACHzC,UAAUG,mBAAmB,wBAC5B,KAACrG;4BAAMmQ,UAAS;sCACb,+DACC,4DACA,gEACA;6BAEFhJ,SAASL,MAAM,KAAK,KAAKzB,uBAC3B,KAAC7E;4BAAW8L,SAAQ;4BAAQ2C,OAAM;sCAC/B5I,mBAAmB,YAChB,aACA;6BAEJc,SAASL,MAAM,KAAK,kBACtB,KAACvH;4BACC+J,OAAOjD,mBAAmB,YAAY,sBAAsB;4BAC5DmK,aACEnK,mBAAmB,YACfqD,YACA/E,gBACE,wIACA;4BAER8L,QACEpK,mBAAmB,YAAYqD,YAAY,CAAC/E,8BAC1C,MAACrE;gCAAMsO,WAAU;gCAAMD,SAAS;;kDAC9B,KAACjM;wCAAqB4J,SAAQ;wCAAY2C,OAAM;kDAC7C;;kDAEH,KAACvM;kDAAsB;;;+CAGzB,MAACpC;gCAAMsO,WAAU;gCAAMD,SAAS;;kDAC9B,KAAC1O;wCACCsP,MAAK;wCACLjD,SAAQ;wCACR2C,OAAM;wCACNQ,UAAU,CAACnL;wCACXkL,SAAS;4CACPtC,eAAe;4CACf5F,cAAc;wCAChB;kDAEC;;kDAEH,KAAC7H;wCAAoBiE,QAAQA;wCAAQC,KAAKA;;;;2CAMlD;;gCAKGiG,cAAc9C,MAAM,iBACnB,KAACtG;oCAAW8L,SAAQ;oCAAU2C,OAAM;8CACjC,AAACrF,CAAAA,cAAc9C,MAAM,KAAK,IACvB,IAAGtD,sCAAAA,aAAa,CAACoG,aAAa,CAAC,EAAE,CAACzG,KAAK,CAAC,YAArCK,sCAAyCoG,aAAa,CAAC,EAAE,CAACzG,KAAK,CAAC,SAAS,CAAC,GAC7E,GAAGyG,cAAc9C,MAAM,CAAC,uBAAuB,CAAC,AAAD,IACjD,kDACCrD,CAAAA,KAAKwC,MAAM,GACR,CAAC,UAAU,GAAEzC,mCAAAA,aAAa,CAACC,KAAKwC,MAAM,CAAC9C,KAAK,CAAC,YAAhCK,mCAAoCC,KAAKwC,MAAM,CAAC9C,KAAK,CAAC,8BAA8B,CAAC,GAClG,GAAE,IACN;qCAEF;8CACJ,KAAC3D;oCAAgBkE,QAAQA;oCAAQC,KAAKA;oCAAKU,OAAOC;oCAAWE,cAAcA;oCAAckM,MAAMxG;oCAASyG,UAAUhE;oCAAaiE,kBAAkBhE;oCAAgBiE,KAAK3C;oCAAY0B,aAAa,CAACjL;;8CAChM,KAACxC;oCAAuBqD,OAAOqG,KAAKiF,WAAW;8CAC7C,cAAA,KAACvS;wCACCmS,MAAMxG;wCACN6G,SAASlF,KAAKkF,OAAO;wCACrBC,OAAO9O;wCACP+O,YAAY;4CAAEN,UAAUhE;4CAAapB,UAAUqB;wCAAe;wCAC9DsE,QAAQ,CAAC7H,KAAOtF,OAAOkK,IAAI,CAACnK,OAAOkD,OAAO,CAACqC;wCAC3C;;;;;;;;mBAQC,GACD8H,YAAW;wCACXC,aAAa/F,WAAW+F,WAAW;wCACnCC,qBAAqBhG,WAAWgG,mBAAmB;wCACnDC,WAAW;wCACXC,uBAAuB,IAAM/J,gBAAgB;wCAC7CgK,uBAAuB3F,KAAK2F,qBAAqB;wCACjDC,+BAA+B5F,KAAK4F,6BAA6B;wCACjEC,WAAW7F,KAAK6F,SAAS;wCACzBC,mBAAmB9F,KAAK8F,iBAAiB;;;;;sCAKjD,KAACpQ;4BAAmBmC,QAAQA;4BAAQC,KAAKA;4BAAKE,UAAUP,MAAMO,QAAQ;;;;;0BAO1E,MAAC3D;gBACC0R,MAAM7F,gBAAgB;gBACtB8F,SAAS,IAAM7F,eAAe;gBAC9B8F,UAAS;gBACTC,SAAS;;kCAET,KAAC1R;kCAAa;;kCACd,KAACD;kCACC,cAAA,MAACE;4BAAMqO,SAAS;4BAAGE,IAAI;gCAAEmD,IAAI;4BAAE;;8CAC7B,KAACxR;oCAAW8L,SAAQ;oCAAQ2C,OAAM;8CAC/B;;8CAEH,KAAC1O;oCACC0R,SAAS;oCACT1C,MAAK;oCACLjG,OAAM;oCACN9D,KAAK,EAAEuG,sBAAAA,cAAe;oCACtBR,UAAU,CAAC2G,QAAUlG,eAAekG,MAAMC,MAAM,CAAC3M,KAAK,CAAC2G,KAAK,CAAC,GAAG;oCAChEiG,WAAW,CAACF;wCACV,IAAIA,MAAMlH,GAAG,KAAK,SAAS;4CACzBkH,MAAMG,cAAc;4CACpB,KAAKpG;wCACP;oCACF;oCACAqG,WAAW;wCAAEC,WAAW;4CAAEC,WAAW;wCAAG;oCAAE;;;;;kCAIhD,MAACrS;;0CACC,KAACF;gCAAOuP,SAAS,IAAMxD,eAAe;0CAAQ;;0CAC9C,KAAC/L;gCACCqM,SAAQ;gCACRmD,UAAU,CAAC,CAAC1D,sBAAAA,cAAe,IAAIG,IAAI,MAAM,CAAC5H;gCAC1CkL,SAAS,IAAM,KAAKvD;0CAEnB;;;;;;YAQN5E,2BACC,KAAC1F;gBACCiQ,IAAI;gBACJC,SAAS,IAAMvK,cAAc;gBAC7B5D,QAAQA;gBACRC,KAAKA;gBACL8O,MAAM1F;gBACNP,OAAOS;gBACPyF,QAAQ5K,QAAQwC,OAAO;gBACvBqI,aAAa7K,QAAQ3D,KAAK;gBAC1ByO,UAAU,CAACxF,SAAW,KAAKD,aAAaC;iBAExC;;;AAGV;AACA/J,sBAAsBwP,WAAW,GAAG;AAEpC,eAAexP,sBAAqB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/contacts-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport {\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_LIFECYCLE_STAGES,\n CONTACT_SOURCE_LABELS,\n CRM_CONTACT_VIEW_FIELDS,\n type CrmViewFilterClause,\n crmContactCustomColumn,\n crmViewFiltersFromSegment,\n crmViewSegmentFilters,\n newResourceScopeFields,\n ORG_SCOPE_TOKEN,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { type ConsolePluginPageProps } from '@aglyn/aglyn'\nimport {\n hiddenFilterVisibility,\n matchListFilter,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n CONTACT_LIST_FILTER_FIELDS,\n CONTACT_LIST_FILTER_HEADERS,\n contactCustomFilterFields,\n contactCustomFilterHeaders,\n} from '../constants/contact-filters'\nimport {\n type ContactRecord,\n contactPrimaryGroup,\n contactRecordFromDoc,\n} from '../model/contact-record'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { crmVisibleToClause, useCrmScope } from '../hooks/use-crm-scope'\nimport { contactsListSeed } from '../model/contacts-list-seed'\nimport { crmRoutes } from '../model/crm-routes'\nimport {\n type ContactCsvOptions,\n contactsCsv,\n downloadTextFile,\n} from '../model/contacts-csv'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport ContactsBulkBar from './contacts-bulk-bar'\nimport { ContactImportButton } from './contact-import-drawer'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n listFilterPlan,\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteDoc,\n doc,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport RecentActivityFeed from './recent-activity-feed'\nimport {\n CONTACT_FILTER_COLUMNS,\n CONTACT_OPTIONAL_COLUMNS,\n contactListColumns,\n} from './contact-list-columns'\nimport NewContactDrawer, { type NewContactValues } from './new-contact-drawer'\nimport { useCrmApi } from './use-crm-api'\nimport { useOrgMembers } from './use-org-members'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { customFieldColumns } from './contact-custom-columns'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport { useCompanyOptions } from './company-picker'\nimport CrmFilterBar from './crm-filter-bar'\nimport {\n type CrmFilterOption,\n crmFilterColumns,\n crmRowMatchesSearch,\n} from '../model/crm-grid-filter'\nimport { useCrmGridFilter } from '../hooks/use-crm-grid-filter'\nimport CrmViewsControl, { type CrmViewPreset } from './crm-views-control'\nimport { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from './crm-suite-lock'\n\n/**\n * The shared labels, under the name this file has always called them.\n *\n * The map lives beside the `ContactSource` union so the dynamic-list rule\n * editor and this filter cannot disagree about what `order` is called.\n */\nconst SOURCE_LABELS = CONTACT_SOURCE_LABELS\n\n/** What the grid's quick search reads on a contact: who they are, and their tags. */\nconst CONTACT_SEARCH_FIELDS = ['name', 'email', 'phone', 'company', 'tags'] as const\n\n/**\n * What the list keeps out of sight until a view says otherwise: the\n * filter-only columns, which never show, and the optional columns\n * (AGL-2616), which start hidden and come back through the column menu or\n * a saved view's column list.\n */\nconst HIDDEN_COLUMNS: Readonly<Record<string, boolean>> = {\n ...hiddenFilterVisibility(CONTACT_LIST_FILTER_FIELDS, CONTACT_FILTER_COLUMNS),\n ...Object.fromEntries(CONTACT_OPTIONAL_COLUMNS.map((field) => [field, false])),\n}\n\n/**\n * Why a refund found no contact to record itself against (AGL-2329).\n *\n * The three reasons `recordContactRefund` distinguishes, in the operator's\n * language rather than the writer's enum. They are not interchangeable: two\n * of them are things the merchant can act on and one is a deletion nothing\n * should undo, and collapsing them into \"unmatched\" would turn an actionable\n * fact into a statistic.\n */\nconst UNMATCHED_REFUND_REASON: Record<string, string> = {\n 'no-email': 'the order carried no email address',\n 'no-contact': 'no contact record matched the buyer',\n 'contact-deleted': 'the contact was deleted between the sale and the refund',\n}\n\n/**\n * Contacts CRM (AGL-198): the unified people list fed by AGL-197's\n * ingestion — search, source badges, a profile drawer with the\n * interaction timeline plus tags/notes editing, and CSV export. Part of\n * the CRM, included from Starter (AGL-2851); the contactsPerHost band\n * counts the records in it.\n */\n/**\n * Contacts CRM (AGL-109 → AGL-395): the unified contacts list, segments,\n * and profile drawer, owned by the contacts plugin and rendered by the\n * shell's generic plugin route. The shell applies the `release_crm`\n * gate (via the nav tab) and passes the resolved `org` doc for the\n * `contactsPerHost` quota check.\n *\n * Since AGL-2595 this is the `people` SECTION of the Contacts hub rather\n * than the whole page: `crm-console-page.tsx` routes to it, and the\n * companies, deals, tasks, reports and fields sections sit beside it. The\n * body is the v1 page unchanged, and it takes the shell's full prop bag so\n * nothing the v1 page read has to be threaded through the hub by name.\n */\nexport function ContactsPeopleSection(props: ConsolePluginPageProps) {\n const { hostId, org, releaseFlag, basePath } = props\n /*\n * A row is a LINK now (AGL-2596): opening a person navigates to their own\n * page rather than a drawer over the list, so the address can be pasted\n * and every other CRM record can point at it. `basePath` is what the hub\n * hands every section; the empty fallback only exists for a direct mount\n * without the shell, where there is nowhere to navigate to anyway.\n */\n const routes = crmRoutes(basePath ?? '')\n const router = useRouter()\n // Whether the audience overage on this page is actually INVOICED\n // (AGL-1662), and whether that question has been answered yet.\n //\n // AGL-1604 stopped the usage cron putting `contactsOverageUsd` into\n // `billedCents` while `release_crm` is off for the org; `db5ecdf2b`\n // taught the console billing page's caption the same thing. This page's own\n // alert still quoted the dollar figure with no flag check — and this is the\n // surface a staff member reaches with the flag OFF, because the shell's\n // `FeatureGate` admits them on `visible` (`released || isStaff`). Support\n // then reads that number back to a customer whose invoice will not carry it.\n //\n // The shell resolves this from `released`, never `visible`: staff opening a\n // page does not put a line on the customer's bill.\n //\n // Both default to the WITHHELD answer when the prop is absent, which only\n // happens in a direct mount — the shell always supplies it for a surface\n // with a nav-tab flag. A caller that has not resolved the verdict has not\n // earned the right to quote a charge.\n const contactsBilled = releaseFlag?.released ?? false\n const releaseFlagsReady = releaseFlag?.ready ?? false\n // Org-shared data root (AGL-237). Null until the org lookup settles\n // (AGL-1061), and for a host with no owning org — the pre-migration host\n // path this used to fall back to is gone (AGL-1050), so the CRM lists\n // nothing rather than listing somewhere else. Resolved by the one scope\n // hook (AGL-2614), which also answers for the organization-level mount\n // (AGL-2630): there `hostId` is null, the org comes from the hub's mount,\n // and the viewing group and the scope clause are both `null`.\n const crmScope = useCrmScope({ hostId, org })\n const { scope: dataScope, orgId, consentGroup, visibleTo: visibleToTokens } = crmScope\n /*\n * THE CRM (AGL-2788). This list is the CRM's, and the shell mounts no CRM\n * page for a plan without it (AGL-2851). The locks below — adding a person\n * by hand, importing a file, a saved view, the owner, stage and company of\n * a selection — draw only for an org whose plan lacks the CRM. The shell\n * mounts this page only once the org has settled, so the plan read here\n * is an answer.\n */\n const suiteIncluded = crmSuiteIncluded(org)\n // The org's site list, at the organization level only — what names the\n // sites in the \"Known by\" column (AGL-2630).\n const mount = useCrmOrgMount()\n // The org's custom fields, for the optional columns below (AGL-2601).\n const customFields = useContactFieldDefinitions(dataScope?.[1] ?? null)\n /*==========================================\n * THE CONTROLLER THIS PAGE IS SHOWING.\n *\n * A contact document is shared by every site in the org — one human who\n * touched two sites is one row, which is the dedupe the shared address book\n * exists for and the reason the org is billed once for them. Almost nothing\n * ON that row is shared: the notes, tags, timeline and lifetime value are\n * the holder's own business records, and while they lived at the top of the\n * document every site in an agency's account could read every client's.\n *\n * So the page resolves the group it is being viewed as — the sites declared\n * to be one sender, or this site alone — and reads that group's facet.\n * Pure, from the org document the shell already passed, so it costs no\n * read. `consentGroup` above is that group; at the ORGANIZATION level it\n * is `null`, and each row is flattened through the person's own primary\n * holder instead (`contactPrimaryGroup`), so an org-wide member reads the\n * profile the first capturing site keeps — and the \"Known by\" column says\n * which other sites hold one.\n *=========================================*/\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n\n /*\n * WHAT THE LIST WAS OPENED FOR (AGL-2612).\n *\n * Two surfaces address this list with a question rather than a record: a\n * form's own page (\"who came in through this form\") and an Inbox\n * submission row (\"open the person with this address\"). Both arrive as\n * query keys, parsed once into the same filter the grid's panel would\n * produce, so the seeded query IS a typed query and nothing here is a\n * second predicate.\n */\n const searchParams = useSearchParams()\n const seed = useMemo(() => contactsListSeed(searchParams), [searchParams])\n /*\n * The seed as CLAUSES, in front of everything else (AGL-2617).\n *\n * A view holds a list of clauses in the grammar the grid's panel speaks,\n * and the seed is two of them — the form's captures and the address, plus\n * the source the form link names. They lead every state the list works\n * from, whichever view is open, so the query serves the seed (it reaches\n * the whole collection, as it did before views) and a view narrows what\n * comes back. The controller keeps them out of what a view SAVES.\n */\n const seedFilters = useMemo<CrmViewFilterClause[]>(\n () => [\n ...(seed.filter ? [seed.filter] : []),\n ...(seed.source\n ? [{ field: CRM_CONTACT_VIEW_FIELDS.source, op: 'equals', value: seed.source }]\n : []),\n ],\n [seed],\n )\n const viewInitial = useMemo(() => ({ filters: seedFilters }), [seedFilters])\n const views = useCrmSavedView({\n section: 'contacts',\n hostId,\n org,\n basePath: basePath ?? '',\n initial: viewInitial,\n })\n /*\n * The grammar this list speaks: the declared fields, and one field per\n * custom contact field the org has defined — so a view can filter on\n * \"plan is enterprise\" the way it filters on a stage.\n */\n const filterFields = useMemo(\n () => [...CONTACT_LIST_FILTER_FIELDS, ...contactCustomFilterFields(customFields.active)],\n [customFields.active],\n )\n const filterHeaders = useMemo(\n () => ({\n ...CONTACT_LIST_FILTER_HEADERS,\n ...contactCustomFilterHeaders(customFields.active),\n }),\n [customFields.active],\n )\n /*\n * ONE clause on the query, the rest over the window.\n *\n * Firestore takes one array clause per query and composes the rest only\n * through indexes nobody built for every pair of columns, so the first\n * clause the translator can serve is the one that reaches every contact\n * and every other narrows the thousand the query returned. The caption\n * under the bar says which is which; the chips mark the served one.\n */\n const plan = useMemo(\n () => listFilterPlan(filterFields, views.state.filters),\n [filterFields, views.state.filters],\n )\n const filter = plan.served\n /**\n * The one filter that cannot carry the scope clause.\n *\n * `formIds` is an `array-contains` on the mirror, and Firestore takes one\n * array clause per query — so this listener drops its `visibleTo`\n * predicate for it, exactly as the company contacts card does for\n * `companyIds`, and the rules then admit the read to an org-wide member\n * only. A scoped member's listener errors instead of listing, and the\n * alert below says so rather than showing an empty form.\n */\n const byForm = filter?.field === 'formIds'\n const {\n data: contactDocs,\n status: contactsStatus,\n /**\n * The rows the profile drawer is seeded from are unconfirmed by the\n * server (AGL-1358). This payload is narrower than most sites in this\n * issue — `email`, `sources` and `interactions` are not in it — but the\n * two fields that are, `tags` and `notes`, are BOTH written on every\n * save and both come off the seed. Edit the notes against a cached read\n * and the tags go back with them, and tags are what\n * `contactMatchesSegment` runs on: a saved segment is a campaign\n * audience, so a rollback here silently changes who gets emailed.\n */\n fromCache: contactsFromCache,\n } = useFirestoreCollection<any>(\n () => {\n if (!dataScope) return null\n /*\n * ORDERED, and filtered by the QUERY (AGL-2501, AGL-2292).\n *\n * Two bugs shared this one line. `limit(1000)` with no `orderBy` returns\n * documents in ID order — contacts are created with `.add()` and\n * `createResourceUid()`, so that is a pseudo-random SAMPLE of a thousand,\n * and the client `.sort()` below made it look reliably newest-first. An\n * org with forty thousand contacts saw a thousand arbitrary ones,\n * convincingly sorted.\n *\n * The search then ran over that sample, so a name on the wrong side of\n * the cap answered \"no contacts match\" — the answer a search must never\n * give wrongly, on the list a merchant uses to find one person.\n *\n * The cap STAYS: nobody needs forty thousand rows streamed into a table,\n * and the head-count has been a server aggregate since AGL-1706. What\n * changes is that the thousand are now the newest thousand, and that a\n * filter reaches the whole collection before the cap applies.\n */\n const constraints = plan.constraints\n /*\n * SCOPED, and this is the leak it closes.\n *\n * The listener had no `where()` at all: `hostId` reached it only to\n * resolve which ORG owns the data, so every site in the account listed\n * every contact in the account. An agency's client opened Contacts and\n * read the other clients' customers.\n *\n * `array-contains-any` over the group's tokens is the same predicate\n * the rules evaluate with `hasAny`, so a filtered query is provable\n * per-document and an UNFILTERED one is now permission-denied rather\n * than quietly returning everything.\n */\n return query(\n collection(firestore, dataScope[0], dataScope[1], 'contacts'),\n // Dropped for the form mirror alone — see `byForm` — and absent at\n // the organization level, where the tokens are `null` (AGL-2630).\n ...(byForm ? [] : crmVisibleToClause(visibleToTokens)),\n ...(constraints ?? [orderBy('updatedAt', 'desc')]),\n limit(1000),\n )\n },\n [firestore, dataScope, plan.constraints, byForm, visibleToTokens],\n { idField: '$id' },\n )\n /*\n * OPENED FOR ONE ADDRESS: move straight on to the record (AGL-2612).\n *\n * The Inbox links here with an email because a contact's id is minted at\n * capture and nothing outside the CRM holds it; the list is the lookup.\n * Once the seeded `email equals` query has answered with exactly one row,\n * that row is the person and the list is a detour — `replace`, so Back\n * returns to the Inbox rather than to this redirect. Nothing else\n * navigates: no match leaves the filtered list on screen, which is the\n * honest answer for a submission whose contact the band dropped, and a\n * filter the reader has since changed is theirs.\n */\n const openedByEmail = useRef(false)\n useEffect(() => {\n if (openedByEmail.current || !seed.openEmail) return\n if (contactsStatus !== 'success') return\n if (filter?.field !== 'email' || filter.value !== seed.openEmail) return\n const only = (contactDocs ?? []).length === 1 ? contactDocs?.[0] : null\n if (!only) return\n openedByEmail.current = true\n router.replace(routes.contact(String(only.$id)))\n }, [seed.openEmail, contactsStatus, filter, contactDocs, router, routes])\n /**\n * Every row, flattened through THIS group's facet.\n *\n * One projection rather than a facet read at each of the nine places that\n * touch `tags`, `notes`, `name` or `interactions`: a surface that reads one\n * of them off the top of the document is a surface showing another\n * holder's records, and nine chances to forget is nine leaks.\n *\n * The canonical `name` is the fallback and the shared identity; a holder's\n * own override wins, so one business renaming a person cannot change what\n * an unrelated business sees.\n */\n const contacts: ContactRecord[] = useMemo(\n () =>\n (contactDocs ?? []).map((row) =>\n contactRecordFromDoc(\n row,\n consentGroup ?? contactPrimaryGroup(row, org as Record<string, unknown>),\n ),\n ),\n [contactDocs, consentGroup, org],\n )\n /*\n * The roster, for the Owner column — read only once a loaded row actually\n * carries an owner. A list of people nobody has assigned never pays for\n * the team list, and the create drawer asks for it in its own right.\n */\n const [createOpen, setCreateOpen] = useState(false)\n /*\n * The filter bar's pickers name people and companies, so the roster is\n * also read once the reader has reached for a filter, or is looking at a\n * view that names an owner or a company (AGL-2617).\n */\n const [filterOpened, setFilterOpened] = useState(false)\n const namesFilter = views.state.filters.some(\n (clause) =>\n clause.field === CRM_CONTACT_VIEW_FIELDS.owner ||\n clause.field === CRM_CONTACT_VIEW_FIELDS.company,\n )\n const members = useOrgMembers(orgId, {\n enabled:\n createOpen ||\n filterOpened ||\n namesFilter ||\n contacts.some((contact) => Boolean(contact.ownerUid)),\n })\n /**\n * The HEAD-COUNT, read as a server-side aggregate (AGL-1706).\n *\n * The listener above is `limit(1000)` and always will be — nobody needs\n * 40,000 rows streamed into a table. What it must not do is answer \"how\n * many contacts does this org have\", and it did: `contacts.length`\n * saturated at 1,000, which is *exactly* the smallest paid included band\n * (`starter`). So `overageContacts = max(0, used − included)` was 0 on\n * every stock plan and the alert below could not render at all.\n *\n * Worse than the dead alert, the same capped number fed the readout in the\n * toolbar. An org with 40,000 contacts on Pro read \"1,000 contacts ·\n * 10,000 included\" — a page whose job is telling a customer where they sit\n * in their band, telling them they have room they do not have. The console\n * billing page read the truth from `getCountFromServer` on this very\n * collection, so the two surfaces disagreed about the same org's audience.\n *\n * THE LIST AND THE COUNT ARE DIFFERENT QUESTIONS and now have different\n * answers: one aggregate read per mount, the same call\n * `billing-usage.component.tsx` already makes against the same path.\n *\n * The counting RULE is untouched — `checkCrmRecordsQuota` is an\n * entitlement input and the usage cron is what bills from it. Only this\n * page's input stopped being a saturated one.\n *\n * THE BAND IS THE RECORDS BAND (AGL-2611): contacts, companies and deals\n * together, so the alert below is fed the sum of three aggregates while\n * the readout beside the toolbar keeps the people count — a workspace\n * whose hundred records are ninety companies is told it holds ten\n * contacts and that its band is full, which are both true. The three\n * reads and the fallback rule live in `useCrmRecordsQuota`, shared with\n * the company and deal drawers so every door on this hub refuses on the\n * same number.\n */\n const records = useCrmRecordsQuota(dataScope, org, {\n contactsFallback: contacts.length,\n })\n // Pending or denied, the listener length stands in. It can only UNDERSTATE\n // (it is the same collection, capped), never overstate, so no alert this\n // number gates can fire on a count larger than the truth.\n const contactCount = records.contactsCount ?? contacts.length\n // Records bands (AGL-890): a plan with an overage rate meters past the\n // included count instead of blocking; a plan without one hard-bands\n // (quota.allowed = false).\n const quota = records.quota\n // Signups whose CRM record was dropped at a hard band (AGL-891), Free's\n // included: capture still fills that band on a plan without the CRM.\n // Written by upsert-contact, host-scoped.\n // Host-scoped, so absent at the organization level (AGL-2630): the\n // counter is a fact about one site's capture, and summing thirty of them\n // would be a figure about nothing in particular.\n const { data: droppedCounter } = useFirestoreDoc<any>(\n () => (hostId ? doc(firestore, 'hosts', hostId, 'counters', 'contactsDropped') : null),\n [firestore, hostId],\n )\n const droppedTotal = Number(droppedCounter?.['total'] ?? 0)\n\n /*==========================================\n * REFUNDS THAT REACHED NO CONTACT (AGL-2329).\n *\n * `recordContactRefund` refuses to create a contact for a refund — a\n * contact holding a refund and no purchase is a phantom record with\n * negative lifetime value — and increments\n * `hosts/{hostId}/counters/contactRefundsUnmatched` instead. Its own\n * comment says the shape mirrors `contactsDropped` \"so an operator…\", and\n * there the sentence stops: `contactsDropped` had this reader and the\n * refund counter had none, so a refund that reached no contact record\n * incremented a number nobody could see.\n *\n * It sits beside the dropped-signup alert because they are the same kind\n * of fact — something that happened to this host's CRM and left no row —\n * and an operator reconciling their contact list needs both or neither.\n *=========================================*/\n const { data: unmatchedRefundCounter } = useFirestoreDoc<any>(\n () =>\n hostId\n ? doc(firestore, 'hosts', hostId, 'counters', 'contactRefundsUnmatched')\n : null,\n [firestore, hostId],\n )\n const unmatchedRefundTotal = Number(unmatchedRefundCounter?.['total'] ?? 0)\n const unmatchedRefundReason = String(\n unmatchedRefundCounter?.['lastReason'] ?? '',\n )\n\n // Saved segments (AGL-199): reusable audience filters.\n const { data: segmentDocs } = useFirestoreCollection<any>(\n () =>\n dataScope\n ? query(\n collection(\n firestore,\n dataScope[0],\n dataScope[1],\n 'contactSegments',\n ),\n limit(50),\n )\n : null,\n [firestore, dataScope],\n { idField: '$id' },\n )\n const segments = useMemo(\n () =>\n [...(segmentDocs ?? [])].sort((a, b) =>\n String(a.name ?? '').localeCompare(String(b.name ?? '')),\n ),\n [segmentDocs],\n )\n /*\n * Every saved segment, offered in the views menu as a view (AGL-2617):\n * choosing one puts its tag and source clauses on the filter bar. A\n * segment can only have come FROM an org scope, so the delete is\n * unreachable without one in practice — but the scope is nullable\n * (AGL-1050) and an unguarded deref would be a crash rather than a no-op\n * if that ever stopped being true.\n */\n const segmentPresets = useMemo<CrmViewPreset[]>(\n () =>\n segments.map((segment: any) => ({\n id: String(segment.$id),\n label: String(segment.name ?? ''),\n filters: crmViewFiltersFromSegment({\n tags: segment.tags ?? [],\n sources: segment.sources ?? [],\n }),\n onRemove: dataScope\n ? () =>\n deleteDoc(\n doc(\n firestore,\n dataScope[0],\n dataScope[1],\n 'contactSegments',\n String(segment.$id),\n ),\n )\n : undefined,\n })),\n [segments, dataScope, firestore],\n )\n\n /*\n * THE WINDOW CLAUSES narrow in the browser, and the caption says so.\n *\n * An owner, a stage, a source, a company and a custom value all live on\n * the viewing group's FACET, and a facet path is per group, so none of\n * them is a field the query grammar can reach without an index per\n * group; a second tag clause, or \"any of these tags\", is an array clause\n * the scope predicate already occupies. So every clause the plan could\n * not serve refines the ordered window rather than the collection —\n * through the same matcher the grammar declares, so a chip and the rows\n * under it agree. Ordinary orgs load their whole list into the window\n * anyway; the caption below says what these narrow for the ones that do\n * not.\n */\n const uid = user?.uid ?? ''\n const windowClauses = plan.window\n // The quick search is the grid's box, answered here over the same window.\n const [searchWords, setSearchWords] = useState<string[]>([])\n const searchKey = searchWords.join(' ')\n const visible = useMemo(\n () =>\n windowClauses.length || searchWords.length\n ? contacts.filter(\n (contact) =>\n windowClauses.every((clause) =>\n matchListFilter(contact, filterFields, clause),\n ) && crmRowMatchesSearch(contact, CONTACT_SEARCH_FIELDS, searchWords),\n )\n : contacts,\n // `searchKey` stands for the words, which are a new array each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [contacts, windowClauses, filterFields, searchKey],\n )\n /*\n * The choices the filter bar picks from. The companies are read only once\n * the reader has reached for a filter or is looking at a view that names\n * one — a read a list nobody is narrowing should not pay for, the same\n * bargain the roster gets above. A stage and a source are fixed lists and\n * cost nothing; a custom `select` field offers its own options.\n */\n const companies = useCompanyOptions({\n hostId,\n org,\n enabled: filterOpened || namesFilter,\n })\n const filterOptions = useMemo<Record<string, readonly CrmFilterOption[]>>(\n () => ({\n [CRM_CONTACT_VIEW_FIELDS.owner]: [\n ...(uid ? [{ value: uid, label: 'Me' }] : []),\n ...members.options\n .filter((option) => option.uid !== uid)\n .map((option) => ({ value: option.uid, label: Aglyn.crmMemberPickerLabel(option) })),\n ],\n [CRM_CONTACT_VIEW_FIELDS.stage]: CONTACT_LIFECYCLE_STAGES.map((stage) => ({\n value: stage,\n label: CONTACT_LIFECYCLE_STAGE_LABELS[stage],\n })),\n [CRM_CONTACT_VIEW_FIELDS.source]: Object.entries(SOURCE_LABELS).map(\n ([value, label]) => ({ value, label }),\n ),\n [CRM_CONTACT_VIEW_FIELDS.company]: companies.options.map((company) => ({\n value: company.id,\n label: company.name,\n })),\n // The verdict on the address (AGL-3245): \"Email is bounced\".\n emailState: Aglyn.EMAIL_STATE_STATUSES.map((status) => ({\n value: status,\n label: Aglyn.EMAIL_STATE_LABELS[status],\n })),\n ...Object.fromEntries(\n customFields.active\n .filter((definition) => definition.type === 'select' && definition.options?.length)\n .map((definition) => [\n crmContactCustomColumn(definition.key),\n (definition.options ?? []).map((option) => ({ value: option, label: option })),\n ]),\n ),\n }),\n [uid, members.options, companies.options, customFields.active],\n )\n\n /* One row grammar, the console's (AGL-2501) — the same table everywhere. */\n const contactColumns = useMemo(\n () => [\n ...contactListColumns({\n memberName: members.memberName,\n // \"Known by\", at the organization level only (AGL-2630).\n siteName: mount ? mount.siteName : undefined,\n }),\n // The org's custom fields as optional columns (AGL-2601).\n ...customFieldColumns(customFields.active),\n ],\n [members.memberName, mount, customFields.active],\n )\n /*\n * The grid's column and sort models are the VIEW'S (AGL-2617). The\n * filter-only hidden columns — the declared fields that are not table\n * columns, never the custom ones, which are shown — stay hidden whatever\n * a view says, so a saved arrangement cannot unhide a strip of blank\n * cells.\n */\n /*\n * The grid's own Filters panel and quick search edit the view's clauses\n * (AGL-3313). The picked fields — owner, stage, source, company, email\n * verdict, a custom select — are select columns over the same choices the\n * chips name them by; the rest keep the operators the grammar declares.\n */\n const filterColumns = useMemo(\n () => crmFilterColumns(contactColumns, filterFields, filterOptions, filterHeaders),\n [contactColumns, filterFields, filterOptions, filterHeaders],\n )\n const gridFilter = useCrmGridFilter({\n clauses: views.state.filters,\n onChange: views.setFilters,\n selectFields: Object.keys(filterOptions),\n search: { words: searchWords, onChange: setSearchWords },\n })\n const grid = useCrmViewGrid(views, filterColumns, HIDDEN_COLUMNS)\n\n /*\n * A SEGMENT IS A VIEW'S TAG AND SOURCE CLAUSES, kept where a campaign\n * can reach them (AGL-2617).\n *\n * The segment stays: it is what a campaign audience and a dynamic list\n * resolve, and `contactMatchesSegment` is the one predicate both the\n * console and the sender read. What changed is where it comes from — the\n * filter bar's tag and source clauses, read back through\n * `crmViewSegmentFilters`, so \"Save as segment\" is offered exactly when\n * the working filters hold something a segment can hold. Each saved\n * segment is offered in the views menu as a preset, and applying one\n * puts its clauses on the bar, where they can be narrowed further and\n * saved as a view.\n */\n const segmentFilters = useMemo(\n () => crmViewSegmentFilters(views.state.filters),\n [views.state.filters],\n )\n const [segmentName, setSegmentName] = useState<string | null>(null)\n const handleSaveSegment = useCallback(async () => {\n const name = (segmentName ?? '').trim().slice(0, 60)\n // No org, no place to put it (AGL-1050). The button is disabled in\n // this state; the guard is here so the callback cannot outlive it.\n if (!name || !segmentFilters || !dataScope) return\n setSegmentName(null)\n try {\n await addDoc(\n collection(firestore, dataScope[0], dataScope[1], 'contactSegments'),\n {\n name,\n tags: segmentFilters.tags ?? [],\n sources: segmentFilters.sources ?? [],\n // `contactSegments` is in SCOPED_COLLECTIONS and was the one\n // member of it with no `array-contains-any` reader and no rules\n // `hasAny` — the rules gate it on `isOrgWideMember()` and\n // `campaign-send` checks it with `visibleToHost`, which passes on\n // a missing field. So nothing broke, which is precisely why this\n // went unnoticed (AGL-1478): a collection listed as scoped that\n // nothing enforces is a trap set for whoever wires up the first\n // scoped read, and they would inherit every segment ever saved\n // already broken. Stamped at creation instead, like its siblings.\n //\n // Org-wide, unconditionally: `useOrgDataScope` resolves to\n // `['orgs', orgId]` or to null, AGL-1061 having removed the\n // `hosts/{hostId}` branch after counting zero documents there.\n // A segment is a saved filter over org contacts, so it is exactly\n // as visible as they are.\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n createdAt: new Date(),\n },\n )\n enqueueSnackbar(\n `Segment \"${name}\" saved — usable as a campaign audience`,\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n }, [segmentName, segmentFilters, firestore, dataScope, enqueueSnackbar])\n\n /** The rows ticked for a bulk action (AGL-2603); the bar above the table acts on them. */\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n /*==========================================\n * ADDING ONE PERSON BY HAND (AGL-2596).\n *\n * Through the plugin's server route rather than a client write, because\n * creating a contact is the one act on this surface the rules cannot fully\n * judge: the dedupe against every holder's rows is a lookup the capturing\n * site may not read, and the audience band is a count the browser cannot\n * take. The route answers `{ contactId, created }`; a merge onto somebody\n * already in the address book is not an error, and the page says which\n * happened before it opens the record.\n *=========================================*/\n // The site the route resolves the org from and stamps the record with: the\n // mounted site, or at the organization level the one the drawer's picker\n // named (AGL-2630) — the drawer holds its submit until it is known.\n const crmApi = useCrmApi(crmScope.createHostId)\n const [createBusy, setCreateBusy] = useState(false)\n const [createError, setCreateError] = useState<string | null>(null)\n const handleCreate = useCallback(\n async (values: NewContactValues) => {\n setCreateBusy(true)\n setCreateError(null)\n try {\n const { response, payload } = await crmApi('contacts-create', {\n email: values.email,\n ...(values.name ? { name: values.name } : {}),\n ...(values.phone ? { phone: values.phone } : {}),\n ...(values.jobTitle ? { jobTitle: values.jobTitle } : {}),\n ...(values.companyName ? { companyName: values.companyName } : {}),\n ...(values.companyId ? { companyId: values.companyId } : {}),\n ...(values.address ? { address: values.address } : {}),\n ...(values.ownerUid ? { ownerUid: values.ownerUid } : {}),\n ...(values.lifecycleStage\n ? { lifecycleStage: values.lifecycleStage }\n : {}),\n ...(values.tags.length ? { tags: values.tags } : {}),\n marketingConsent: values.marketingConsent,\n })\n if (!response.ok) {\n // The route's own sentence, shown above the form unchanged: the\n // band refusal is the list's wording, and a field refusal names\n // the field.\n setCreateError(\n String(payload['error'] ?? 'The contact could not be added.'),\n )\n return\n }\n const contactId = String(payload['contactId'] ?? '')\n // The activity entry is the route's (AGL-2622): it verified the\n // caller and performed the write, so it is the one writer that\n // cannot record an add that did not happen — and a second entry\n // from here would put two rows on one act.\n enqueueSnackbar(\n payload['created']\n ? 'Contact added'\n : 'Already a contact — what you entered was merged into their record',\n { variant: 'success', persist: false },\n )\n setCreateOpen(false)\n if (contactId) router.push(routes.contact(contactId))\n } catch (error) {\n console.error(error)\n setCreateError('The contact could not be added.')\n } finally {\n setCreateBusy(false)\n }\n },\n [crmApi, enqueueSnackbar, router, routes],\n )\n\n /*\n * The file is `contactsCsv()`'s — every CRM column, the owner by address,\n * one column per custom field — and the same options reach the bulk bar,\n * so the table's export and the selection's are one file over two row\n * sets (AGL-2621).\n */\n const csvOptions: ContactCsvOptions = useMemo(\n () => ({ ownerEmail: members.memberEmail, customFields: customFields.active }),\n [members.memberEmail, customFields.active],\n )\n const handleExport = useCallback(() => {\n downloadTextFile('contacts.csv', 'text/csv', contactsCsv(visible, csvOptions))\n }, [visible, csvOptions])\n\n return (\n <>\n <CardDisplay\n header={'Contacts'}\n help={pluginDocsHelp('contacts', { anchor: '#the-contacts-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ flex: 1 }}>\n {/* The org's audience, not the page's row count (AGL-1706) —\n these two stopped being the same number the moment the\n listener grew a `limit(1000)`. */}\n {`${contactCount.toLocaleString()} contacts · ${\n Number.isFinite(quota.included)\n ? `${quota.used.toLocaleString()} of ${quota.included.toLocaleString()} CRM records`\n : `${quota.used.toLocaleString()} CRM records`\n }`}\n </Typography>\n {suiteIncluded ? (\n <ContactImportButton hostId={hostId} org={org} />\n ) : (\n <CrmSuiteLockedButton>{'Import CSV'}</CrmSuiteLockedButton>\n )}\n <Button\n size=\"small\"\n onClick={handleExport}\n disabled={!visible.length}\n >\n {'Export CSV'}\n </Button>\n {/* A button that opens a drawer — never a create form above the\n list. Disabled until the org has resolved, because the route\n resolves the org from the site and a click before that has\n nowhere to write. */}\n {suiteIncluded ? (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={!dataScope}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {'New contact'}\n </Button>\n ) : (\n <CrmSuiteLockedButton variant=\"contained\" color=\"primary\">\n {'New contact'}\n </CrmSuiteLockedButton>\n )}\n </Stack>\n {suiteIncluded ? null : (\n <CrmSuiteNotice>\n {'Contacts arrive here on their own from your forms, sign-ups, ' +\n 'orders and bookings. Adding one by hand, importing a CSV, ' +\n \"saved views, and a contact's owner, stage and company are \" +\n 'part of the CRM.'}\n </CrmSuiteNotice>\n )}\n {/*\n The view this list is showing, and the clauses narrowing it\n (AGL-2617). The control names the view and holds everything a\n reader does to one; the bar holds the clauses, the seed's\n included, with the one the query serves marked. Segments are\n offered in the control's menu and saved from it.\n */}\n <Stack spacing={1}>\n <CrmViewsControl\n controller={views}\n allLabel=\"All contacts\"\n suiteLocked={!suiteIncluded}\n presets={segmentPresets}\n onSaveAsSegment={\n segmentFilters && dataScope ? () => setSegmentName('') : null\n }\n />\n <CrmFilterBar\n fields={filterFields}\n headers={filterHeaders}\n clauses={views.state.filters}\n onChange={views.setFilters}\n options={filterOptions}\n servedField={plan.served?.field ?? null}\n />\n </Stack>\n {!quota.allowed ? (\n <Alert severity=\"warning\">\n {'CRM records limit reached — new visitors are no longer ' +\n 'captured' +\n (droppedTotal > 0\n ? ` (${droppedTotal.toLocaleString()} missed so far)`\n : '') +\n '. Upgrade in Billing to keep collecting.'}\n </Alert>\n ) : quota.overageRecords > 0 &&\n quota.overageRateUsd != null &&\n // No claim about money until the verdict that decides it has\n // settled (AGL-1662). Before Remote Config activation the flag\n // reads its registry default, which is no org's verdict, so an\n // ungated alert could assert the wrong wording for one paint.\n releaseFlagsReady ? (\n <Alert severity=\"info\">\n {contactsBilled\n ? // Same sentence as the billing page's caption, and the same\n // basis (AGL-2399): the count here is LIVE, the invoice\n // charges the last reading before the month closes, so the\n // dollar figure is a projection until the month ends. Staff\n // and customer must not read different sentences about the\n // same org's money — that applies to WHEN it is measured as\n // much as to how much.\n `${quota.overageRecords.toLocaleString()} CRM records over ` +\n `your plan's included ${quota.included.toLocaleString()} — ` +\n `metered at $${quota.overageRateUsd}/1,000 per month ` +\n `(≈$${quota.overageMonthlyUsd.toFixed(2)} if your list ends ` +\n 'the month at this size). ' +\n 'Upgrade in Billing for a larger included audience.'\n : // The wording `db5ecdf2b` put on the billing page, which is\n // itself the wording `1a2aed5cb` published to\n // `billing-and-plans/overview.md` (AGL-1601/1603). Staff and\n // customer must not read different sentences about the same\n // org's money.\n //\n // THE COUNT STAYS, THE TOTAL GOES: the head-count is real —\n // ingestion captured those records — and is not a claim\n // about money. The upgrade nudge goes with the total, since\n // it prompts a purchase premised on a charge that is not\n // happening.\n `${quota.overageRecords.toLocaleString()} CRM records over ` +\n `your plan's included ${quota.included.toLocaleString()} — ` +\n 'not billed while the CRM is unavailable. ' +\n `The $${quota.overageRateUsd}/1,000 rate applies once ` +\n 'Contacts opens.'}\n </Alert>\n ) : droppedTotal > 0 ? (\n <Alert severity=\"info\">\n {`${droppedTotal.toLocaleString()} earlier visitor${\n droppedTotal === 1 ? ' was' : 's were'\n } not captured while your CRM records band was full.`}\n </Alert>\n ) : null}\n {/*\n Unlike the band alert above, this one is NOT exclusive with the\n others: a host can be over its band AND have refunds that landed\n nowhere, and the two have different remedies. Chaining it into the\n same ternary would hide whichever fact came second.\n */}\n {unmatchedRefundTotal > 0 ? (\n <Alert severity=\"warning\">\n {`${unmatchedRefundTotal.toLocaleString()} refund${\n unmatchedRefundTotal === 1 ? '' : 's'\n } could not be recorded against a contact${\n UNMATCHED_REFUND_REASON[unmatchedRefundReason]\n ? ` — most recently because ${UNMATCHED_REFUND_REASON[unmatchedRefundReason]}`\n : ''\n }. The money moved; the customer's timeline does not show it.`}\n </Alert>\n ) : null}\n {byForm && contactsStatus === 'error' ? (\n <Alert severity=\"info\">\n {'The contacts this form captured could not be listed. Your ' +\n 'access is limited to specific sites, and a form filter ' +\n 'cannot be narrowed to them — an organization administrator ' +\n 'can see it.'}\n </Alert>\n ) : contacts.length === 0 && !views.state.filters.length ? (\n <EmptyStateComponent\n label={contactsStatus === 'loading' ? 'Loading contacts…' : 'No contacts yet'}\n description={\n contactsStatus === 'loading'\n ? undefined\n : suiteIncluded\n ? 'Form submissions, member sign-ups, orders and bookings become contacts on their own; add one by hand or bring a list in from a CSV.'\n : 'Form submissions, member sign-ups, orders and bookings become contacts on their own.'\n }\n action={\n contactsStatus === 'loading' ? undefined : !suiteIncluded ? (\n <Stack direction=\"row\" spacing={1}>\n <CrmSuiteLockedButton variant=\"contained\" color=\"primary\">\n {'New contact'}\n </CrmSuiteLockedButton>\n <CrmSuiteLockedButton>{'Import CSV'}</CrmSuiteLockedButton>\n </Stack>\n ) : (\n <Stack direction=\"row\" spacing={1}>\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={!dataScope}\n onClick={() => {\n setCreateError(null)\n setCreateOpen(true)\n }}\n >\n {'New contact'}\n </Button>\n <ContactImportButton hostId={hostId} org={org} />\n </Stack>\n )\n }\n />\n ) : (\n <>\n {/* Which clauses reached the whole collection and which\n narrowed the loaded window. Said out loud, because a control\n that narrows less than it looks like it does is the thing\n this page has been fixing. */}\n {windowClauses.length ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {(windowClauses.length === 1\n ? `${filterHeaders[windowClauses[0].field] ?? windowClauses[0].field} narrows `\n : `${windowClauses.length} of the filters narrow `) +\n 'the loaded window — the newest 1,000 contacts' +\n (plan.served\n ? ` matching ${filterHeaders[plan.served.field] ?? plan.served.field}, which reached every contact.`\n : '.') +\n ' Sorting reorders that window.'}\n </Typography>\n ) : null}\n <ContactsBulkBar hostId={hostId} org={org} scope={dataScope} consentGroup={consentGroup} rows={visible} selected={selectedIds} onSelectedChange={setSelectedIds} csv={csvOptions} suiteLocked={!suiteIncluded} />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={visible}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n onOpen={(id) => router.push(routes.contact(id))}\n /*\n * The grid's Filters panel and quick search edit the view's\n * clauses; the list answers them, onto the query where it\n * can and over the loaded window where it cannot, so the\n * grid must not filter again itself (AGL-3313). Opening the\n * panel is what reads the roster and the companies its\n * pickers name. Columns and sort are the view's, controlled\n * so a saved arrangement is what the grid shows.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n onPreferencePanelOpen={() => setFilterOpened(true)}\n // A filter that matched nothing is said inside the grid,\n // so the toolbar that set it stays to change it.\n noRowsLabel={\n contactsStatus === 'loading' ? 'Loading…' : 'No contacts match these filters'\n }\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={grid.onColumnVisibilityModelChange}\n sortModel={grid.sortModel}\n onSortModelChange={grid.onSortModelChange}\n />\n </CrmColumnOrderProvider>\n </>\n )}\n <RecentActivityFeed hostId={hostId} org={org} basePath={props.basePath} />\n </Stack>\n </CardDisplay>\n {/*\n Naming a segment is a dialog, never a form above the list. Open\n while the name is a string; `null` is closed.\n */}\n <Dialog\n open={segmentName !== null}\n onClose={() => setSegmentName(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{'Save as segment'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The tag and source filters, kept as a segment an email campaign or a dynamic list can send to. The other filters stay on this view only.'}\n </Typography>\n <TextField\n autoFocus\n size=\"small\"\n label=\"Segment name\"\n value={segmentName ?? ''}\n onChange={(event) => setSegmentName(event.target.value.slice(0, 60))}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n void handleSaveSegment()\n }\n }}\n slotProps={{ htmlInput: { maxLength: 60 } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setSegmentName(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n disabled={!(segmentName ?? '').trim() || !dataScope}\n onClick={() => void handleSaveSegment()}\n >\n {'Save segment'}\n </Button>\n </DialogActions>\n </Dialog>\n {/*\n Mounted only while open, so a page that never reaches for it never\n renders a drawer's chrome, and every opening starts from a blank form.\n */}\n {createOpen ? (\n <NewContactDrawer\n open\n onClose={() => setCreateOpen(false)}\n hostId={hostId}\n org={org}\n busy={createBusy}\n error={createError}\n owners={members.options}\n ownersReady={members.ready}\n onSubmit={(values) => void handleCreate(values)}\n />\n ) : null}\n </>\n )\n}\nContactsPeopleSection.displayName = 'ContactsPeopleSection'\n\nexport default ContactsPeopleSection\n"],"names":["Aglyn","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_LIFECYCLE_STAGES","CONTACT_SOURCE_LABELS","CRM_CONTACT_VIEW_FIELDS","crmContactCustomColumn","crmViewFiltersFromSegment","crmViewSegmentFilters","newResourceScopeFields","ORG_SCOPE_TOKEN","pluginDocsHelp","hiddenFilterVisibility","matchListFilter","ListTable","CONTACT_LIST_FILTER_FIELDS","CONTACT_LIST_FILTER_HEADERS","contactCustomFilterFields","contactCustomFilterHeaders","contactPrimaryGroup","contactRecordFromDoc","useCrmOrgMount","crmVisibleToClause","useCrmScope","contactsListSeed","crmRoutes","contactsCsv","downloadTextFile","useCrmRecordsQuota","CardDisplay","EmptyStateComponent","ContactsBulkBar","ContactImportButton","useSnackbar","listFilterPlan","useFirestore","useFirestoreCollection","useFirestoreDoc","useUser","Alert","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","addDoc","collection","deleteDoc","doc","limit","orderBy","query","useRouter","useSearchParams","useCallback","useEffect","useMemo","useRef","useState","RecentActivityFeed","CONTACT_FILTER_COLUMNS","CONTACT_OPTIONAL_COLUMNS","contactListColumns","NewContactDrawer","useCrmApi","useOrgMembers","useContactFieldDefinitions","customFieldColumns","useCrmSavedView","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","useCompanyOptions","CrmFilterBar","crmFilterColumns","crmRowMatchesSearch","useCrmGridFilter","CrmViewsControl","CrmSuiteLockedButton","CrmSuiteNotice","crmSuiteIncluded","SOURCE_LABELS","CONTACT_SEARCH_FIELDS","HIDDEN_COLUMNS","Object","fromEntries","map","field","UNMATCHED_REFUND_REASON","ContactsPeopleSection","props","records","filterHeaders","plan","hostId","org","releaseFlag","basePath","routes","router","contactsBilled","released","releaseFlagsReady","ready","crmScope","scope","dataScope","orgId","consentGroup","visibleTo","visibleToTokens","suiteIncluded","mount","customFields","firestore","enqueueSnackbar","data","user","searchParams","seed","seedFilters","filter","source","op","value","viewInitial","filters","views","section","initial","filterFields","active","state","served","byForm","contactDocs","status","contactsStatus","fromCache","contactsFromCache","constraints","idField","openedByEmail","current","openEmail","only","length","replace","contact","String","$id","contacts","row","createOpen","setCreateOpen","filterOpened","setFilterOpened","namesFilter","some","clause","owner","company","members","enabled","Boolean","ownerUid","contactsFallback","contactCount","contactsCount","quota","droppedCounter","droppedTotal","Number","unmatchedRefundCounter","unmatchedRefundTotal","unmatchedRefundReason","segmentDocs","segments","sort","a","b","name","localeCompare","segmentPresets","segment","id","label","tags","sources","onRemove","undefined","uid","windowClauses","window","searchWords","setSearchWords","searchKey","join","visible","every","companies","filterOptions","options","option","crmMemberPickerLabel","stage","entries","emailState","EMAIL_STATE_STATUSES","EMAIL_STATE_LABELS","definition","type","key","contactColumns","memberName","siteName","filterColumns","gridFilter","clauses","onChange","setFilters","selectFields","keys","search","words","grid","segmentFilters","segmentName","setSegmentName","handleSaveSegment","trim","slice","createdAt","Date","variant","persist","error","console","allowDuplicate","selectedIds","setSelectedIds","crmApi","createHostId","createBusy","setCreateBusy","createError","setCreateError","handleCreate","values","payload","response","email","phone","jobTitle","companyName","companyId","address","lifecycleStage","marketingConsent","ok","contactId","push","csvOptions","ownerEmail","memberEmail","handleExport","header","help","anchor","contentGutterX","contentGutterY","spacing","direction","sx","alignItems","flexWrap","rowGap","color","flex","toLocaleString","isFinite","included","used","size","onClick","disabled","controller","allLabel","suiteLocked","presets","onSaveAsSegment","fields","headers","servedField","allowed","severity","overageRecords","overageRateUsd","overageMonthlyUsd","toFixed","description","action","rows","selected","onSelectedChange","csv","columnOrder","columns","slots","selectable","onOpen","filterMode","filterModel","onFilterModelChange","quickFilter","onPreferencePanelOpen","noRowsLabel","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","open","onClose","maxWidth","fullWidth","pt","autoFocus","event","target","onKeyDown","preventDefault","slotProps","htmlInput","maxLength","busy","owners","ownersReady","onSubmit","displayName"],"mappings":"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"}
@@ -650,21 +650,7 @@ const DEAL_FILTER_OPTIONS = {
650
650
  })
651
651
  ]
652
652
  }),
653
- paged.status === 'success' && paged.rows.length === 0 && paged.page === 0 ? /*#__PURE__*/ _jsx(EmptyStateComponent, {
654
- label: statusFilter === 'all' ? 'No deals yet' : `No ${statusFilter} deals`,
655
- description: statusFilter === 'all' ? 'A deal is a sale in progress, moved across the pipeline as it advances.' : undefined,
656
- action: statusFilter === 'all' ? /*#__PURE__*/ _jsx(Button, {
657
- size: "small",
658
- variant: "contained",
659
- startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
660
- path: mdiPlus.path,
661
- size: 0.8
662
- }),
663
- disabled: !pipeline || !scope.orgId,
664
- onClick: ()=>setCreating(true),
665
- children: 'New deal'
666
- }) : undefined
667
- }) : /*#__PURE__*/ _jsxs(_Fragment, {
653
+ /*#__PURE__*/ _jsxs(_Fragment, {
668
654
  children: [
669
655
  /*#__PURE__*/ _jsx(DealsBulkBar, {
670
656
  hostId: hostId,
@@ -696,6 +682,21 @@ const DEAL_FILTER_OPTIONS = {
696
682
  filterModel: gridFilter.filterModel,
697
683
  onFilterModelChange: gridFilter.onFilterModelChange,
698
684
  quickFilter: false,
685
+ // An empty table is said inside the grid, so its toolbar
686
+ // stays to change the status that emptied it.
687
+ noRowsLabel: statusFilter === 'all' ? 'No deals yet' : `No ${statusFilter} deals`,
688
+ noRowsDescription: statusFilter === 'all' ? 'A deal is a sale in progress, moved across the pipeline as it advances.' : undefined,
689
+ noRowsAction: statusFilter === 'all' ? /*#__PURE__*/ _jsx(Button, {
690
+ size: "small",
691
+ variant: "contained",
692
+ startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
693
+ path: mdiPlus.path,
694
+ size: 0.8
695
+ }),
696
+ disabled: !pipeline || !scope.orgId,
697
+ onClick: ()=>setCreating(true),
698
+ children: 'New deal'
699
+ }) : undefined,
699
700
  // Columns and sort are the view's, controlled (AGL-2617).
700
701
  columnVisibilityModel: grid.columnVisibilityModel,
701
702
  onColumnVisibilityModelChange: grid.onColumnVisibilityModelChange,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deals-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ConsolePluginPageProps,\n type CrmDealStatus,\n dealStageById,\n filterByNextActivity,\n findOrgMember,\n isNoNextActivityClause,\n ORG_SCOPE_TOKEN,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport {\n mdiCogOutline,\n mdiPlus,\n mdiTableLarge,\n mdiViewColumnOutline,\n} from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CrmListActions, CrmListToolbar } from './crm-list-toolbar'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport {\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n CRM_NEXT_ACTIVITY_FILTER_HEADER,\n nextActivityColumn,\n} from './crm-next-activity-column'\nimport CrmFilterBar from './crm-filter-bar'\nimport { crmFilterColumns } from '../model/crm-grid-filter'\nimport { useCrmGridFilter } from '../hooks/use-crm-grid-filter'\nimport CrmViewsControl from './crm-views-control'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n Chip,\n CircularProgress,\n MenuItem,\n Stack,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useDealStageApi } from '../hooks/use-deal-stage-api'\nimport {\n BOARD_CLOSED_LIMIT,\n BOARD_OPEN_LIMIT,\n useDealsByStatus,\n usePagedDeals,\n} from '../hooks/use-deals'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { usePipeline } from '../hooks/use-pipeline'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport { type DealCsvOptions, dealsCsv } from '../model/deals-csv'\nimport {\n boardSummary,\n DEAL_STATUS_LABELS,\n type DealDoc,\n formatAmountByCurrency,\n formatMoney,\n} from '../model/deal-board-model'\nimport { customFieldColumns } from './contact-custom-columns'\nimport { DealBoard } from './deal-board'\nimport { DealEditDrawer } from './deal-edit-drawer'\nimport { DealImportButton } from './deal-import-drawer'\nimport DealsBulkBar from './deals-bulk-bar'\nimport { LostReasonDialog } from './lost-reason-dialog'\nimport { OwnerAvatar } from './owner-avatar'\nimport { PipelinesDialog } from './pipelines-dialog'\n\ntype View = 'board' | 'table'\ntype StatusFilter = CrmDealStatus | 'all'\n\n/** What the deals table's panel offers: the served status, and \"No next activity\". */\nconst DEAL_GRID_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals'] },\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n]\nconst DEAL_GRID_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n [CRM_NEXT_ACTIVITY_FILTER_FIELD.column]: CRM_NEXT_ACTIVITY_FILTER_HEADER,\n}\nconst DEAL_FILTER_OPTIONS = {\n status: (['open', 'won', 'lost'] as const).map((value) => ({\n value,\n label: DEAL_STATUS_LABELS[value],\n })),\n}\n\n\n/** What a pipeline made at the organization level is stamped with — the org's own token. */\nconst ORG_PIPELINE_TOKENS: readonly string[] = [ORG_SCOPE_TOKEN]\n\n/**\n * `/crm/deals` — the pipeline (AGL-2598).\n *\n * ## Two views of one pipeline\n *\n * The BOARD is one pipeline's open deals, one column per open stage, read\n * in one bounded listener over `(visibleTo, pipelineId, status, updatedAt)`\n * and sorted into columns here — a listener per column would be five\n * subscriptions for one board. The TABLE is the same pipeline's deals,\n * paged by the query, with a status toggle; it is where the closed history\n * is found. The three figures above both come from the board's read, so\n * they describe what is in play whichever view is showing.\n *\n * ## Which pipeline\n *\n * The switcher in the header (AGL-2620) picks among the ACTIVE pipelines\n * and opens on the default; the board, the table, the figures and the New\n * deal drawer all follow it. It is section state rather than a route: a\n * pipeline is a lens on the section, and a saved view that names one\n * composes with it. **Pipelines** opens the dialog that creates, renames,\n * defaults, archives and — through it — edits the stages of each.\n *\n * ## Every move goes through the route\n *\n * A drag, a card's \"Move to\", \"Mark won\" and \"Mark lost\" all end in\n * `useDealStageApi`, never a Firestore write: the stage is the fact the\n * automations listen for, and only the server emits the event. The section\n * marks the card pending while the request runs and lets the listener land\n * it — see `DealBoard` for why a move is not applied optimistically.\n *\n * ## Creation is a button\n *\n * \"New deal\" opens a drawer; there is no form above the list. The drawer\n * writes client-direct against the rules, stamped with this console's scope.\n *\n * ## The table selects and exports (AGL-2621)\n *\n * The table's rows are selectable, and a selection raises `DealsBulkBar`\n * over it — whose stage moves go through the same `useDealStageApi` as a\n * drag. Export CSV beside the status toggle writes the page through\n * `dealsCsv()`, naming the pipeline, the stage and the owner; the bar\n * writes the same file over the selection.\n */\nexport function DealsSection(props: ConsolePluginPageProps) {\n const { hostId, org, basePath } = props\n const routes = crmRoutes(basePath ?? '')\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const scope = useCrmScope({ hostId, org })\n const pipelineState = usePipeline(scope.orgId, {\n hostId,\n org: (org ?? null) as Record<string, unknown> | null,\n })\n const { activePipelines } = pipelineState\n const [pipelineId, setPipelineId] = useState<string | null>(null)\n // The chosen pipeline while it is still active; else the default. A\n // pipeline archived from another tab falls back rather than leaving the\n // board on a pipeline the picker no longer lists.\n const pipeline =\n (pipelineId ? activePipelines.find((entry) => entry.$id === pipelineId) : null) ??\n pipelineState.pipeline\n const roster = useOrgMemberDirectory(scope.orgId)\n const api = useDealStageApi(hostId)\n const nowMs = useMemo(() => Date.now(), [])\n // The org's deal fields, for the table's optional columns (AGL-2661).\n const dealFields = useContactFieldDefinitions(scope.orgId, 'deal')\n\n const [view, setView] = useState<View>('board')\n const [closedExpanded, setClosedExpanded] = useState(false)\n /*\n * The table's status filter is the saved VIEW'S (AGL-2617): a view of\n * deals is a view of the table, and it holds the status beside the\n * columns and the sort. Opening one shows the table, because the board\n * has no columns to arrange and answers to the pipeline's stages alone.\n */\n const views = useCrmSavedView({\n section: 'deals',\n hostId,\n org,\n basePath: basePath ?? '',\n })\n const statusFilter: StatusFilter = useMemo(() => {\n const value = views.state.filters.find((clause) => clause.field === 'status')?.value\n return value === 'open' || value === 'won' || value === 'lost' ? value : 'all'\n }, [views.state.filters])\n // The status is the query's clause; the \"No next activity\" clause beside\n // it (AGL-2661) narrows the loaded page and survives a status change.\n const viewFilters = views.state.filters\n useEffect(() => {\n if (views.currentId) setView('table')\n }, [views.currentId])\n\n // The board's read stays on in the table view too: it is what the summary\n // above both views is computed from, and it is already bounded.\n const open = useDealsByStatus(\n scope.orgId,\n scope.visibleTo,\n pipeline?.$id ?? null,\n 'open',\n BOARD_OPEN_LIMIT,\n )\n const closedPipelineId =\n view === 'board' && closedExpanded ? (pipeline?.$id ?? null) : null\n const won = useDealsByStatus(scope.orgId, scope.visibleTo, closedPipelineId, 'won', BOARD_CLOSED_LIMIT)\n const lost = useDealsByStatus(scope.orgId, scope.visibleTo, closedPipelineId, 'lost', BOARD_CLOSED_LIMIT)\n const paged = usePagedDeals(\n view === 'table' ? scope.orgId : null,\n scope.visibleTo,\n statusFilter,\n pipeline?.$id ?? null,\n )\n\n const summary = useMemo(() => boardSummary(open.data, pipeline), [open.data, pipeline])\n\n /*\n * MOVES IN FLIGHT. A card in this set is dimmed and cannot be dragged\n * again until the route answers; the listener is what moves it.\n */\n const [moving, setMoving] = useState<ReadonlySet<string>>(() => new Set())\n const track = useCallback(\n async (deal: DealDoc, request: () => Promise<unknown>, done: string) => {\n setMoving((current) => new Set(current).add(deal.$id))\n try {\n await request()\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The deal could not be moved.',\n { variant: 'warning', allowDuplicate: true },\n )\n return false\n } finally {\n setMoving((current) => {\n const next = new Set(current)\n next.delete(deal.$id)\n return next\n })\n }\n },\n [enqueueSnackbar],\n )\n const handleMove = useCallback(\n (deal: DealDoc, stageId: string) => {\n const stage = dealStageById(pipeline, stageId)\n void track(\n deal,\n () => api.moveToStage(deal, stageId),\n `Moved to ${stage?.name ?? 'stage'}`,\n )\n },\n [api, pipeline, track],\n )\n const handleWon = useCallback(\n (deal: DealDoc) => void track(deal, () => api.markWon(deal), 'Deal won'),\n [api, track],\n )\n const [losing, setLosing] = useState<DealDoc | null>(null)\n const handleLost = useCallback((deal: DealDoc) => setLosing(deal), [])\n const confirmLost = useCallback(\n (reason: string) => {\n const deal = losing\n if (!deal) return\n setLosing(null)\n void track(deal, () => api.markLost(deal, reason), 'Deal marked lost')\n },\n [api, losing, track],\n )\n\n const openDeal = useCallback(\n (deal: DealDoc) => router.push(routes.deal(deal.$id)),\n [router, routes],\n )\n\n const [creating, setCreating] = useState(false)\n const [managingPipelines, setManagingPipelines] = useState(false)\n\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n // A page or a status is a different set of rows; a selection made on\n // the last one would be a count over rows no longer on screen.\n useEffect(() => setSelectedIds([]), [statusFilter, paged.page])\n const csvOptions: DealCsvOptions = useMemo(\n () => ({\n pipelineName: (id) => pipelineState.pipelineById(id)?.name,\n stageName: (pipelineId, stageId) =>\n dealStageById(pipelineState.pipelineById(pipelineId), stageId)?.name,\n ownerEmail: (uid) => {\n const member = findOrgMember(roster.members, uid)\n return member?.email || member?.label || uid\n },\n }),\n [pipelineState, roster.members],\n )\n const handleExport = useCallback(() => {\n downloadTextFile('deals.csv', 'text/csv', dealsCsv(paged.rows, csvOptions))\n }, [paged.rows, csvOptions])\n\n const columns: GridColDef[] = useMemo(\n () => [\n {\n field: 'title',\n headerName: 'Deal',\n flex: 1.6,\n minWidth: 220,\n renderCell: ({ row }: { row: DealDoc }) => {\n const withWhom = [row.contactName, row.companyName].filter(Boolean).join(' · ')\n return (\n <Stack sx={{ justifyContent: 'center', lineHeight: 1.25 }}>\n <Typography variant=\"body2\" sx={{ lineHeight: 1.25 }}>\n {row.title || 'Untitled deal'}\n </Typography>\n {withWhom ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap sx={{ lineHeight: 1.25 }}>\n {withWhom}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n {\n field: 'stageId',\n headerName: 'Stage',\n flex: 0.9,\n minWidth: 140,\n valueGetter: (_value: unknown, row: DealDoc) =>\n dealStageById(pipelineState.pipelineById(row.pipelineId), row.stageId)?.name ??\n row.stageId,\n },\n {\n field: 'amountCents',\n headerName: 'Amount',\n flex: 0.8,\n minWidth: 120,\n align: 'right',\n headerAlign: 'right',\n valueGetter: (_value: unknown, row: DealDoc) => row.amountCents ?? null,\n renderCell: ({ row }: { row: DealDoc }) =>\n typeof row.amountCents === 'number' ? formatMoney(row.amountCents, row.currency) : '—',\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 0.9,\n minWidth: 140,\n valueGetter: (_value: unknown, row: DealDoc) => roster.nameOf(row.ownerUid),\n renderCell: ({ row }: { row: DealDoc }) => {\n const label = roster.nameOf(row.ownerUid)\n return label ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <OwnerAvatar label={label} size={22} />\n <Typography variant=\"body2\" noWrap>\n {label}\n </Typography>\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'—'}\n </Typography>\n )\n },\n },\n {\n field: 'expectedCloseAtMs',\n headerName: 'Expected close',\n flex: 0.8,\n minWidth: 130,\n type: 'date',\n valueGetter: (_value: unknown, row: DealDoc) =>\n typeof row.expectedCloseAtMs === 'number' ? new Date(row.expectedCloseAtMs) : null,\n renderCell: ({ row }: { row: DealDoc }) =>\n typeof row.expectedCloseAtMs === 'number'\n ? new Date(row.expectedCloseAtMs).toLocaleDateString()\n : '—',\n },\n {\n field: 'status',\n headerName: 'Status',\n flex: 0.6,\n minWidth: 100,\n renderCell: ({ row }: { row: DealDoc }) => (\n <Chip\n size=\"small\"\n label={DEAL_STATUS_LABELS[row.status] ?? row.status}\n color={row.status === 'won' ? 'success' : row.status === 'lost' ? 'default' : 'primary'}\n variant={row.status === 'open' ? 'outlined' : 'filled'}\n />\n ),\n },\n // When the earliest open task against the deal is due (AGL-2661).\n nextActivityColumn(nowMs),\n // The org's deal fields as optional columns (AGL-2661).\n ...customFieldColumns(dealFields.active),\n ],\n [pipelineState, roster, dealFields.active, nowMs],\n )\n const tableRows = useMemo(\n () => filterByNextActivity(paged.rows, viewFilters),\n [paged.rows, viewFilters],\n )\n /*\n * The table's column and sort models are the view's (AGL-2617); its\n * filters are the grid's own panel over the view's clauses (AGL-3313).\n * The query serves the status, so the panel's clause replaces it, and\n * \"No next activity\" stands beside it over the loaded page.\n */\n const filterColumns = useMemo(\n () => crmFilterColumns(columns, DEAL_GRID_FILTER_FIELDS, DEAL_FILTER_OPTIONS, DEAL_GRID_FILTER_HEADERS),\n [columns],\n )\n const grid = useCrmViewGrid(views, filterColumns)\n const gridFilter = useCrmGridFilter({\n clauses: viewFilters,\n onChange: views.setFilters,\n selectFields: ['status'],\n single: true,\n keepAlongside: isNoNextActivityClause,\n })\n\n const noOrg = scope.ready && !scope.orgId\n const pipelineLoading =\n !noOrg && (pipelineState.status === 'loading' || pipelineState.seeding || (!pipeline && pipelineState.status !== 'error'))\n\n return (\n <>\n <CardDisplay\n header={'Deals'}\n help={pluginDocsHelp('deals', { anchor: '#the-board-and-the-table' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n // The record actions, top right and never clipped (AGL-3311).\n action: (\n <CrmListActions>\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiCogOutline.path} size={0.8} />}\n disabled={!scope.orgId || pipelineState.status === 'loading'}\n onClick={() => setManagingPipelines(true)}\n >\n {'Pipelines'}\n </Button>\n <Button\n size=\"small\"\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n </CrmListActions>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack\n direction=\"row\"\n spacing={3}\n sx={{ alignItems: 'flex-end', flexWrap: 'wrap', rowGap: 1 }}\n >\n <Figure label=\"Open deals\" value={summary.openCount.toLocaleString()} />\n <Figure label=\"Pipeline value\" value={formatAmountByCurrency(summary.valueByCurrency)} />\n <Figure label=\"Weighted value\" value={formatAmountByCurrency(summary.weightedByCurrency)} />\n <Stack sx={{ flex: 1 }} />\n {/* Which pipeline the board and table show, above them rather than under (AGL-3311). */}\n {activePipelines.length > 1 ? (\n <TextField\n select\n size=\"small\"\n label=\"Pipeline\"\n value={pipeline?.$id ?? ''}\n onChange={(event) => setPipelineId(event.target.value)}\n sx={{ minWidth: 160 }}\n slotProps={{ select: { 'aria-label': 'Pipeline' } }}\n >\n {activePipelines.map((entry) => (\n <MenuItem key={entry.$id} value={entry.$id}>\n {entry.name}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={view}\n onChange={(_event, next) => {\n if (next) setView(next as View)\n }}\n aria-label=\"View\"\n >\n <ToggleButton value=\"board\" aria-label=\"Board\">\n <MdiIcon path={mdiViewColumnOutline.path} size={0.8} />\n <Typography variant=\"button\" sx={{ ml: 0.5 }}>\n {'Board'}\n </Typography>\n </ToggleButton>\n <ToggleButton value=\"table\" aria-label=\"Table\">\n <MdiIcon path={mdiTableLarge.path} size={0.8} />\n <Typography variant=\"button\" sx={{ ml: 0.5 }}>\n {'Table'}\n </Typography>\n </ToggleButton>\n </ToggleButtonGroup>\n </Stack>\n\n {noOrg ? (\n <Alert severity=\"info\">\n {'This site has no organization, so it has no pipeline.'}\n </Alert>\n ) : pipelineState.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The pipeline could not be read. Deals need the \"Manage data\" permission.'}\n </Alert>\n ) : pipelineLoading ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <CircularProgress size={18} />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {pipelineState.seeding ? 'Setting up your Sales pipeline…' : 'Loading the pipeline…'}\n </Typography>\n </Stack>\n ) : view === 'board' && pipeline ? (\n <>\n {open.status === 'success' && open.data.length === 0 ? (\n <EmptyStateComponent\n compact\n label={'No open deals yet'}\n description={'A deal is a sale in progress, moved across the stages below as it advances.'}\n action={\n <Button\n size=\"small\"\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n }\n />\n ) : null}\n <DealBoard\n pipeline={pipeline}\n deals={open.data}\n won={closedExpanded && won.status !== 'loading' ? won.data : null}\n lost={closedExpanded && lost.status !== 'loading' ? lost.data : null}\n closedExpanded={closedExpanded}\n onToggleClosed={() => setClosedExpanded((current) => !current)}\n labelFor={roster.nameOf}\n moving={moving}\n onOpen={openDeal}\n onMove={handleMove}\n onWon={handleWon}\n onLost={handleLost}\n nowMs={nowMs}\n />\n </>\n ) : (\n <Stack spacing={1.5}>\n {/*\n The view this table is showing, and the clauses narrowing it\n as chips (AGL-2617, AGL-3313). Status and \"No next activity\"\n are set in the grid's own Filters panel.\n */}\n <CrmListToolbar label=\"Deal filters\">\n <CrmViewsControl controller={views} allLabel=\"All deals\" />\n <CrmFilterBar\n fields={DEAL_GRID_FILTER_FIELDS}\n headers={DEAL_GRID_FILTER_HEADERS}\n clauses={viewFilters}\n onChange={views.setFilters}\n options={DEAL_FILTER_OPTIONS}\n servedField=\"status\"\n />\n <Stack sx={{ flex: 1 }} />\n <DealImportButton hostId={hostId} />\n <Button size=\"small\" onClick={handleExport} disabled={!paged.rows.length}>\n {'Export CSV'}\n </Button>\n </CrmListToolbar>\n {paged.status === 'success' && paged.rows.length === 0 && paged.page === 0 ? (\n <EmptyStateComponent\n label={statusFilter === 'all' ? 'No deals yet' : `No ${statusFilter} deals`}\n description={\n statusFilter === 'all'\n ? 'A deal is a sale in progress, moved across the pipeline as it advances.'\n : undefined\n }\n action={\n statusFilter === 'all' ? (\n <Button\n size=\"small\"\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n ) : undefined\n }\n />\n ) : (\n <>\n <DealsBulkBar\n hostId={hostId}\n scope={scope.scope}\n rows={tableRows}\n selected={selectedIds}\n onSelectedChange={setSelectedIds}\n pipelineById={pipelineState.pipelineById}\n roster={roster}\n api={api}\n csv={csvOptions}\n />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={tableRows}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n onOpen={(_id, row) => openDeal(row as DealDoc)}\n // The panel's status is the query's; \"No next activity\"\n // narrows the loaded page (AGL-3313). No quick search:\n // nothing indexes a deal's name, and a search over one\n // page answers \"no match\" for a deal on the next.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter={false}\n // Columns and sort are the view's, controlled (AGL-2617).\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={grid.onColumnVisibilityModelChange}\n sortModel={grid.sortModel}\n onSortModelChange={grid.onSortModelChange}\n hideFooter\n />\n </CrmColumnOrderProvider>\n <ListPagination\n page={paged.page}\n pageSize={paged.pageSize}\n rowCount={paged.rows.length}\n hasMore={paged.hasMore}\n onPageChange={paged.setPage}\n onPageSizeChange={paged.setPageSize}\n />\n </>\n )}\n </Stack>\n )}\n </Stack>\n </CardDisplay>\n <DealEditDrawer\n open={creating}\n onClose={() => setCreating(false)}\n hostId={hostId}\n org={org}\n pipelines={activePipelines}\n defaultPipeline={pipeline}\n />\n <PipelinesDialog\n open={managingPipelines}\n onClose={() => setManagingPipelines(false)}\n orgId={scope.orgId ?? ''}\n // A pipeline made here is stamped the way the seeded default is\n // (`usePipeline`): the site's own tokens under a site; at the\n // organization level the org token, with the picked site as its\n // provenance (AGL-2630).\n hostId={scope.createHostId}\n pipelines={pipelineState.pipelines}\n fromCache={pipelineState.fromCache}\n unreadable={pipelineState.status === 'error'}\n visibleToTokens={scope.visibleTo}\n createTokens={scope.level === 'site' ? scope.createTokens : ORG_PIPELINE_TOKENS}\n />\n <LostReasonDialog\n open={Boolean(losing)}\n dealTitle={losing?.title ?? ''}\n onClose={() => setLosing(null)}\n onConfirm={confirmLost}\n />\n </>\n )\n}\nDealsSection.displayName = 'DealsSection'\n\nfunction Figure(props: { label: string; value: string }) {\n return (\n <Stack sx={{ lineHeight: 1.2 }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"h6\" component=\"span\">\n {props.value}\n </Typography>\n </Stack>\n )\n}\nFigure.displayName = 'Figure'\n\nexport default DealsSection\n"],"names":["dealStageById","filterByNextActivity","findOrgMember","isNoNextActivityClause","ORG_SCOPE_TOKEN","pluginDocsHelp","mdiCogOutline","mdiPlus","mdiTableLarge","mdiViewColumnOutline","CardDisplay","MdiIcon","ListPagination","ListTable","useCrmSavedView","useCrmViewGrid","CrmListActions","CrmListToolbar","CRM_LIST_SLOTS","CrmColumnOrderProvider","CRM_NEXT_ACTIVITY_FILTER_FIELD","CRM_NEXT_ACTIVITY_FILTER_HEADER","nextActivityColumn","CrmFilterBar","crmFilterColumns","useCrmGridFilter","CrmViewsControl","EmptyStateComponent","useSnackbar","Alert","Button","Chip","CircularProgress","MenuItem","Stack","TextField","ToggleButton","ToggleButtonGroup","Typography","useRouter","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmScope","useDealStageApi","BOARD_CLOSED_LIMIT","BOARD_OPEN_LIMIT","useDealsByStatus","usePagedDeals","useOrgMemberDirectory","usePipeline","downloadTextFile","crmRoutes","dealsCsv","boardSummary","DEAL_STATUS_LABELS","formatAmountByCurrency","formatMoney","customFieldColumns","DealBoard","DealEditDrawer","DealImportButton","DealsBulkBar","LostReasonDialog","OwnerAvatar","PipelinesDialog","DEAL_GRID_FILTER_FIELDS","column","kind","path","operators","DEAL_GRID_FILTER_HEADERS","status","DEAL_FILTER_OPTIONS","map","value","label","ORG_PIPELINE_TOKENS","DealsSection","props","pipelineId","scope","hostId","org","basePath","routes","router","enqueueSnackbar","pipelineState","orgId","activePipelines","setPipelineId","pipeline","find","entry","$id","roster","api","nowMs","Date","now","dealFields","view","setView","closedExpanded","setClosedExpanded","views","section","statusFilter","state","filters","clause","field","viewFilters","currentId","open","visibleTo","closedPipelineId","won","lost","paged","summary","data","moving","setMoving","Set","track","deal","request","done","current","add","variant","persist","error","Error","message","allowDuplicate","next","delete","handleMove","stageId","stage","moveToStage","name","handleWon","markWon","losing","setLosing","handleLost","confirmLost","reason","markLost","openDeal","push","creating","setCreating","managingPipelines","setManagingPipelines","selectedIds","setSelectedIds","page","csvOptions","pipelineName","id","pipelineById","stageName","ownerEmail","uid","member","members","email","handleExport","rows","columns","headerName","flex","minWidth","renderCell","row","withWhom","contactName","companyName","filter","Boolean","join","sx","justifyContent","lineHeight","title","color","noWrap","valueGetter","_value","align","headerAlign","amountCents","currency","nameOf","ownerUid","direction","spacing","alignItems","size","type","expectedCloseAtMs","toLocaleDateString","active","tableRows","filterColumns","grid","gridFilter","clauses","onChange","setFilters","selectFields","single","keepAlongside","noOrg","ready","pipelineLoading","seeding","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","startIcon","disabled","onClick","flexWrap","rowGap","Figure","openCount","toLocaleString","valueByCurrency","weightedByCurrency","length","select","event","target","slotProps","exclusive","_event","aria-label","ml","severity","compact","description","deals","onToggleClosed","labelFor","onOpen","onMove","onWon","onLost","controller","allLabel","fields","headers","options","servedField","undefined","selected","onSelectedChange","csv","columnOrder","slots","selectable","_id","filterMode","filterModel","onFilterModelChange","quickFilter","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","hideFooter","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","onClose","pipelines","defaultPipeline","createHostId","fromCache","unreadable","visibleToTokens","createTokens","level","dealTitle","onConfirm","displayName","component"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deals-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ConsolePluginPageProps,\n type CrmDealStatus,\n dealStageById,\n filterByNextActivity,\n findOrgMember,\n isNoNextActivityClause,\n ORG_SCOPE_TOKEN,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport {\n mdiCogOutline,\n mdiPlus,\n mdiTableLarge,\n mdiViewColumnOutline,\n} from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CrmListActions, CrmListToolbar } from './crm-list-toolbar'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport {\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n CRM_NEXT_ACTIVITY_FILTER_HEADER,\n nextActivityColumn,\n} from './crm-next-activity-column'\nimport CrmFilterBar from './crm-filter-bar'\nimport { crmFilterColumns } from '../model/crm-grid-filter'\nimport { useCrmGridFilter } from '../hooks/use-crm-grid-filter'\nimport CrmViewsControl from './crm-views-control'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n Button,\n Chip,\n CircularProgress,\n MenuItem,\n Stack,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useDealStageApi } from '../hooks/use-deal-stage-api'\nimport {\n BOARD_CLOSED_LIMIT,\n BOARD_OPEN_LIMIT,\n useDealsByStatus,\n usePagedDeals,\n} from '../hooks/use-deals'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { usePipeline } from '../hooks/use-pipeline'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport { type DealCsvOptions, dealsCsv } from '../model/deals-csv'\nimport {\n boardSummary,\n DEAL_STATUS_LABELS,\n type DealDoc,\n formatAmountByCurrency,\n formatMoney,\n} from '../model/deal-board-model'\nimport { customFieldColumns } from './contact-custom-columns'\nimport { DealBoard } from './deal-board'\nimport { DealEditDrawer } from './deal-edit-drawer'\nimport { DealImportButton } from './deal-import-drawer'\nimport DealsBulkBar from './deals-bulk-bar'\nimport { LostReasonDialog } from './lost-reason-dialog'\nimport { OwnerAvatar } from './owner-avatar'\nimport { PipelinesDialog } from './pipelines-dialog'\n\ntype View = 'board' | 'table'\ntype StatusFilter = CrmDealStatus | 'all'\n\n/** What the deals table's panel offers: the served status, and \"No next activity\". */\nconst DEAL_GRID_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals'] },\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n]\nconst DEAL_GRID_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n [CRM_NEXT_ACTIVITY_FILTER_FIELD.column]: CRM_NEXT_ACTIVITY_FILTER_HEADER,\n}\nconst DEAL_FILTER_OPTIONS = {\n status: (['open', 'won', 'lost'] as const).map((value) => ({\n value,\n label: DEAL_STATUS_LABELS[value],\n })),\n}\n\n\n/** What a pipeline made at the organization level is stamped with — the org's own token. */\nconst ORG_PIPELINE_TOKENS: readonly string[] = [ORG_SCOPE_TOKEN]\n\n/**\n * `/crm/deals` — the pipeline (AGL-2598).\n *\n * ## Two views of one pipeline\n *\n * The BOARD is one pipeline's open deals, one column per open stage, read\n * in one bounded listener over `(visibleTo, pipelineId, status, updatedAt)`\n * and sorted into columns here — a listener per column would be five\n * subscriptions for one board. The TABLE is the same pipeline's deals,\n * paged by the query, with a status toggle; it is where the closed history\n * is found. The three figures above both come from the board's read, so\n * they describe what is in play whichever view is showing.\n *\n * ## Which pipeline\n *\n * The switcher in the header (AGL-2620) picks among the ACTIVE pipelines\n * and opens on the default; the board, the table, the figures and the New\n * deal drawer all follow it. It is section state rather than a route: a\n * pipeline is a lens on the section, and a saved view that names one\n * composes with it. **Pipelines** opens the dialog that creates, renames,\n * defaults, archives and — through it — edits the stages of each.\n *\n * ## Every move goes through the route\n *\n * A drag, a card's \"Move to\", \"Mark won\" and \"Mark lost\" all end in\n * `useDealStageApi`, never a Firestore write: the stage is the fact the\n * automations listen for, and only the server emits the event. The section\n * marks the card pending while the request runs and lets the listener land\n * it — see `DealBoard` for why a move is not applied optimistically.\n *\n * ## Creation is a button\n *\n * \"New deal\" opens a drawer; there is no form above the list. The drawer\n * writes client-direct against the rules, stamped with this console's scope.\n *\n * ## The table selects and exports (AGL-2621)\n *\n * The table's rows are selectable, and a selection raises `DealsBulkBar`\n * over it — whose stage moves go through the same `useDealStageApi` as a\n * drag. Export CSV beside the status toggle writes the page through\n * `dealsCsv()`, naming the pipeline, the stage and the owner; the bar\n * writes the same file over the selection.\n */\nexport function DealsSection(props: ConsolePluginPageProps) {\n const { hostId, org, basePath } = props\n const routes = crmRoutes(basePath ?? '')\n const router = useRouter()\n const { enqueueSnackbar } = useSnackbar()\n const scope = useCrmScope({ hostId, org })\n const pipelineState = usePipeline(scope.orgId, {\n hostId,\n org: (org ?? null) as Record<string, unknown> | null,\n })\n const { activePipelines } = pipelineState\n const [pipelineId, setPipelineId] = useState<string | null>(null)\n // The chosen pipeline while it is still active; else the default. A\n // pipeline archived from another tab falls back rather than leaving the\n // board on a pipeline the picker no longer lists.\n const pipeline =\n (pipelineId ? activePipelines.find((entry) => entry.$id === pipelineId) : null) ??\n pipelineState.pipeline\n const roster = useOrgMemberDirectory(scope.orgId)\n const api = useDealStageApi(hostId)\n const nowMs = useMemo(() => Date.now(), [])\n // The org's deal fields, for the table's optional columns (AGL-2661).\n const dealFields = useContactFieldDefinitions(scope.orgId, 'deal')\n\n const [view, setView] = useState<View>('board')\n const [closedExpanded, setClosedExpanded] = useState(false)\n /*\n * The table's status filter is the saved VIEW'S (AGL-2617): a view of\n * deals is a view of the table, and it holds the status beside the\n * columns and the sort. Opening one shows the table, because the board\n * has no columns to arrange and answers to the pipeline's stages alone.\n */\n const views = useCrmSavedView({\n section: 'deals',\n hostId,\n org,\n basePath: basePath ?? '',\n })\n const statusFilter: StatusFilter = useMemo(() => {\n const value = views.state.filters.find((clause) => clause.field === 'status')?.value\n return value === 'open' || value === 'won' || value === 'lost' ? value : 'all'\n }, [views.state.filters])\n // The status is the query's clause; the \"No next activity\" clause beside\n // it (AGL-2661) narrows the loaded page and survives a status change.\n const viewFilters = views.state.filters\n useEffect(() => {\n if (views.currentId) setView('table')\n }, [views.currentId])\n\n // The board's read stays on in the table view too: it is what the summary\n // above both views is computed from, and it is already bounded.\n const open = useDealsByStatus(\n scope.orgId,\n scope.visibleTo,\n pipeline?.$id ?? null,\n 'open',\n BOARD_OPEN_LIMIT,\n )\n const closedPipelineId =\n view === 'board' && closedExpanded ? (pipeline?.$id ?? null) : null\n const won = useDealsByStatus(scope.orgId, scope.visibleTo, closedPipelineId, 'won', BOARD_CLOSED_LIMIT)\n const lost = useDealsByStatus(scope.orgId, scope.visibleTo, closedPipelineId, 'lost', BOARD_CLOSED_LIMIT)\n const paged = usePagedDeals(\n view === 'table' ? scope.orgId : null,\n scope.visibleTo,\n statusFilter,\n pipeline?.$id ?? null,\n )\n\n const summary = useMemo(() => boardSummary(open.data, pipeline), [open.data, pipeline])\n\n /*\n * MOVES IN FLIGHT. A card in this set is dimmed and cannot be dragged\n * again until the route answers; the listener is what moves it.\n */\n const [moving, setMoving] = useState<ReadonlySet<string>>(() => new Set())\n const track = useCallback(\n async (deal: DealDoc, request: () => Promise<unknown>, done: string) => {\n setMoving((current) => new Set(current).add(deal.$id))\n try {\n await request()\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n enqueueSnackbar(\n error instanceof Error ? error.message : 'The deal could not be moved.',\n { variant: 'warning', allowDuplicate: true },\n )\n return false\n } finally {\n setMoving((current) => {\n const next = new Set(current)\n next.delete(deal.$id)\n return next\n })\n }\n },\n [enqueueSnackbar],\n )\n const handleMove = useCallback(\n (deal: DealDoc, stageId: string) => {\n const stage = dealStageById(pipeline, stageId)\n void track(\n deal,\n () => api.moveToStage(deal, stageId),\n `Moved to ${stage?.name ?? 'stage'}`,\n )\n },\n [api, pipeline, track],\n )\n const handleWon = useCallback(\n (deal: DealDoc) => void track(deal, () => api.markWon(deal), 'Deal won'),\n [api, track],\n )\n const [losing, setLosing] = useState<DealDoc | null>(null)\n const handleLost = useCallback((deal: DealDoc) => setLosing(deal), [])\n const confirmLost = useCallback(\n (reason: string) => {\n const deal = losing\n if (!deal) return\n setLosing(null)\n void track(deal, () => api.markLost(deal, reason), 'Deal marked lost')\n },\n [api, losing, track],\n )\n\n const openDeal = useCallback(\n (deal: DealDoc) => router.push(routes.deal(deal.$id)),\n [router, routes],\n )\n\n const [creating, setCreating] = useState(false)\n const [managingPipelines, setManagingPipelines] = useState(false)\n\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n // A page or a status is a different set of rows; a selection made on\n // the last one would be a count over rows no longer on screen.\n useEffect(() => setSelectedIds([]), [statusFilter, paged.page])\n const csvOptions: DealCsvOptions = useMemo(\n () => ({\n pipelineName: (id) => pipelineState.pipelineById(id)?.name,\n stageName: (pipelineId, stageId) =>\n dealStageById(pipelineState.pipelineById(pipelineId), stageId)?.name,\n ownerEmail: (uid) => {\n const member = findOrgMember(roster.members, uid)\n return member?.email || member?.label || uid\n },\n }),\n [pipelineState, roster.members],\n )\n const handleExport = useCallback(() => {\n downloadTextFile('deals.csv', 'text/csv', dealsCsv(paged.rows, csvOptions))\n }, [paged.rows, csvOptions])\n\n const columns: GridColDef[] = useMemo(\n () => [\n {\n field: 'title',\n headerName: 'Deal',\n flex: 1.6,\n minWidth: 220,\n renderCell: ({ row }: { row: DealDoc }) => {\n const withWhom = [row.contactName, row.companyName].filter(Boolean).join(' · ')\n return (\n <Stack sx={{ justifyContent: 'center', lineHeight: 1.25 }}>\n <Typography variant=\"body2\" sx={{ lineHeight: 1.25 }}>\n {row.title || 'Untitled deal'}\n </Typography>\n {withWhom ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap sx={{ lineHeight: 1.25 }}>\n {withWhom}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n {\n field: 'stageId',\n headerName: 'Stage',\n flex: 0.9,\n minWidth: 140,\n valueGetter: (_value: unknown, row: DealDoc) =>\n dealStageById(pipelineState.pipelineById(row.pipelineId), row.stageId)?.name ??\n row.stageId,\n },\n {\n field: 'amountCents',\n headerName: 'Amount',\n flex: 0.8,\n minWidth: 120,\n align: 'right',\n headerAlign: 'right',\n valueGetter: (_value: unknown, row: DealDoc) => row.amountCents ?? null,\n renderCell: ({ row }: { row: DealDoc }) =>\n typeof row.amountCents === 'number' ? formatMoney(row.amountCents, row.currency) : '—',\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 0.9,\n minWidth: 140,\n valueGetter: (_value: unknown, row: DealDoc) => roster.nameOf(row.ownerUid),\n renderCell: ({ row }: { row: DealDoc }) => {\n const label = roster.nameOf(row.ownerUid)\n return label ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <OwnerAvatar label={label} size={22} />\n <Typography variant=\"body2\" noWrap>\n {label}\n </Typography>\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'—'}\n </Typography>\n )\n },\n },\n {\n field: 'expectedCloseAtMs',\n headerName: 'Expected close',\n flex: 0.8,\n minWidth: 130,\n type: 'date',\n valueGetter: (_value: unknown, row: DealDoc) =>\n typeof row.expectedCloseAtMs === 'number' ? new Date(row.expectedCloseAtMs) : null,\n renderCell: ({ row }: { row: DealDoc }) =>\n typeof row.expectedCloseAtMs === 'number'\n ? new Date(row.expectedCloseAtMs).toLocaleDateString()\n : '—',\n },\n {\n field: 'status',\n headerName: 'Status',\n flex: 0.6,\n minWidth: 100,\n renderCell: ({ row }: { row: DealDoc }) => (\n <Chip\n size=\"small\"\n label={DEAL_STATUS_LABELS[row.status] ?? row.status}\n color={row.status === 'won' ? 'success' : row.status === 'lost' ? 'default' : 'primary'}\n variant={row.status === 'open' ? 'outlined' : 'filled'}\n />\n ),\n },\n // When the earliest open task against the deal is due (AGL-2661).\n nextActivityColumn(nowMs),\n // The org's deal fields as optional columns (AGL-2661).\n ...customFieldColumns(dealFields.active),\n ],\n [pipelineState, roster, dealFields.active, nowMs],\n )\n const tableRows = useMemo(\n () => filterByNextActivity(paged.rows, viewFilters),\n [paged.rows, viewFilters],\n )\n /*\n * The table's column and sort models are the view's (AGL-2617); its\n * filters are the grid's own panel over the view's clauses (AGL-3313).\n * The query serves the status, so the panel's clause replaces it, and\n * \"No next activity\" stands beside it over the loaded page.\n */\n const filterColumns = useMemo(\n () => crmFilterColumns(columns, DEAL_GRID_FILTER_FIELDS, DEAL_FILTER_OPTIONS, DEAL_GRID_FILTER_HEADERS),\n [columns],\n )\n const grid = useCrmViewGrid(views, filterColumns)\n const gridFilter = useCrmGridFilter({\n clauses: viewFilters,\n onChange: views.setFilters,\n selectFields: ['status'],\n single: true,\n keepAlongside: isNoNextActivityClause,\n })\n\n const noOrg = scope.ready && !scope.orgId\n const pipelineLoading =\n !noOrg && (pipelineState.status === 'loading' || pipelineState.seeding || (!pipeline && pipelineState.status !== 'error'))\n\n return (\n <>\n <CardDisplay\n header={'Deals'}\n help={pluginDocsHelp('deals', { anchor: '#the-board-and-the-table' })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n // The record actions, top right and never clipped (AGL-3311).\n action: (\n <CrmListActions>\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiCogOutline.path} size={0.8} />}\n disabled={!scope.orgId || pipelineState.status === 'loading'}\n onClick={() => setManagingPipelines(true)}\n >\n {'Pipelines'}\n </Button>\n <Button\n size=\"small\"\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n </CrmListActions>\n ),\n }}\n >\n <Stack spacing={2}>\n <Stack\n direction=\"row\"\n spacing={3}\n sx={{ alignItems: 'flex-end', flexWrap: 'wrap', rowGap: 1 }}\n >\n <Figure label=\"Open deals\" value={summary.openCount.toLocaleString()} />\n <Figure label=\"Pipeline value\" value={formatAmountByCurrency(summary.valueByCurrency)} />\n <Figure label=\"Weighted value\" value={formatAmountByCurrency(summary.weightedByCurrency)} />\n <Stack sx={{ flex: 1 }} />\n {/* Which pipeline the board and table show, above them rather than under (AGL-3311). */}\n {activePipelines.length > 1 ? (\n <TextField\n select\n size=\"small\"\n label=\"Pipeline\"\n value={pipeline?.$id ?? ''}\n onChange={(event) => setPipelineId(event.target.value)}\n sx={{ minWidth: 160 }}\n slotProps={{ select: { 'aria-label': 'Pipeline' } }}\n >\n {activePipelines.map((entry) => (\n <MenuItem key={entry.$id} value={entry.$id}>\n {entry.name}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={view}\n onChange={(_event, next) => {\n if (next) setView(next as View)\n }}\n aria-label=\"View\"\n >\n <ToggleButton value=\"board\" aria-label=\"Board\">\n <MdiIcon path={mdiViewColumnOutline.path} size={0.8} />\n <Typography variant=\"button\" sx={{ ml: 0.5 }}>\n {'Board'}\n </Typography>\n </ToggleButton>\n <ToggleButton value=\"table\" aria-label=\"Table\">\n <MdiIcon path={mdiTableLarge.path} size={0.8} />\n <Typography variant=\"button\" sx={{ ml: 0.5 }}>\n {'Table'}\n </Typography>\n </ToggleButton>\n </ToggleButtonGroup>\n </Stack>\n\n {noOrg ? (\n <Alert severity=\"info\">\n {'This site has no organization, so it has no pipeline.'}\n </Alert>\n ) : pipelineState.status === 'error' ? (\n <Alert severity=\"warning\">\n {'The pipeline could not be read. Deals need the \"Manage data\" permission.'}\n </Alert>\n ) : pipelineLoading ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <CircularProgress size={18} />\n <Typography variant=\"body2\" color=\"text.secondary\">\n {pipelineState.seeding ? 'Setting up your Sales pipeline…' : 'Loading the pipeline…'}\n </Typography>\n </Stack>\n ) : view === 'board' && pipeline ? (\n <>\n {open.status === 'success' && open.data.length === 0 ? (\n <EmptyStateComponent\n compact\n label={'No open deals yet'}\n description={'A deal is a sale in progress, moved across the stages below as it advances.'}\n action={\n <Button\n size=\"small\"\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n }\n />\n ) : null}\n <DealBoard\n pipeline={pipeline}\n deals={open.data}\n won={closedExpanded && won.status !== 'loading' ? won.data : null}\n lost={closedExpanded && lost.status !== 'loading' ? lost.data : null}\n closedExpanded={closedExpanded}\n onToggleClosed={() => setClosedExpanded((current) => !current)}\n labelFor={roster.nameOf}\n moving={moving}\n onOpen={openDeal}\n onMove={handleMove}\n onWon={handleWon}\n onLost={handleLost}\n nowMs={nowMs}\n />\n </>\n ) : (\n <Stack spacing={1.5}>\n {/*\n The view this table is showing, and the clauses narrowing it\n as chips (AGL-2617, AGL-3313). Status and \"No next activity\"\n are set in the grid's own Filters panel.\n */}\n <CrmListToolbar label=\"Deal filters\">\n <CrmViewsControl controller={views} allLabel=\"All deals\" />\n <CrmFilterBar\n fields={DEAL_GRID_FILTER_FIELDS}\n headers={DEAL_GRID_FILTER_HEADERS}\n clauses={viewFilters}\n onChange={views.setFilters}\n options={DEAL_FILTER_OPTIONS}\n servedField=\"status\"\n />\n <Stack sx={{ flex: 1 }} />\n <DealImportButton hostId={hostId} />\n <Button size=\"small\" onClick={handleExport} disabled={!paged.rows.length}>\n {'Export CSV'}\n </Button>\n </CrmListToolbar>\n <>\n <DealsBulkBar\n hostId={hostId}\n scope={scope.scope}\n rows={tableRows}\n selected={selectedIds}\n onSelectedChange={setSelectedIds}\n pipelineById={pipelineState.pipelineById}\n roster={roster}\n api={api}\n csv={csvOptions}\n />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={tableRows}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n onOpen={(_id, row) => openDeal(row as DealDoc)}\n // The panel's status is the query's; \"No next activity\"\n // narrows the loaded page (AGL-3313). No quick search:\n // nothing indexes a deal's name, and a search over one\n // page answers \"no match\" for a deal on the next.\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter={false}\n // An empty table is said inside the grid, so its toolbar\n // stays to change the status that emptied it.\n noRowsLabel={statusFilter === 'all' ? 'No deals yet' : `No ${statusFilter} deals`}\n noRowsDescription={\n statusFilter === 'all'\n ? 'A deal is a sale in progress, moved across the pipeline as it advances.'\n : undefined\n }\n noRowsAction={\n statusFilter === 'all' ? (\n <Button\n size=\"small\"\n variant=\"contained\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={!pipeline || !scope.orgId}\n onClick={() => setCreating(true)}\n >\n {'New deal'}\n </Button>\n ) : undefined\n }\n // Columns and sort are the view's, controlled (AGL-2617).\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={grid.onColumnVisibilityModelChange}\n sortModel={grid.sortModel}\n onSortModelChange={grid.onSortModelChange}\n hideFooter\n />\n </CrmColumnOrderProvider>\n <ListPagination\n page={paged.page}\n pageSize={paged.pageSize}\n rowCount={paged.rows.length}\n hasMore={paged.hasMore}\n onPageChange={paged.setPage}\n onPageSizeChange={paged.setPageSize}\n />\n </>\n </Stack>\n )}\n </Stack>\n </CardDisplay>\n <DealEditDrawer\n open={creating}\n onClose={() => setCreating(false)}\n hostId={hostId}\n org={org}\n pipelines={activePipelines}\n defaultPipeline={pipeline}\n />\n <PipelinesDialog\n open={managingPipelines}\n onClose={() => setManagingPipelines(false)}\n orgId={scope.orgId ?? ''}\n // A pipeline made here is stamped the way the seeded default is\n // (`usePipeline`): the site's own tokens under a site; at the\n // organization level the org token, with the picked site as its\n // provenance (AGL-2630).\n hostId={scope.createHostId}\n pipelines={pipelineState.pipelines}\n fromCache={pipelineState.fromCache}\n unreadable={pipelineState.status === 'error'}\n visibleToTokens={scope.visibleTo}\n createTokens={scope.level === 'site' ? scope.createTokens : ORG_PIPELINE_TOKENS}\n />\n <LostReasonDialog\n open={Boolean(losing)}\n dealTitle={losing?.title ?? ''}\n onClose={() => setLosing(null)}\n onConfirm={confirmLost}\n />\n </>\n )\n}\nDealsSection.displayName = 'DealsSection'\n\nfunction Figure(props: { label: string; value: string }) {\n return (\n <Stack sx={{ lineHeight: 1.2 }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {props.label}\n </Typography>\n <Typography variant=\"h6\" component=\"span\">\n {props.value}\n </Typography>\n </Stack>\n )\n}\nFigure.displayName = 'Figure'\n\nexport default DealsSection\n"],"names":["dealStageById","filterByNextActivity","findOrgMember","isNoNextActivityClause","ORG_SCOPE_TOKEN","pluginDocsHelp","mdiCogOutline","mdiPlus","mdiTableLarge","mdiViewColumnOutline","CardDisplay","MdiIcon","ListPagination","ListTable","useCrmSavedView","useCrmViewGrid","CrmListActions","CrmListToolbar","CRM_LIST_SLOTS","CrmColumnOrderProvider","CRM_NEXT_ACTIVITY_FILTER_FIELD","CRM_NEXT_ACTIVITY_FILTER_HEADER","nextActivityColumn","CrmFilterBar","crmFilterColumns","useCrmGridFilter","CrmViewsControl","EmptyStateComponent","useSnackbar","Alert","Button","Chip","CircularProgress","MenuItem","Stack","TextField","ToggleButton","ToggleButtonGroup","Typography","useRouter","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmScope","useDealStageApi","BOARD_CLOSED_LIMIT","BOARD_OPEN_LIMIT","useDealsByStatus","usePagedDeals","useOrgMemberDirectory","usePipeline","downloadTextFile","crmRoutes","dealsCsv","boardSummary","DEAL_STATUS_LABELS","formatAmountByCurrency","formatMoney","customFieldColumns","DealBoard","DealEditDrawer","DealImportButton","DealsBulkBar","LostReasonDialog","OwnerAvatar","PipelinesDialog","DEAL_GRID_FILTER_FIELDS","column","kind","path","operators","DEAL_GRID_FILTER_HEADERS","status","DEAL_FILTER_OPTIONS","map","value","label","ORG_PIPELINE_TOKENS","DealsSection","props","pipelineId","scope","hostId","org","basePath","routes","router","enqueueSnackbar","pipelineState","orgId","activePipelines","setPipelineId","pipeline","find","entry","$id","roster","api","nowMs","Date","now","dealFields","view","setView","closedExpanded","setClosedExpanded","views","section","statusFilter","state","filters","clause","field","viewFilters","currentId","open","visibleTo","closedPipelineId","won","lost","paged","summary","data","moving","setMoving","Set","track","deal","request","done","current","add","variant","persist","error","Error","message","allowDuplicate","next","delete","handleMove","stageId","stage","moveToStage","name","handleWon","markWon","losing","setLosing","handleLost","confirmLost","reason","markLost","openDeal","push","creating","setCreating","managingPipelines","setManagingPipelines","selectedIds","setSelectedIds","page","csvOptions","pipelineName","id","pipelineById","stageName","ownerEmail","uid","member","members","email","handleExport","rows","columns","headerName","flex","minWidth","renderCell","row","withWhom","contactName","companyName","filter","Boolean","join","sx","justifyContent","lineHeight","title","color","noWrap","valueGetter","_value","align","headerAlign","amountCents","currency","nameOf","ownerUid","direction","spacing","alignItems","size","type","expectedCloseAtMs","toLocaleDateString","active","tableRows","filterColumns","grid","gridFilter","clauses","onChange","setFilters","selectFields","single","keepAlongside","noOrg","ready","pipelineLoading","seeding","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","startIcon","disabled","onClick","flexWrap","rowGap","Figure","openCount","toLocaleString","valueByCurrency","weightedByCurrency","length","select","event","target","slotProps","exclusive","_event","aria-label","ml","severity","compact","description","deals","onToggleClosed","labelFor","onOpen","onMove","onWon","onLost","controller","allLabel","fields","headers","options","servedField","selected","onSelectedChange","csv","columnOrder","slots","selectable","_id","filterMode","filterModel","onFilterModelChange","quickFilter","noRowsLabel","noRowsDescription","undefined","noRowsAction","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","hideFooter","pageSize","rowCount","hasMore","onPageChange","setPage","onPageSizeChange","setPageSize","onClose","pipelines","defaultPipeline","createHostId","fromCache","unreadable","visibleToTokens","createTokens","level","dealTitle","onConfirm","displayName","component"],"mappings":"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"}
@@ -24,7 +24,6 @@ import { useCrmScope } from "../hooks/use-crm-scope.js";
24
24
  import { useCrmViewGrid } from "../hooks/use-crm-view-grid.js";
25
25
  import { CRM_LIST_SLOTS, CrmColumnOrderProvider } from "./crm-column-menu.js";
26
26
  import CrmViewsControl from "./crm-views-control.js";
27
- import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.component";
28
27
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
29
28
  import { useFirestore, useUser } from "@aglyn/tenant-feature-instance";
30
29
  import { Button, Checkbox, Stack, ToggleButton, ToggleButtonGroup, Typography } from "@mui/material";
@@ -583,19 +582,6 @@ const EMPTY_LABEL = {
583
582
  variant: "body2",
584
583
  color: "error",
585
584
  children: 'The tasks could not be loaded. Reload to try again.'
586
- }) : status === 'success' && !loaded.length ? /*#__PURE__*/ _jsx(EmptyStateComponent, {
587
- label: EMPTY_LABEL[view],
588
- description: EMPTY_COPY[view],
589
- action: /*#__PURE__*/ _jsx(Button, {
590
- size: "small",
591
- variant: "contained",
592
- color: "primary",
593
- onClick: ()=>setDrawer({
594
- open: true,
595
- task: null
596
- }),
597
- children: 'New task'
598
- })
599
585
  }) : /*#__PURE__*/ _jsxs(_Fragment, {
600
586
  children: [
601
587
  truncated ? /*#__PURE__*/ _jsx(Typography, {
@@ -639,8 +625,23 @@ const EMPTY_LABEL = {
639
625
  task: found
640
626
  });
641
627
  },
642
- // The panel and the search are the grid's; the section
643
- // answers them, the view through the query (AGL-3313).
628
+ /*
629
+ * The panel and the search are the grid's; the section
630
+ * answers them, the view through the query (AGL-3313). An
631
+ * empty view is said inside the grid rather than instead of
632
+ * it, so its toolbar stays to choose another view.
633
+ */ noRowsLabel: loaded.length ? 'No tasks match these filters' : EMPTY_LABEL[view],
634
+ noRowsDescription: loaded.length ? undefined : EMPTY_COPY[view],
635
+ noRowsAction: loaded.length ? undefined : /*#__PURE__*/ _jsx(Button, {
636
+ size: "small",
637
+ variant: "contained",
638
+ color: "primary",
639
+ onClick: ()=>setDrawer({
640
+ open: true,
641
+ task: null
642
+ }),
643
+ children: 'New task'
644
+ }),
644
645
  filterMode: "server",
645
646
  filterModel: gridFilter.filterModel,
646
647
  onFilterModelChange: gridFilter.onFilterModelChange,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/tasks-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ConsolePluginPageProps,\n CRM_COLLECTIONS,\n type CrmViewFilterClause,\n findOrgMember,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport CrmViewsControl from './crm-views-control'\nimport EmptyStateComponent from '@aglyn/shared-ui-jsx/components/empty-state.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Checkbox,\n Stack,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { deleteField, doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useCrmRecordNames } from '../hooks/use-crm-record-names'\nimport { type CrmTaskRow, useCrmTaskList, useNowMs } from '../hooks/use-crm-tasks'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport { refreshCrmNextActivity } from '../model/next-activity-api'\nimport { completeCrmTask } from '../model/task-api'\nimport { crmTaskCallScope } from '../model/task-routes'\nimport { type TaskCsvOptions, tasksCsv } from '../model/tasks-csv'\nimport {\n CRM_TASK_KIND_LABELS,\n CRM_TASK_KINDS,\n CRM_TASK_PRIORITIES,\n CRM_TASK_PRIORITY_LABELS,\n CRM_TASK_VIEW_LIMIT,\n CRM_TASK_VIEWS,\n type CrmTaskView,\n} from '../model/task-views'\nimport {\n type ListFilterField,\n matchListFilter,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { crmFilterColumns, crmRowMatchesSearch } from '../model/crm-grid-filter'\nimport { useCrmGridFilter } from '../hooks/use-crm-grid-filter'\nimport CrmFilterBar from './crm-filter-bar'\nimport { CrmListToolbar } from './crm-list-toolbar'\nimport {\n TaskDueText,\n TaskKindCell,\n TaskPriorityChip,\n TaskRecordLink,\n} from './task-cells'\nimport TaskEditDrawer from './task-edit-drawer'\nimport TasksCalendar from './tasks-calendar'\nimport { TaskImportButton } from './task-import-drawer'\nimport TaskSnoozeMenu from './task-snooze-menu'\nimport TasksBulkBar from './tasks-bulk-bar'\n\n/** What an empty view is headed, by view — \"nothing overdue\" is good news. */\n/*\n * What the tasks grid's panel offers (AGL-3313). `view` is the task view\n * the query serves — a hidden column, since it is no field of a task — and\n * the rest narrow the rows that view loaded.\n */\nconst TASK_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'view', kind: 'exact', path: 'view', operators: ['equals'] },\n { column: 'kind', kind: 'exact', path: 'kind', operators: ['equals', 'isAnyOf'] },\n { column: 'priority', kind: 'exact', path: 'priority', operators: ['equals', 'isAnyOf'] },\n { column: 'assigneeUid', kind: 'exact', path: 'assigneeUid', operators: ['equals', 'isAnyOf'] },\n]\nconst TASK_FILTER_HEADERS: Readonly<Record<string, string>> = {\n view: 'Show',\n kind: 'Kind',\n priority: 'Priority',\n assigneeUid: 'Assignee',\n}\n/** The filter-only `view` column never shows. */\nconst TASK_HIDDEN_COLUMNS: Readonly<Record<string, boolean>> = { view: false }\n/** No `view` clause is \"My tasks\", which the section opened on before views existed. */\nconst TASK_VIEW_DEFAULT: CrmViewFilterClause = { field: 'view', op: 'equals', value: 'mine' }\n/** What the quick search reads on a task. */\nconst TASK_SEARCH_FIELDS = ['title', 'notes'] as const\n\nconst EMPTY_LABEL: Record<CrmTaskView, string> = {\n mine: 'Nothing is assigned to you',\n overdue: 'Nothing is overdue',\n today: 'Nothing is due today',\n upcoming: 'Nothing is scheduled beyond today',\n open: 'No open tasks',\n done: 'No completed tasks yet',\n}\n\n/** The one sentence under it, by view. */\nconst EMPTY_COPY: Record<CrmTaskView, string> = {\n mine: 'Tasks you create are yours unless you hand them to someone.',\n overdue: 'Every dated task is on or ahead of schedule.',\n today: 'Nothing on your calendar day is owed to anyone in the CRM.',\n upcoming: 'A task with a due date past today would be listed here.',\n open: 'A task is a call, an email, a meeting or a to-do owed to a contact, a company or a deal.',\n done: 'Tasks ticked off on their record, or here, are kept for the history.',\n}\n\n/**\n * `/crm/tasks` — what the team owes the people in the CRM (AGL-2599).\n *\n * Six views over `orgs/{orgId}/crmTasks`, each a bounded listener on one of\n * the collection's indexes, chosen by a segmented control. A task hangs off\n * a contact, a company or a deal, but it is listed HERE because \"what is due\n * today\" is a question about the team's day and not about any one record;\n * the record pages carry their own `RecordTasksCard` for the other\n * direction.\n *\n * Completing goes through `crm/task-complete`, because completing fires the\n * `taskCompleted` host event and only the server can run a workflow.\n * Reopening is client-direct: undoing a tick has no side effect beyond the\n * document, and the rules gate it like any other CRM write.\n *\n * The rows are selectable, and a selection raises `TasksBulkBar` over the\n * list — Complete and Assign through their routes, the due date and the\n * delete as batched writes (AGL-2621). Export CSV beside the view control\n * writes the view through `tasksCsv()`, naming the assignee and the linked\n * records; the bar writes the same file over the selection.\n */\nexport function TasksSection(props: ConsolePluginPageProps) {\n const { hostId, org, basePath = '' } = props\n const orgRecord = org as Record<string, unknown> | undefined\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const nowMs = useNowMs()\n const routes = useMemo(() => crmRoutes(basePath), [basePath])\n // The viewing group a linked contact is named through, or `null` at the\n // organization level, where each is named through its own holder.\n const { consentGroup } = useCrmScope({ hostId, org: orgRecord })\n const groupId = consentGroup?.groupId ?? null\n\n /*\n * Which of the six task views is open is the saved VIEW'S (AGL-2617): a\n * saved view of tasks holds the task view beside the columns and the\n * sort, and the toggle below writes into it. Unset reads as \"My tasks\",\n * which is what the section opened on before views existed.\n */\n const views = useCrmSavedView({ section: 'tasks', hostId, org: orgRecord, basePath })\n const view: CrmTaskView = useMemo(() => {\n const value = views.state.filters.find((clause) => clause.field === 'view')?.value\n return CRM_TASK_VIEWS.some((option) => option.id === value)\n ? (value as CrmTaskView)\n : 'mine'\n }, [views.state.filters])\n /*\n * The grid's own Filters panel and quick search edit the view's clauses\n * (AGL-3313). \"Show\" is the task view the query serves — the hidden\n * `view` column, as the toggle stored it, with none meaning \"My tasks\" —\n * and Kind, Priority and Assignee narrow the rows it loaded.\n */\n const clauses = useMemo(\n () =>\n views.state.filters.some((clause) => clause.field === 'view')\n ? views.state.filters\n : [TASK_VIEW_DEFAULT, ...views.state.filters],\n [views.state.filters],\n )\n const setClauses = useCallback(\n (next: CrmViewFilterClause[]) =>\n views.setFilters(\n next.filter(\n (clause) => !(clause.field === 'view' && clause.value === TASK_VIEW_DEFAULT.value),\n ),\n ),\n [views.setFilters],\n )\n const gridFilter = useCrmGridFilter({\n clauses,\n onChange: setClauses,\n selectFields: ['view', 'kind', 'priority', 'assigneeUid'],\n })\n const list = useCrmTaskList({\n hostId,\n org: orgRecord,\n view,\n uid: user?.uid,\n nowMs,\n })\n const { tasks: loaded, status, fromCache, truncated, scope, orgId, readTokens } = list\n const directory = useOrgMemberDirectory(orgId)\n const filterOptions = useMemo(\n () => ({\n view: CRM_TASK_VIEWS.map((option) => ({ value: option.id, label: option.label })),\n kind: CRM_TASK_KINDS.map((kind) => ({ value: kind, label: CRM_TASK_KIND_LABELS[kind] })),\n priority: CRM_TASK_PRIORITIES.map((priority) => ({\n value: priority,\n label: CRM_TASK_PRIORITY_LABELS[priority],\n })),\n assigneeUid: directory.members.map((member) => ({\n value: member.uid,\n label: member.label,\n })),\n }),\n [directory.members],\n )\n // Every clause but the served view, and the search, over the loaded rows.\n const searchKey = gridFilter.searchWords.join(' ')\n const tasks = useMemo(\n () =>\n loaded.filter(\n (task) =>\n clauses.every(\n (clause) =>\n clause.field === 'view' || matchListFilter(task, TASK_FILTER_FIELDS, clause),\n ) && crmRowMatchesSearch(task, TASK_SEARCH_FIELDS, gridFilter.searchWords),\n ),\n // `searchKey` stands for the words, which are a new array each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [loaded, clauses, searchKey],\n )\n\n /*\n * Every record a task names, not only the one its \"For\" cell shows: the\n * export writes the contact, the company and the deal each by name, and\n * a task that names two would otherwise carry an id in one column.\n */\n const linked = useMemo(\n () =>\n tasks.flatMap((task) => [\n ...(task.contactId ? [{ kind: 'contact' as const, id: task.contactId }] : []),\n ...(task.companyId ? [{ kind: 'company' as const, id: task.companyId }] : []),\n ...(task.dealId ? [{ kind: 'deal' as const, id: task.dealId }] : []),\n ]),\n [tasks],\n )\n const nameOf = useCrmRecordNames({ orgId, groupId, org: orgRecord, records: linked })\n\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n // A view is a different set of rows; a selection made on the last one\n // would be a count over rows no longer on screen.\n useEffect(() => setSelectedIds([]), [view])\n const csvOptions: TaskCsvOptions = useMemo(\n () => ({\n assigneeEmail: (uid) => {\n const member = findOrgMember(directory.members, uid)\n return member?.email || member?.label || uid\n },\n recordName: nameOf,\n }),\n [directory.members, nameOf],\n )\n const handleExport = useCallback(() => {\n downloadTextFile('tasks.csv', 'text/csv', tasksCsv(tasks, csvOptions))\n }, [tasks, csvOptions])\n\n /*\n * List or month (AGL-2662). A layout choice over the SAME rows and the\n * same view — switching does not narrow anything and costs no read, so\n * it is a toggle rather than another saved view.\n */\n const [layout, setLayout] = useState<'list' | 'calendar'>('list')\n const [drawer, setDrawer] = useState<{ open: boolean; task: CrmTaskRow | null }>({\n open: false,\n task: null,\n })\n const [busyId, setBusyId] = useState<string | null>(null)\n\n /*\n * Tick or untick. The two directions are two different writes on purpose\n * — see the module comment — and the row is disabled while either is in\n * flight so a double-click cannot complete a task and reopen it in the\n * same breath.\n */\n const toggleDone = useCallback(\n async (task: CrmTaskRow) => {\n if (!scope || busyId) return\n setBusyId(task.$id)\n try {\n if (task.status === 'done') {\n await updateDoc(\n doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.tasks, task.$id),\n {\n status: 'open',\n completedAtMs: null,\n completedByUid: deleteField(),\n updatedAt: serverTimestamp(),\n },\n )\n enqueueSnackbar('Task reopened', { variant: 'success' })\n // A client-direct write: the records it names are told (AGL-2661).\n await refreshCrmNextActivity(user, crmTaskCallScope(hostId, orgId), [task])\n } else {\n // The route runs as the mounted site, which hears the event, or\n // at the organization level as the org — the org variant, which\n // emits on the task's own site, or nowhere for an org task.\n const callScope = crmTaskCallScope(hostId, orgId)\n if (!callScope) return\n await completeCrmTask(user, { ...callScope, taskId: task.$id })\n enqueueSnackbar('Task completed', { variant: 'success' })\n }\n } catch (cause) {\n enqueueSnackbar(\n cause instanceof Error ? cause.message : 'The task could not be updated.',\n { variant: 'warning' },\n )\n } finally {\n setBusyId(null)\n }\n },\n [scope, busyId, firestore, user, hostId, orgId, enqueueSnackbar],\n )\n\n const columns: GridColDef[] = useMemo(\n () => [\n {\n field: 'status',\n headerName: '',\n width: 56,\n sortable: false,\n filterable: false,\n renderCell: ({ row }: { row: CrmTaskRow }) => (\n <Checkbox\n size=\"small\"\n checked={row.status === 'done'}\n disabled={busyId === row.$id}\n onClick={(event) => {\n event.stopPropagation()\n void toggleDone(row)\n }}\n slotProps={{\n input: {\n 'aria-label':\n row.status === 'done'\n ? `Reopen \"${row.title}\"`\n : `Complete \"${row.title}\"`,\n },\n }}\n />\n ),\n },\n {\n field: 'title',\n headerName: 'Task',\n flex: 2,\n minWidth: 220,\n sortable: false,\n renderCell: ({ row }: { row: CrmTaskRow }) => (\n <Stack sx={{ justifyContent: 'center', height: '100%', minWidth: 0 }}>\n <Typography\n variant=\"body2\"\n noWrap\n sx={{\n textDecoration: row.status === 'done' ? 'line-through' : undefined,\n color: row.status === 'done' ? 'text.secondary' : undefined,\n }}\n >\n {row.title}\n </Typography>\n {row.notes ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {row.notes}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'kind',\n headerName: 'Kind',\n width: 120,\n sortable: false,\n renderCell: ({ row }: { row: CrmTaskRow }) => <TaskKindCell kind={row.kind} />,\n },\n {\n field: 'priority',\n headerName: 'Priority',\n width: 110,\n sortable: false,\n renderCell: ({ row }: { row: CrmTaskRow }) => (\n <TaskPriorityChip priority={row.priority} />\n ),\n },\n {\n field: 'dueAtMs',\n headerName: 'Due',\n width: 260,\n sortable: false,\n renderCell: ({ row }: { row: CrmTaskRow }) => (\n <Stack direction=\"row\" spacing={0.5} sx={{ alignItems: 'center', height: '100%' }}>\n <TaskDueText task={row} nowMs={nowMs} />\n {row.status === 'done' || !scope ? null : (\n <TaskSnoozeMenu\n dueAtMs={row.dueAtMs}\n remindAtMs={row.remindAtMs}\n target={{\n write: {\n scope,\n taskId: row.$id,\n call: crmTaskCallScope(hostId, orgId),\n links: row,\n },\n }}\n disabled={busyId === row.$id}\n />\n )}\n </Stack>\n ),\n },\n {\n field: 'assigneeUid',\n headerName: 'Assignee',\n flex: 1,\n minWidth: 140,\n sortable: false,\n valueGetter: (_value: unknown, row: CrmTaskRow) =>\n directory.nameOf(row.assigneeUid),\n renderCell: ({ row }: { row: CrmTaskRow }) => (\n <Typography variant=\"body2\" color={row.assigneeUid ? undefined : 'text.secondary'}>\n {row.assigneeUid ? directory.nameOf(row.assigneeUid) : 'Unassigned'}\n </Typography>\n ),\n },\n {\n field: 'record',\n headerName: 'For',\n flex: 1,\n minWidth: 160,\n sortable: false,\n renderCell: ({ row }: { row: CrmTaskRow }) => (\n <TaskRecordLink task={row} routes={routes} nameOf={nameOf} />\n ),\n },\n ],\n [busyId, toggleDone, nowMs, directory, routes, nameOf, scope],\n )\n /* The column and sort models are the view's (AGL-2617). */\n const filterColumns = useMemo(\n () => crmFilterColumns(columns, TASK_FILTER_FIELDS, filterOptions, TASK_FILTER_HEADERS),\n [columns, filterOptions],\n )\n const grid = useCrmViewGrid(views, filterColumns, TASK_HIDDEN_COLUMNS)\n\n return (\n <>\n <CardDisplay\n header={'Tasks'}\n help={pluginDocsHelp('crmTasks', {\n anchor: '#the-tasks-page',\n excerpt:\n 'Calls, emails, meetings and to-dos, each linked to the contact, ' +\n 'company or deal it is for. Overdue and today are read off the ' +\n 'due date as you look.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={() => setDrawer({ open: true, task: null })}\n >\n {'New task'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n {/*\n The saved view, and the clauses narrowing it as chips — \"Show\"\n among them — set in the grid's own Filters panel (AGL-3313).\n */}\n <CrmListToolbar label=\"Task filters\">\n <CrmViewsControl controller={views} allLabel=\"All tasks\" />\n <CrmFilterBar\n fields={TASK_FILTER_FIELDS}\n headers={TASK_FILTER_HEADERS}\n clauses={clauses}\n onChange={setClauses}\n options={filterOptions}\n servedField=\"view\"\n />\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={layout}\n onChange={(_event, next) => {\n if (next) setLayout(next as 'list' | 'calendar')\n }}\n aria-label=\"Task layout\"\n >\n <ToggleButton value=\"list\">{'List'}</ToggleButton>\n <ToggleButton value=\"calendar\">{'Calendar'}</ToggleButton>\n </ToggleButtonGroup>\n <Stack sx={{ flex: 1 }} />\n <TaskImportButton hostId={hostId} />\n <Button size=\"small\" onClick={handleExport} disabled={!tasks.length}>\n {'Export CSV'}\n </Button>\n </CrmListToolbar>\n {status === 'error' ? (\n <Typography variant=\"body2\" color=\"error\">\n {'The tasks could not be loaded. Reload to try again.'}\n </Typography>\n ) : status === 'success' && !loaded.length ? (\n <EmptyStateComponent\n label={EMPTY_LABEL[view]}\n description={EMPTY_COPY[view]}\n action={\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={() => setDrawer({ open: true, task: null })}\n >\n {'New task'}\n </Button>\n }\n />\n ) : (\n <>\n {truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Showing the first ${CRM_TASK_VIEW_LIMIT} — narrow the view to see the rest.`}\n </Typography>\n ) : null}\n {/*\n The calendar draws the same rows, so there is no bulk bar\n over it: a selection is a list gesture, and a month grid\n offers nowhere to make one.\n */}\n {layout === 'calendar' ? (\n <TasksCalendar\n tasks={tasks}\n truncated={truncated}\n onOpen={(task) => setDrawer({ open: true, task })}\n />\n ) : (\n <>\n <TasksBulkBar\n hostId={hostId}\n scope={scope}\n rows={tasks}\n selected={selectedIds}\n onSelectedChange={setSelectedIds}\n directory={directory}\n csv={csvOptions}\n />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={tasks}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n loading={status === 'loading'}\n onOpen={(id) => {\n const found = tasks.find((row) => row.$id === id)\n if (found) setDrawer({ open: true, task: found })\n }}\n // The panel and the search are the grid's; the section\n // answers them, the view through the query (AGL-3313).\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // Columns and sort are the view's, controlled (AGL-2617).\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={grid.onColumnVisibilityModelChange}\n sortModel={grid.sortModel}\n onSortModelChange={grid.onSortModelChange}\n />\n </CrmColumnOrderProvider>\n </>\n )}\n </>\n )}\n </Stack>\n </CardDisplay>\n <TaskEditDrawer\n open={drawer.open}\n onClose={() => setDrawer((prev) => ({ ...prev, open: false }))}\n hostId={hostId}\n org={orgRecord}\n orgId={orgId}\n scope={scope}\n readTokens={readTokens}\n task={drawer.task}\n seed={{ fromCache, unreadable: status === 'error' }}\n />\n </>\n )\n}\nTasksSection.displayName = 'TasksSection'\n\nexport default TasksSection\n"],"names":["CRM_COLLECTIONS","findOrgMember","pluginDocsHelp","CardDisplay","ListTable","useCrmSavedView","useCrmScope","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","CrmViewsControl","EmptyStateComponent","useSnackbar","useFirestore","useUser","Button","Checkbox","Stack","ToggleButton","ToggleButtonGroup","Typography","deleteField","doc","serverTimestamp","updateDoc","useCallback","useEffect","useMemo","useState","useCrmRecordNames","useCrmTaskList","useNowMs","useOrgMemberDirectory","downloadTextFile","crmRoutes","refreshCrmNextActivity","completeCrmTask","crmTaskCallScope","tasksCsv","CRM_TASK_KIND_LABELS","CRM_TASK_KINDS","CRM_TASK_PRIORITIES","CRM_TASK_PRIORITY_LABELS","CRM_TASK_VIEW_LIMIT","CRM_TASK_VIEWS","matchListFilter","crmFilterColumns","crmRowMatchesSearch","useCrmGridFilter","CrmFilterBar","CrmListToolbar","TaskDueText","TaskKindCell","TaskPriorityChip","TaskRecordLink","TaskEditDrawer","TasksCalendar","TaskImportButton","TaskSnoozeMenu","TasksBulkBar","TASK_FILTER_FIELDS","column","kind","path","operators","TASK_FILTER_HEADERS","view","priority","assigneeUid","TASK_HIDDEN_COLUMNS","TASK_VIEW_DEFAULT","field","op","value","TASK_SEARCH_FIELDS","EMPTY_LABEL","mine","overdue","today","upcoming","open","done","EMPTY_COPY","TasksSection","props","hostId","org","basePath","orgRecord","firestore","data","user","enqueueSnackbar","nowMs","routes","consentGroup","groupId","views","section","state","filters","find","clause","some","option","id","clauses","setClauses","next","setFilters","filter","gridFilter","onChange","selectFields","list","uid","tasks","loaded","status","fromCache","truncated","scope","orgId","readTokens","directory","filterOptions","map","label","members","member","searchKey","searchWords","join","task","every","linked","flatMap","contactId","companyId","dealId","nameOf","records","selectedIds","setSelectedIds","csvOptions","assigneeEmail","email","recordName","handleExport","layout","setLayout","drawer","setDrawer","busyId","setBusyId","toggleDone","$id","completedAtMs","completedByUid","updatedAt","variant","callScope","taskId","cause","Error","message","columns","headerName","width","sortable","filterable","renderCell","row","size","checked","disabled","onClick","event","stopPropagation","slotProps","input","title","flex","minWidth","sx","justifyContent","height","noWrap","textDecoration","undefined","color","notes","direction","spacing","alignItems","dueAtMs","remindAtMs","target","write","call","links","valueGetter","_value","filterColumns","grid","header","help","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","controller","allLabel","fields","headers","options","servedField","exclusive","_event","aria-label","length","description","onOpen","rows","selected","onSelectedChange","csv","columnOrder","slots","selectable","loading","found","filterMode","filterModel","onFilterModelChange","quickFilter","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","onClose","prev","seed","unreadable","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/tasks-section.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ConsolePluginPageProps,\n CRM_COLLECTIONS,\n type CrmViewFilterClause,\n findOrgMember,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport CrmViewsControl from './crm-views-control'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Checkbox,\n Stack,\n ToggleButton,\n ToggleButtonGroup,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { deleteField, doc, serverTimestamp, updateDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useCrmRecordNames } from '../hooks/use-crm-record-names'\nimport { type CrmTaskRow, useCrmTaskList, useNowMs } from '../hooks/use-crm-tasks'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport { refreshCrmNextActivity } from '../model/next-activity-api'\nimport { completeCrmTask } from '../model/task-api'\nimport { crmTaskCallScope } from '../model/task-routes'\nimport { type TaskCsvOptions, tasksCsv } from '../model/tasks-csv'\nimport {\n CRM_TASK_KIND_LABELS,\n CRM_TASK_KINDS,\n CRM_TASK_PRIORITIES,\n CRM_TASK_PRIORITY_LABELS,\n CRM_TASK_VIEW_LIMIT,\n CRM_TASK_VIEWS,\n type CrmTaskView,\n} from '../model/task-views'\nimport {\n type ListFilterField,\n matchListFilter,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { crmFilterColumns, crmRowMatchesSearch } from '../model/crm-grid-filter'\nimport { useCrmGridFilter } from '../hooks/use-crm-grid-filter'\nimport CrmFilterBar from './crm-filter-bar'\nimport { CrmListToolbar } from './crm-list-toolbar'\nimport {\n TaskDueText,\n TaskKindCell,\n TaskPriorityChip,\n TaskRecordLink,\n} from './task-cells'\nimport TaskEditDrawer from './task-edit-drawer'\nimport TasksCalendar from './tasks-calendar'\nimport { TaskImportButton } from './task-import-drawer'\nimport TaskSnoozeMenu from './task-snooze-menu'\nimport TasksBulkBar from './tasks-bulk-bar'\n\n/** What an empty view is headed, by view — \"nothing overdue\" is good news. */\n/*\n * What the tasks grid's panel offers (AGL-3313). `view` is the task view\n * the query serves — a hidden column, since it is no field of a task — and\n * the rest narrow the rows that view loaded.\n */\nconst TASK_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'view', kind: 'exact', path: 'view', operators: ['equals'] },\n { column: 'kind', kind: 'exact', path: 'kind', operators: ['equals', 'isAnyOf'] },\n { column: 'priority', kind: 'exact', path: 'priority', operators: ['equals', 'isAnyOf'] },\n { column: 'assigneeUid', kind: 'exact', path: 'assigneeUid', operators: ['equals', 'isAnyOf'] },\n]\nconst TASK_FILTER_HEADERS: Readonly<Record<string, string>> = {\n view: 'Show',\n kind: 'Kind',\n priority: 'Priority',\n assigneeUid: 'Assignee',\n}\n/** The filter-only `view` column never shows. */\nconst TASK_HIDDEN_COLUMNS: Readonly<Record<string, boolean>> = { view: false }\n/** No `view` clause is \"My tasks\", which the section opened on before views existed. */\nconst TASK_VIEW_DEFAULT: CrmViewFilterClause = { field: 'view', op: 'equals', value: 'mine' }\n/** What the quick search reads on a task. */\nconst TASK_SEARCH_FIELDS = ['title', 'notes'] as const\n\nconst EMPTY_LABEL: Record<CrmTaskView, string> = {\n mine: 'Nothing is assigned to you',\n overdue: 'Nothing is overdue',\n today: 'Nothing is due today',\n upcoming: 'Nothing is scheduled beyond today',\n open: 'No open tasks',\n done: 'No completed tasks yet',\n}\n\n/** The one sentence under it, by view. */\nconst EMPTY_COPY: Record<CrmTaskView, string> = {\n mine: 'Tasks you create are yours unless you hand them to someone.',\n overdue: 'Every dated task is on or ahead of schedule.',\n today: 'Nothing on your calendar day is owed to anyone in the CRM.',\n upcoming: 'A task with a due date past today would be listed here.',\n open: 'A task is a call, an email, a meeting or a to-do owed to a contact, a company or a deal.',\n done: 'Tasks ticked off on their record, or here, are kept for the history.',\n}\n\n/**\n * `/crm/tasks` — what the team owes the people in the CRM (AGL-2599).\n *\n * Six views over `orgs/{orgId}/crmTasks`, each a bounded listener on one of\n * the collection's indexes, chosen by a segmented control. A task hangs off\n * a contact, a company or a deal, but it is listed HERE because \"what is due\n * today\" is a question about the team's day and not about any one record;\n * the record pages carry their own `RecordTasksCard` for the other\n * direction.\n *\n * Completing goes through `crm/task-complete`, because completing fires the\n * `taskCompleted` host event and only the server can run a workflow.\n * Reopening is client-direct: undoing a tick has no side effect beyond the\n * document, and the rules gate it like any other CRM write.\n *\n * The rows are selectable, and a selection raises `TasksBulkBar` over the\n * list — Complete and Assign through their routes, the due date and the\n * delete as batched writes (AGL-2621). Export CSV beside the view control\n * writes the view through `tasksCsv()`, naming the assignee and the linked\n * records; the bar writes the same file over the selection.\n */\nexport function TasksSection(props: ConsolePluginPageProps) {\n const { hostId, org, basePath = '' } = props\n const orgRecord = org as Record<string, unknown> | undefined\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const nowMs = useNowMs()\n const routes = useMemo(() => crmRoutes(basePath), [basePath])\n // The viewing group a linked contact is named through, or `null` at the\n // organization level, where each is named through its own holder.\n const { consentGroup } = useCrmScope({ hostId, org: orgRecord })\n const groupId = consentGroup?.groupId ?? null\n\n /*\n * Which of the six task views is open is the saved VIEW'S (AGL-2617): a\n * saved view of tasks holds the task view beside the columns and the\n * sort, and the toggle below writes into it. Unset reads as \"My tasks\",\n * which is what the section opened on before views existed.\n */\n const views = useCrmSavedView({ section: 'tasks', hostId, org: orgRecord, basePath })\n const view: CrmTaskView = useMemo(() => {\n const value = views.state.filters.find((clause) => clause.field === 'view')?.value\n return CRM_TASK_VIEWS.some((option) => option.id === value)\n ? (value as CrmTaskView)\n : 'mine'\n }, [views.state.filters])\n /*\n * The grid's own Filters panel and quick search edit the view's clauses\n * (AGL-3313). \"Show\" is the task view the query serves — the hidden\n * `view` column, as the toggle stored it, with none meaning \"My tasks\" —\n * and Kind, Priority and Assignee narrow the rows it loaded.\n */\n const clauses = useMemo(\n () =>\n views.state.filters.some((clause) => clause.field === 'view')\n ? views.state.filters\n : [TASK_VIEW_DEFAULT, ...views.state.filters],\n [views.state.filters],\n )\n const setClauses = useCallback(\n (next: CrmViewFilterClause[]) =>\n views.setFilters(\n next.filter(\n (clause) => !(clause.field === 'view' && clause.value === TASK_VIEW_DEFAULT.value),\n ),\n ),\n [views.setFilters],\n )\n const gridFilter = useCrmGridFilter({\n clauses,\n onChange: setClauses,\n selectFields: ['view', 'kind', 'priority', 'assigneeUid'],\n })\n const list = useCrmTaskList({\n hostId,\n org: orgRecord,\n view,\n uid: user?.uid,\n nowMs,\n })\n const { tasks: loaded, status, fromCache, truncated, scope, orgId, readTokens } = list\n const directory = useOrgMemberDirectory(orgId)\n const filterOptions = useMemo(\n () => ({\n view: CRM_TASK_VIEWS.map((option) => ({ value: option.id, label: option.label })),\n kind: CRM_TASK_KINDS.map((kind) => ({ value: kind, label: CRM_TASK_KIND_LABELS[kind] })),\n priority: CRM_TASK_PRIORITIES.map((priority) => ({\n value: priority,\n label: CRM_TASK_PRIORITY_LABELS[priority],\n })),\n assigneeUid: directory.members.map((member) => ({\n value: member.uid,\n label: member.label,\n })),\n }),\n [directory.members],\n )\n // Every clause but the served view, and the search, over the loaded rows.\n const searchKey = gridFilter.searchWords.join(' ')\n const tasks = useMemo(\n () =>\n loaded.filter(\n (task) =>\n clauses.every(\n (clause) =>\n clause.field === 'view' || matchListFilter(task, TASK_FILTER_FIELDS, clause),\n ) && crmRowMatchesSearch(task, TASK_SEARCH_FIELDS, gridFilter.searchWords),\n ),\n // `searchKey` stands for the words, which are a new array each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [loaded, clauses, searchKey],\n )\n\n /*\n * Every record a task names, not only the one its \"For\" cell shows: the\n * export writes the contact, the company and the deal each by name, and\n * a task that names two would otherwise carry an id in one column.\n */\n const linked = useMemo(\n () =>\n tasks.flatMap((task) => [\n ...(task.contactId ? [{ kind: 'contact' as const, id: task.contactId }] : []),\n ...(task.companyId ? [{ kind: 'company' as const, id: task.companyId }] : []),\n ...(task.dealId ? [{ kind: 'deal' as const, id: task.dealId }] : []),\n ]),\n [tasks],\n )\n const nameOf = useCrmRecordNames({ orgId, groupId, org: orgRecord, records: linked })\n\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n // A view is a different set of rows; a selection made on the last one\n // would be a count over rows no longer on screen.\n useEffect(() => setSelectedIds([]), [view])\n const csvOptions: TaskCsvOptions = useMemo(\n () => ({\n assigneeEmail: (uid) => {\n const member = findOrgMember(directory.members, uid)\n return member?.email || member?.label || uid\n },\n recordName: nameOf,\n }),\n [directory.members, nameOf],\n )\n const handleExport = useCallback(() => {\n downloadTextFile('tasks.csv', 'text/csv', tasksCsv(tasks, csvOptions))\n }, [tasks, csvOptions])\n\n /*\n * List or month (AGL-2662). A layout choice over the SAME rows and the\n * same view — switching does not narrow anything and costs no read, so\n * it is a toggle rather than another saved view.\n */\n const [layout, setLayout] = useState<'list' | 'calendar'>('list')\n const [drawer, setDrawer] = useState<{ open: boolean; task: CrmTaskRow | null }>({\n open: false,\n task: null,\n })\n const [busyId, setBusyId] = useState<string | null>(null)\n\n /*\n * Tick or untick. The two directions are two different writes on purpose\n * — see the module comment — and the row is disabled while either is in\n * flight so a double-click cannot complete a task and reopen it in the\n * same breath.\n */\n const toggleDone = useCallback(\n async (task: CrmTaskRow) => {\n if (!scope || busyId) return\n setBusyId(task.$id)\n try {\n if (task.status === 'done') {\n await updateDoc(\n doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.tasks, task.$id),\n {\n status: 'open',\n completedAtMs: null,\n completedByUid: deleteField(),\n updatedAt: serverTimestamp(),\n },\n )\n enqueueSnackbar('Task reopened', { variant: 'success' })\n // A client-direct write: the records it names are told (AGL-2661).\n await refreshCrmNextActivity(user, crmTaskCallScope(hostId, orgId), [task])\n } else {\n // The route runs as the mounted site, which hears the event, or\n // at the organization level as the org — the org variant, which\n // emits on the task's own site, or nowhere for an org task.\n const callScope = crmTaskCallScope(hostId, orgId)\n if (!callScope) return\n await completeCrmTask(user, { ...callScope, taskId: task.$id })\n enqueueSnackbar('Task completed', { variant: 'success' })\n }\n } catch (cause) {\n enqueueSnackbar(\n cause instanceof Error ? cause.message : 'The task could not be updated.',\n { variant: 'warning' },\n )\n } finally {\n setBusyId(null)\n }\n },\n [scope, busyId, firestore, user, hostId, orgId, enqueueSnackbar],\n )\n\n const columns: GridColDef[] = useMemo(\n () => [\n {\n field: 'status',\n headerName: '',\n width: 56,\n sortable: false,\n filterable: false,\n renderCell: ({ row }: { row: CrmTaskRow }) => (\n <Checkbox\n size=\"small\"\n checked={row.status === 'done'}\n disabled={busyId === row.$id}\n onClick={(event) => {\n event.stopPropagation()\n void toggleDone(row)\n }}\n slotProps={{\n input: {\n 'aria-label':\n row.status === 'done'\n ? `Reopen \"${row.title}\"`\n : `Complete \"${row.title}\"`,\n },\n }}\n />\n ),\n },\n {\n field: 'title',\n headerName: 'Task',\n flex: 2,\n minWidth: 220,\n sortable: false,\n renderCell: ({ row }: { row: CrmTaskRow }) => (\n <Stack sx={{ justifyContent: 'center', height: '100%', minWidth: 0 }}>\n <Typography\n variant=\"body2\"\n noWrap\n sx={{\n textDecoration: row.status === 'done' ? 'line-through' : undefined,\n color: row.status === 'done' ? 'text.secondary' : undefined,\n }}\n >\n {row.title}\n </Typography>\n {row.notes ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {row.notes}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'kind',\n headerName: 'Kind',\n width: 120,\n sortable: false,\n renderCell: ({ row }: { row: CrmTaskRow }) => <TaskKindCell kind={row.kind} />,\n },\n {\n field: 'priority',\n headerName: 'Priority',\n width: 110,\n sortable: false,\n renderCell: ({ row }: { row: CrmTaskRow }) => (\n <TaskPriorityChip priority={row.priority} />\n ),\n },\n {\n field: 'dueAtMs',\n headerName: 'Due',\n width: 260,\n sortable: false,\n renderCell: ({ row }: { row: CrmTaskRow }) => (\n <Stack direction=\"row\" spacing={0.5} sx={{ alignItems: 'center', height: '100%' }}>\n <TaskDueText task={row} nowMs={nowMs} />\n {row.status === 'done' || !scope ? null : (\n <TaskSnoozeMenu\n dueAtMs={row.dueAtMs}\n remindAtMs={row.remindAtMs}\n target={{\n write: {\n scope,\n taskId: row.$id,\n call: crmTaskCallScope(hostId, orgId),\n links: row,\n },\n }}\n disabled={busyId === row.$id}\n />\n )}\n </Stack>\n ),\n },\n {\n field: 'assigneeUid',\n headerName: 'Assignee',\n flex: 1,\n minWidth: 140,\n sortable: false,\n valueGetter: (_value: unknown, row: CrmTaskRow) =>\n directory.nameOf(row.assigneeUid),\n renderCell: ({ row }: { row: CrmTaskRow }) => (\n <Typography variant=\"body2\" color={row.assigneeUid ? undefined : 'text.secondary'}>\n {row.assigneeUid ? directory.nameOf(row.assigneeUid) : 'Unassigned'}\n </Typography>\n ),\n },\n {\n field: 'record',\n headerName: 'For',\n flex: 1,\n minWidth: 160,\n sortable: false,\n renderCell: ({ row }: { row: CrmTaskRow }) => (\n <TaskRecordLink task={row} routes={routes} nameOf={nameOf} />\n ),\n },\n ],\n [busyId, toggleDone, nowMs, directory, routes, nameOf, scope],\n )\n /* The column and sort models are the view's (AGL-2617). */\n const filterColumns = useMemo(\n () => crmFilterColumns(columns, TASK_FILTER_FIELDS, filterOptions, TASK_FILTER_HEADERS),\n [columns, filterOptions],\n )\n const grid = useCrmViewGrid(views, filterColumns, TASK_HIDDEN_COLUMNS)\n\n return (\n <>\n <CardDisplay\n header={'Tasks'}\n help={pluginDocsHelp('crmTasks', {\n anchor: '#the-tasks-page',\n excerpt:\n 'Calls, emails, meetings and to-dos, each linked to the contact, ' +\n 'company or deal it is for. Overdue and today are read off the ' +\n 'due date as you look.',\n })}\n contentGutterX\n contentGutterY\n HeaderProps={{\n action: (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={() => setDrawer({ open: true, task: null })}\n >\n {'New task'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n {/*\n The saved view, and the clauses narrowing it as chips — \"Show\"\n among them — set in the grid's own Filters panel (AGL-3313).\n */}\n <CrmListToolbar label=\"Task filters\">\n <CrmViewsControl controller={views} allLabel=\"All tasks\" />\n <CrmFilterBar\n fields={TASK_FILTER_FIELDS}\n headers={TASK_FILTER_HEADERS}\n clauses={clauses}\n onChange={setClauses}\n options={filterOptions}\n servedField=\"view\"\n />\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={layout}\n onChange={(_event, next) => {\n if (next) setLayout(next as 'list' | 'calendar')\n }}\n aria-label=\"Task layout\"\n >\n <ToggleButton value=\"list\">{'List'}</ToggleButton>\n <ToggleButton value=\"calendar\">{'Calendar'}</ToggleButton>\n </ToggleButtonGroup>\n <Stack sx={{ flex: 1 }} />\n <TaskImportButton hostId={hostId} />\n <Button size=\"small\" onClick={handleExport} disabled={!tasks.length}>\n {'Export CSV'}\n </Button>\n </CrmListToolbar>\n {status === 'error' ? (\n <Typography variant=\"body2\" color=\"error\">\n {'The tasks could not be loaded. Reload to try again.'}\n </Typography>\n ) : (\n <>\n {truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Showing the first ${CRM_TASK_VIEW_LIMIT} — narrow the view to see the rest.`}\n </Typography>\n ) : null}\n {/*\n The calendar draws the same rows, so there is no bulk bar\n over it: a selection is a list gesture, and a month grid\n offers nowhere to make one.\n */}\n {layout === 'calendar' ? (\n <TasksCalendar\n tasks={tasks}\n truncated={truncated}\n onOpen={(task) => setDrawer({ open: true, task })}\n />\n ) : (\n <>\n <TasksBulkBar\n hostId={hostId}\n scope={scope}\n rows={tasks}\n selected={selectedIds}\n onSelectedChange={setSelectedIds}\n directory={directory}\n csv={csvOptions}\n />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rows={tasks}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n loading={status === 'loading'}\n onOpen={(id) => {\n const found = tasks.find((row) => row.$id === id)\n if (found) setDrawer({ open: true, task: found })\n }}\n /*\n * The panel and the search are the grid's; the section\n * answers them, the view through the query (AGL-3313). An\n * empty view is said inside the grid rather than instead of\n * it, so its toolbar stays to choose another view.\n */\n noRowsLabel={loaded.length ? 'No tasks match these filters' : EMPTY_LABEL[view]}\n noRowsDescription={loaded.length ? undefined : EMPTY_COPY[view]}\n noRowsAction={\n loaded.length ? undefined : (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={() => setDrawer({ open: true, task: null })}\n >\n {'New task'}\n </Button>\n )\n }\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // Columns and sort are the view's, controlled (AGL-2617).\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={grid.onColumnVisibilityModelChange}\n sortModel={grid.sortModel}\n onSortModelChange={grid.onSortModelChange}\n />\n </CrmColumnOrderProvider>\n </>\n )}\n </>\n )}\n </Stack>\n </CardDisplay>\n <TaskEditDrawer\n open={drawer.open}\n onClose={() => setDrawer((prev) => ({ ...prev, open: false }))}\n hostId={hostId}\n org={orgRecord}\n orgId={orgId}\n scope={scope}\n readTokens={readTokens}\n task={drawer.task}\n seed={{ fromCache, unreadable: status === 'error' }}\n />\n </>\n )\n}\nTasksSection.displayName = 'TasksSection'\n\nexport default TasksSection\n"],"names":["CRM_COLLECTIONS","findOrgMember","pluginDocsHelp","CardDisplay","ListTable","useCrmSavedView","useCrmScope","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","CrmViewsControl","useSnackbar","useFirestore","useUser","Button","Checkbox","Stack","ToggleButton","ToggleButtonGroup","Typography","deleteField","doc","serverTimestamp","updateDoc","useCallback","useEffect","useMemo","useState","useCrmRecordNames","useCrmTaskList","useNowMs","useOrgMemberDirectory","downloadTextFile","crmRoutes","refreshCrmNextActivity","completeCrmTask","crmTaskCallScope","tasksCsv","CRM_TASK_KIND_LABELS","CRM_TASK_KINDS","CRM_TASK_PRIORITIES","CRM_TASK_PRIORITY_LABELS","CRM_TASK_VIEW_LIMIT","CRM_TASK_VIEWS","matchListFilter","crmFilterColumns","crmRowMatchesSearch","useCrmGridFilter","CrmFilterBar","CrmListToolbar","TaskDueText","TaskKindCell","TaskPriorityChip","TaskRecordLink","TaskEditDrawer","TasksCalendar","TaskImportButton","TaskSnoozeMenu","TasksBulkBar","TASK_FILTER_FIELDS","column","kind","path","operators","TASK_FILTER_HEADERS","view","priority","assigneeUid","TASK_HIDDEN_COLUMNS","TASK_VIEW_DEFAULT","field","op","value","TASK_SEARCH_FIELDS","EMPTY_LABEL","mine","overdue","today","upcoming","open","done","EMPTY_COPY","TasksSection","props","hostId","org","basePath","orgRecord","firestore","data","user","enqueueSnackbar","nowMs","routes","consentGroup","groupId","views","section","state","filters","find","clause","some","option","id","clauses","setClauses","next","setFilters","filter","gridFilter","onChange","selectFields","list","uid","tasks","loaded","status","fromCache","truncated","scope","orgId","readTokens","directory","filterOptions","map","label","members","member","searchKey","searchWords","join","task","every","linked","flatMap","contactId","companyId","dealId","nameOf","records","selectedIds","setSelectedIds","csvOptions","assigneeEmail","email","recordName","handleExport","layout","setLayout","drawer","setDrawer","busyId","setBusyId","toggleDone","$id","completedAtMs","completedByUid","updatedAt","variant","callScope","taskId","cause","Error","message","columns","headerName","width","sortable","filterable","renderCell","row","size","checked","disabled","onClick","event","stopPropagation","slotProps","input","title","flex","minWidth","sx","justifyContent","height","noWrap","textDecoration","undefined","color","notes","direction","spacing","alignItems","dueAtMs","remindAtMs","target","write","call","links","valueGetter","_value","filterColumns","grid","header","help","anchor","excerpt","contentGutterX","contentGutterY","HeaderProps","action","controller","allLabel","fields","headers","options","servedField","exclusive","_event","aria-label","length","onOpen","rows","selected","onSelectedChange","csv","columnOrder","slots","selectable","loading","found","noRowsLabel","noRowsDescription","noRowsAction","filterMode","filterModel","onFilterModelChange","quickFilter","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","onClose","prev","seed","unreadable","displayName"],"mappings":"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"}