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

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.
Files changed (28) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/companies-section.js +97 -54
  3. package/src/lib/components/companies-section.js.map +1 -1
  4. package/src/lib/components/contacts-section.js +51 -19
  5. package/src/lib/components/contacts-section.js.map +1 -1
  6. package/src/lib/components/crm-filter-bar.d.ts +34 -32
  7. package/src/lib/components/crm-filter-bar.js +34 -235
  8. package/src/lib/components/crm-filter-bar.js.map +1 -1
  9. package/src/lib/components/crm-next-activity-column.d.ts +13 -21
  10. package/src/lib/components/crm-next-activity-column.js +21 -32
  11. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  12. package/src/lib/components/deals-section.js +65 -54
  13. package/src/lib/components/deals-section.js.map +1 -1
  14. package/src/lib/components/leads-section.js +106 -286
  15. package/src/lib/components/leads-section.js.map +1 -1
  16. package/src/lib/components/tasks-section.js +139 -34
  17. package/src/lib/components/tasks-section.js.map +1 -1
  18. package/src/lib/hooks/use-crm-grid-filter.d.ts +77 -0
  19. package/src/lib/hooks/use-crm-grid-filter.js +143 -0
  20. package/src/lib/hooks/use-crm-grid-filter.js.map +1 -0
  21. package/src/lib/model/crm-grid-filter.d.ts +64 -0
  22. package/src/lib/model/crm-grid-filter.js +144 -0
  23. package/src/lib/model/crm-grid-filter.js.map +1 -0
  24. package/src/lib/model/lead-filters.d.ts +42 -0
  25. package/src/lib/model/lead-filters.js +182 -9
  26. package/src/lib/model/lead-filters.js.map +1 -1
  27. package/src/lib/server/email-send.js +4 -2
  28. package/src/lib/server/email-send.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-crm",
3
- "version": "1.0.0-beta.182",
3
+ "version": "1.0.0-beta.184",
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.182",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.182",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.182",
31
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.182",
32
- "@aglyn/shared-ui-jsx": "1.0.0-beta.182",
33
- "@aglyn/shared-ui-next": "1.0.0-beta.182",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.182",
35
- "@aglyn/shared-util-email": "1.0.0-beta.182",
36
- "@aglyn/shared-util-http": "1.0.0-beta.182",
37
- "@aglyn/tenant-data-admin": "1.0.0-beta.182",
38
- "@aglyn/tenant-feature-instance": "1.0.0-beta.182",
39
- "@aglyn/tenant-runtime": "1.0.0-beta.182",
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",
40
40
  "@dnd-kit/core": "^6.3.1",
41
41
  "@dnd-kit/utilities": "^3.2.2",
42
42
  "@swc/helpers": "0.5.23",
@@ -21,13 +21,16 @@ import { mdiPlus } from "@aglyn/shared-data-mdi";
21
21
  import { CardDisplay, MdiIcon } from "@aglyn/shared-ui-jsx";
22
22
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
23
23
  import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
24
- import { gridFilterRequest, listFilterColumn } from "@aglyn/shared-ui-jsx/const/list-filter";
24
+ import { listFilterColumn } from "@aglyn/shared-ui-jsx/const/list-filter";
25
25
  import { useCrmSavedView } from "../hooks/use-crm-saved-view.js";
26
26
  import { useCrmViewGrid } from "../hooks/use-crm-view-grid.js";
27
27
  import { CRM_LIST_SLOTS, CrmColumnOrderProvider } from "./crm-column-menu.js";
28
- import { NoNextActivityToggle, nextActivityColumn } from "./crm-next-activity-column.js";
28
+ import { CRM_NEXT_ACTIVITY_FILTER_FIELD, CRM_NEXT_ACTIVITY_FILTER_HEADER, nextActivityColumn } from "./crm-next-activity-column.js";
29
+ import CrmFilterBar from "./crm-filter-bar.js";
30
+ import { crmFilterColumns } from "../model/crm-grid-filter.js";
31
+ import { useCrmGridFilter } from "../hooks/use-crm-grid-filter.js";
29
32
  import CrmViewsControl from "./crm-views-control.js";
30
- import { CrmListActions } from "./crm-list-toolbar.js";
33
+ import { CrmListActions, CrmListToolbar } from "./crm-list-toolbar.js";
31
34
  import { TABLE_ROW_HEIGHT } from "@aglyn/shared-ui-jsx/const/table-pagination";
32
35
  import { listFilterConstraints, usePagedCollection, useFirestore } from "@aglyn/tenant-feature-instance";
33
36
  import { Button, Chip, Stack, Typography } from "@mui/material";
@@ -46,6 +49,15 @@ import CompaniesBulkBar from "./companies-bulk-bar.js";
46
49
  import CompanyEditDrawer from "./company-edit-drawer.js";
47
50
  import { CompanyImportButton } from "./company-import-drawer.js";
48
51
  import { customFieldColumns } from "./contact-custom-columns.js";
52
+ /** What the companies grid's panel offers: the served fields, and "No next activity". */ const COMPANY_GRID_FILTER_FIELDS = [
53
+ ...COMPANY_LIST_FILTER_FIELDS,
54
+ CRM_NEXT_ACTIVITY_FILTER_FIELD
55
+ ];
56
+ const COMPANY_GRID_FILTER_HEADERS = {
57
+ name: 'Company',
58
+ ownerUid: 'Owner',
59
+ [CRM_NEXT_ACTIVITY_FILTER_FIELD.column]: CRM_NEXT_ACTIVITY_FILTER_HEADER
60
+ };
49
61
  /**
50
62
  * `/crm/companies` — the organizations behind the people (AGL-2597).
51
63
  *
@@ -87,7 +99,7 @@ import { customFieldColumns } from "./contact-custom-columns.js";
87
99
  * the same file the bar writes over the selection, headed as the import
88
100
  * reads it — and Import CSV opens the companies drawer beside it.
89
101
  */ export function CompaniesSection(props) {
90
- var _viewFilters_find;
102
+ var _viewFilters_find, _ref;
91
103
  const { hostId, org, basePath } = props;
92
104
  const routes = crmRoutes(basePath);
93
105
  const router = useRouter();
@@ -359,23 +371,60 @@ import { customFieldColumns } from "./contact-custom-columns.js";
359
371
  nowMs
360
372
  ]);
361
373
  /*
374
+ * The grid's own Filters panel and quick search edit the view's clauses
375
+ * (AGL-3313). The query serves ONE clause — a paged list has no window to
376
+ * narrow beyond the page — so the panel's clause replaces the last, and
377
+ * "No next activity", which narrows the loaded page, stands beside it.
378
+ * The search box is the name's prefix: the one text search the indexes
379
+ * serve here, so typing in it is the same clause as "Company starts with".
380
+ */ const ownerOptions = useMemo(()=>members.options.map((option)=>({
381
+ value: option.uid,
382
+ label: crmMemberPickerLabel(option)
383
+ })), [
384
+ members.options
385
+ ]);
386
+ const filterColumns = useMemo(()=>crmFilterColumns(columns, COMPANY_GRID_FILTER_FIELDS, {
387
+ ownerUid: ownerOptions
388
+ }, COMPANY_GRID_FILTER_HEADERS), [
389
+ columns,
390
+ ownerOptions
391
+ ]);
392
+ const searchWords = useMemo(()=>(filter == null ? void 0 : filter.field) === 'name' && filter.op === 'startsWith' ? [
393
+ filter.value
394
+ ] : [], [
395
+ filter
396
+ ]);
397
+ const setSearchWords = useCallback((words)=>{
398
+ const term = words.join(' ').trim();
399
+ if (term) setFilter({
400
+ field: 'name',
401
+ op: 'startsWith',
402
+ value: term
403
+ });
404
+ else if ((filter == null ? void 0 : filter.field) === 'name' && filter.op === 'startsWith') setFilter(null);
405
+ }, [
406
+ filter,
407
+ setFilter
408
+ ]);
409
+ const gridFilter = useCrmGridFilter({
410
+ clauses: viewFilters,
411
+ onChange: views.setFilters,
412
+ selectFields: [
413
+ 'ownerUid'
414
+ ],
415
+ single: true,
416
+ keepAlongside: isNoNextActivityClause,
417
+ search: {
418
+ words: searchWords,
419
+ onChange: setSearchWords
420
+ }
421
+ });
422
+ /*
362
423
  * The grid's models are the view's (AGL-2617). The filter model shows the
363
424
  * view's clause in the panel as a typed one would appear — the owner's
364
425
  * stored `equals` back as the single-select `is` the panel offers — so a
365
426
  * view opened from its address reads as filtered, not as a mystery.
366
- */ const grid = useCrmViewGrid(views, columns);
367
- const filterModel = useMemo(()=>({
368
- items: filter ? [
369
- {
370
- id: 'view',
371
- field: filter.field,
372
- operator: filter.field === 'ownerUid' && filter.op === 'equals' ? 'is' : filter.op,
373
- value: filter.value
374
- }
375
- ] : []
376
- }), [
377
- filter
378
- ]);
427
+ */ const grid = useCrmViewGrid(views, filterColumns);
379
428
  const newCompanyButton = /*#__PURE__*/ _jsx(Button, {
380
429
  size: "small",
381
430
  variant: "contained",
@@ -397,15 +446,18 @@ import { customFieldColumns } from "./contact-custom-columns.js";
397
446
  contentGutterY: true,
398
447
  contentBordered: "all",
399
448
  HeaderProps: {
449
+ // The record actions, top right and never clipped (AGL-3311).
400
450
  action: /*#__PURE__*/ _jsxs(CrmListActions, {
401
451
  children: [
402
- /*#__PURE__*/ _jsx(CrmViewsControl, {
403
- controller: views,
404
- allLabel: "All companies"
452
+ /*#__PURE__*/ _jsx(CompanyImportButton, {
453
+ hostId: hostId,
454
+ org: org
405
455
  }),
406
- /*#__PURE__*/ _jsx(NoNextActivityToggle, {
407
- filters: viewFilters,
408
- onChange: views.setFilters
456
+ /*#__PURE__*/ _jsx(Button, {
457
+ size: "small",
458
+ onClick: handleExport,
459
+ disabled: !companies.length,
460
+ children: 'Export CSV'
409
461
  }),
410
462
  newCompanyButton
411
463
  ]
@@ -415,32 +467,27 @@ import { customFieldColumns } from "./contact-custom-columns.js";
415
467
  /*#__PURE__*/ _jsxs(Stack, {
416
468
  spacing: 1.5,
417
469
  children: [
418
- /*#__PURE__*/ _jsxs(Stack, {
419
- direction: "row",
420
- spacing: 1,
421
- sx: {
422
- alignItems: 'center',
423
- flexWrap: 'wrap',
424
- rowGap: 1
425
- },
470
+ /*#__PURE__*/ _jsx(Typography, {
471
+ variant: "body2",
472
+ color: "text.secondary",
473
+ children: 'The organizations your contacts belong to. Open one to see its ' + 'people, its deals and its open tasks, or to link a contact ' + 'to it.'
474
+ }),
475
+ /*#__PURE__*/ _jsxs(CrmListToolbar, {
476
+ label: "Company filters",
426
477
  children: [
427
- /*#__PURE__*/ _jsx(Typography, {
428
- variant: "body2",
429
- color: "text.secondary",
430
- sx: {
431
- flex: 1
432
- },
433
- children: 'The organizations your contacts belong to. Open one to see its ' + 'people, its deals and its open tasks, or to link a contact ' + 'to it.'
478
+ /*#__PURE__*/ _jsx(CrmViewsControl, {
479
+ controller: views,
480
+ allLabel: "All companies"
434
481
  }),
435
- /*#__PURE__*/ _jsx(CompanyImportButton, {
436
- hostId: hostId,
437
- org: org
438
- }),
439
- /*#__PURE__*/ _jsx(Button, {
440
- size: "small",
441
- onClick: handleExport,
442
- disabled: !companies.length,
443
- children: 'Export CSV'
482
+ /*#__PURE__*/ _jsx(CrmFilterBar, {
483
+ fields: COMPANY_GRID_FILTER_FIELDS,
484
+ headers: COMPANY_GRID_FILTER_HEADERS,
485
+ clauses: viewFilters,
486
+ onChange: views.setFilters,
487
+ options: {
488
+ ownerUid: ownerOptions
489
+ },
490
+ servedField: (_ref = filter == null ? void 0 : filter.field) != null ? _ref : null
444
491
  })
445
492
  ]
446
493
  }),
@@ -475,13 +522,9 @@ import { customFieldColumns } from "./contact-custom-columns.js";
475
522
  * The grid must NOT also filter. The query answers it, so a client
476
523
  * pass could only drop rows the query already matched.
477
524
  */ filterMode: "server",
478
- filterModel: filterModel,
479
- onFilterModelChange: (model)=>{
480
- const request = gridFilterRequest(model);
481
- setFilter(request && request.field === 'ownerUid' && request.op === 'is' ? _extends({}, request, {
482
- op: 'equals'
483
- }) : request);
484
- },
525
+ filterModel: gridFilter.filterModel,
526
+ onFilterModelChange: gridFilter.onFilterModelChange,
527
+ quickFilter: true,
485
528
  // Columns and sort are the view's, controlled (AGL-2617).
486
529
  columnVisibilityModel: grid.columnVisibilityModel,
487
530
  onColumnVisibilityModelChange: grid.onColumnVisibilityModelChange,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/companies-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 AglynOrgBilling,\n CRM_COLLECTIONS,\n type CrmCompany,\n filterByNextActivity,\n isNoNextActivityClause,\n pluginDocsHelp,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n gridFilterRequest,\n listFilterColumn,\n type ListFilterRequest,\n} 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 { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport { NoNextActivityToggle, nextActivityColumn } from './crm-next-activity-column'\nimport CrmViewsControl from './crm-views-control'\nimport { CrmListActions } from './crm-list-toolbar'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport {\n listFilterConstraints,\n usePagedCollection,\n useFirestore,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport {\n getGridSingleSelectOperators,\n type GridColDef,\n} from '@mui/x-data-grid'\nimport { collection, limit, orderBy, query, where } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { COMPANY_LIST_FILTER_FIELDS } from '../constants/company-filters'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { crmVisibleToClause, useCrmScope } from '../hooks/use-crm-scope'\nimport { useOrgMemberOptions } from '../hooks/use-org-member-options'\nimport { type CompanyCsvOptions, companiesCsv } from '../model/companies-csv'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport CompaniesBulkBar from './companies-bulk-bar'\nimport CompanyEditDrawer from './company-edit-drawer'\nimport { CompanyImportButton } from './company-import-drawer'\nimport { customFieldColumns } from './contact-custom-columns'\n\nexport interface CompaniesSectionProps {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling>\n /** The CRM hub URL, which every company route hangs beneath. */\n basePath: string\n}\n\ntype CompanyRow = Partial<CrmCompany> & { $id: string; updatedAt?: any }\n\n/**\n * `/crm/companies` — the organizations behind the people (AGL-2597).\n *\n * A section of its own rather than a column on the contact list, because a\n * company is known by several contacts and carries records of its own: a\n * domain the email suggestion keys on, an address, an owner, and the deals\n * and tasks filed against it. The row opens `/crm/companies/{id}`, where all\n * of that lives; this list is the cheaper surface and reads nothing a row\n * does not show.\n *\n * ## What the listener carries\n *\n * Paged and ordered by the server — `updatedAt desc`, so a company somebody\n * just touched is on page one — and SCOPED by the same `visibleTo\n * array-contains-any` predicate the contact list runs. That predicate is what\n * the rules evaluate: a filtered query is provable per document, and an\n * unfiltered one is refused rather than quietly returning the whole org's\n * accounts to a site that may see one client's.\n *\n * ## What a filter may be\n *\n * The scope predicate is an array clause, and Firestore carries one per\n * query, so the word-prefix search the contact list offers cannot run here.\n * The name is a prefix range and an exact match instead, and the owner is a\n * choice from the roster — each reaching the whole collection, which is the\n * property a search must have: a company on page four is found, not reported\n * missing. `COMPANY_LIST_FILTER_FIELDS` states what the indexes serve.\n *\n * ## Creating is a drawer\n *\n * \"New company\" opens the same eight-field drawer the company's page edits\n * with, and the new record's page opens when it is saved. A form above a\n * list has nowhere to grow, and this one has an address in it.\n *\n * ## Selection, the bar and the file (AGL-2621)\n *\n * The rows are selectable, and a selection raises `CompaniesBulkBar` over\n * the table. Export CSV writes the loaded page through `companiesCsv()` —\n * the same file the bar writes over the selection, headed as the import\n * reads it — and Import CSV opens the companies drawer beside it.\n */\nexport function CompaniesSection(props: CompaniesSectionProps) {\n const { hostId, org, basePath } = props\n const routes = crmRoutes(basePath)\n const router = useRouter()\n const firestore = useFirestore()\n const { scope, orgId, visibleTo } = useCrmScope({ hostId, org })\n /*\n * The team, read once for this surface: the Owner column names a person\n * from a uid, and the create drawer picks from the same roster. One fetch\n * serves both, which is why the drawer takes it as a prop.\n */\n const members = useOrgMemberOptions(orgId)\n // The org's company fields, for the optional columns below (AGL-2661).\n const companyFields = useContactFieldDefinitions(orgId, 'company')\n\n /*\n * The column filter is the saved VIEW'S first clause (AGL-2617): this\n * grid's panel holds one, and a view of companies holds that one beside\n * the columns and the sort. Declared BEFORE the listener that reads it,\n * as the filter always was — the query is rebuilt from it.\n */\n const views = useCrmSavedView({ section: 'companies', hostId, org, basePath })\n /*\n * ...beside the \"No next activity\" clause (AGL-2661), which is not a\n * query the listener can run — absence has no index — and narrows the\n * loaded page below instead. The grid's one clause is the first that is\n * not it, and setting the grid's clause keeps it.\n */\n const viewFilters = views.state.filters\n const filter: ListFilterRequest | null =\n viewFilters.find((clause) => !isNoNextActivityClause(clause)) ?? null\n const setFilter = useCallback(\n (request: ListFilterRequest | null) =>\n views.setFilters([\n ...(request ? [request] : []),\n ...viewFilters.filter(isNoNextActivityClause),\n ]),\n [views.setFilters, viewFilters],\n )\n\n const {\n rows: loaded,\n status,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<CompanyRow>(\n (pageLimit) => {\n if (!scope) return null\n const constraints = listFilterConstraints(\n COMPANY_LIST_FILTER_FIELDS,\n filter,\n )\n return query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies),\n // Absent at the organization level, where the tokens are `null` (AGL-2630).\n ...crmVisibleToClause(visibleTo),\n ...(constraints ?? [orderBy('updatedAt', 'desc')]),\n limit(pageLimit),\n )\n },\n [firestore, scope, visibleTo, filter],\n { idField: '$id' },\n )\n const companies = useMemo(\n () => filterByNextActivity(loaded, viewFilters),\n [loaded, viewFilters],\n )\n const nowMs = useMemo(() => Date.now(), [])\n\n const [createOpen, setCreateOpen] = useState(false)\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n /*\n * The owner is written by ADDRESS, because the companies import resolves\n * an owner by email; the same options reach the bar, so the page's file\n * and the selection's are one format.\n */\n const csvOptions: CompanyCsvOptions = useMemo(\n () => ({ ownerEmail: members.emailFor }),\n [members.emailFor],\n )\n const handleExport = useCallback(() => {\n downloadTextFile('companies.csv', 'text/csv', companiesCsv(companies, csvOptions))\n }, [companies, csvOptions])\n const openCompany = useCallback(\n (id: string) => router.push(routes.company(id)),\n [router, routes],\n )\n\n const columns: GridColDef[] = useMemo(\n () => [\n {\n field: 'name',\n headerName: 'Company',\n flex: 1.6,\n minWidth: 240,\n ...listFilterColumn(COMPANY_LIST_FILTER_FIELDS, 'name'),\n valueGetter: (_value, row: CompanyRow) => String(row.name ?? ''),\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Stack\n sx={{ justifyContent: 'center', height: '100%', lineHeight: 1.25 }}\n >\n <Typography variant=\"body2\" sx={{ lineHeight: 1.25 }}>\n {row.name}\n </Typography>\n {row.industry ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ lineHeight: 1.25 }}\n noWrap\n >\n {row.industry}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'domain',\n headerName: 'Domain',\n flex: 1,\n minWidth: 160,\n // Stored normalized, so it could be filtered exactly — but a domain\n // lookup is what the company suggestion on a contact does, and a\n // filter here would need an index of its own for one more way to\n // find a row the name filter already finds.\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) => String(row.domain ?? ''),\n renderCell: ({ row }: { row: CompanyRow }) =>\n row.domain ? (\n <Typography variant=\"body2\">{row.domain}</Typography>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n field: 'contactsCount',\n headerName: 'Contacts',\n width: 110,\n align: 'right',\n headerAlign: 'right',\n /*\n * The denormalized count every link moves (AGL-2613) — a stored\n * number, so a page of companies costs no read per row. Absent on a\n * company nobody has linked since the counter existed, which reads\n * as zero; the company's own page takes the live aggregate.\n */\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) => Number(row.contactsCount ?? 0),\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Typography\n variant=\"body2\"\n color={row.contactsCount ? 'text.primary' : 'text.secondary'}\n >\n {Number(row.contactsCount ?? 0).toLocaleString()}\n </Typography>\n ),\n },\n {\n field: 'tags',\n headerName: 'Tags',\n flex: 1,\n minWidth: 140,\n // Written by the drawer and the bulk bar (AGL-2621); read here, not\n // filtered — a tag filter would need an `array-contains` index of\n // its own beside the scope predicate.\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) => (row.tags ?? []).join(', '),\n renderCell: ({ row }: { row: CompanyRow }) =>\n row.tags?.length ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ alignItems: 'center', overflow: 'hidden' }}>\n {row.tags.slice(0, 3).map((tag) => (\n <Chip key={tag} size=\"small\" variant=\"outlined\" label={tag} />\n ))}\n {row.tags.length > 3 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`+${row.tags.length - 3}`}\n </Typography>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 1,\n minWidth: 160,\n sortable: false,\n /*\n * A CHOICE from the roster, not a text box for a uid. The grid's\n * single-select `is` operator is what the panel offers; the handler\n * below maps it onto the declared field's `equals`, which the\n * translator turns into the equality the index serves.\n */\n type: 'singleSelect',\n valueOptions: members.options.map((option) => ({\n value: option.uid,\n label: crmMemberPickerLabel(option),\n })),\n filterable: members.options.length > 0,\n filterOperators: getGridSingleSelectOperators().filter(\n (operator) => operator.value === 'is',\n ),\n valueGetter: (_value, row: CompanyRow) => String(row.ownerUid ?? ''),\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Typography\n variant=\"body2\"\n color={row.ownerUid ? 'text.primary' : 'text.secondary'}\n >\n {row.ownerUid\n ? members.ready\n ? members.labelFor(row.ownerUid)\n : '…'\n : '—'}\n </Typography>\n ),\n },\n {\n field: 'updatedAt',\n headerName: 'Updated',\n flex: 0.8,\n minWidth: 140,\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) =>\n row.updatedAt?.seconds ? new Date(row.updatedAt.seconds * 1000) : null,\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {row.updatedAt?.seconds\n ? new Date(row.updatedAt.seconds * 1000).toLocaleDateString()\n : '—'}\n </Typography>\n ),\n },\n // When the earliest open task against the company is due (AGL-2661).\n nextActivityColumn(nowMs),\n // The org's company fields as optional columns (AGL-2661), read off\n // the row's own `custom` map the way the contacts list reads its own.\n ...customFieldColumns(companyFields.active),\n ],\n [members, companyFields.active, nowMs],\n )\n\n /*\n * The grid's models are the view's (AGL-2617). The filter model shows the\n * view's clause in the panel as a typed one would appear — the owner's\n * stored `equals` back as the single-select `is` the panel offers — so a\n * view opened from its address reads as filtered, not as a mystery.\n */\n const grid = useCrmViewGrid(views, columns)\n const filterModel = useMemo(\n () => ({\n items: filter\n ? [\n {\n id: 'view',\n field: filter.field,\n operator:\n filter.field === 'ownerUid' && filter.op === 'equals' ? 'is' : filter.op,\n value: filter.value,\n },\n ]\n : [],\n }),\n [filter],\n )\n\n const newCompanyButton = (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={!scope}\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n onClick={() => setCreateOpen(true)}\n >\n {'New company'}\n </Button>\n )\n\n return (\n <CardDisplay\n header={'Companies'}\n help={pluginDocsHelp('companies', { anchor: '#the-companies-list' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\n <CrmListActions>\n <CrmViewsControl controller={views} allLabel=\"All companies\" />\n <NoNextActivityToggle filters={viewFilters} onChange={views.setFilters} />\n {newCompanyButton}\n </CrmListActions>\n ),\n }}\n >\n <Stack spacing={1.5}>\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 organizations your contacts belong to. Open one to see its ' +\n 'people, its deals and its open tasks, or to link a contact ' +\n 'to it.'}\n </Typography>\n <CompanyImportButton hostId={hostId} org={org} />\n <Button size=\"small\" onClick={handleExport} disabled={!companies.length}>\n {'Export CSV'}\n </Button>\n </Stack>\n <CompaniesBulkBar\n hostId={hostId}\n scope={scope}\n rows={companies}\n selected={selectedIds}\n onSelectedChange={setSelectedIds}\n members={members}\n csv={csvOptions}\n />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rowHeight={TABLE_ROW_HEIGHT}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n rows={companies}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n noRowsLabel=\"No companies yet\"\n noRowsDescription=\"A company groups the contacts who work at one business, with its domain, owner and address. Create the first one, or link a contact to a company from their page.\"\n noRowsAction={newCompanyButton}\n onOpen={(id) => openCompany(String(id))}\n // An empty table while the read is in flight reads as \"you have\n // none\" rather than \"these are on their way\".\n loading={!scope || status === 'loading'}\n /*\n * The grid must NOT also filter. The query answers it, so a client\n * pass could only drop rows the query already matched.\n */\n filterMode=\"server\"\n filterModel={filterModel}\n onFilterModelChange={(model) => {\n const request = gridFilterRequest(model)\n setFilter(\n request && request.field === 'ownerUid' && request.op === 'is'\n ? { ...request, op: 'equals' }\n : request,\n )\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 // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n </CrmColumnOrderProvider>\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={companies.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </Stack>\n {/*\n Mounted only while it is open, so the list pays for none of the\n drawer's state — and a fresh mount is what seeds an empty form.\n */}\n {createOpen ? (\n <CompanyEditDrawer\n open\n onClose={() => setCreateOpen(false)}\n hostId={hostId}\n org={org}\n members={members}\n onSaved={openCompany}\n />\n ) : null}\n </CardDisplay>\n )\n}\nCompaniesSection.displayName = 'CompaniesSection'\n\nexport default CompaniesSection\n"],"names":["CRM_COLLECTIONS","filterByNextActivity","isNoNextActivityClause","pluginDocsHelp","crmMemberPickerLabel","mdiPlus","CardDisplay","MdiIcon","ListPagination","ListTable","gridFilterRequest","listFilterColumn","useCrmSavedView","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","NoNextActivityToggle","nextActivityColumn","CrmViewsControl","CrmListActions","TABLE_ROW_HEIGHT","listFilterConstraints","usePagedCollection","useFirestore","Button","Chip","Stack","Typography","getGridSingleSelectOperators","collection","limit","orderBy","query","useRouter","useCallback","useMemo","useState","COMPANY_LIST_FILTER_FIELDS","useContactFieldDefinitions","crmVisibleToClause","useCrmScope","useOrgMemberOptions","companiesCsv","downloadTextFile","crmRoutes","CompaniesBulkBar","CompanyEditDrawer","CompanyImportButton","customFieldColumns","CompaniesSection","props","viewFilters","hostId","org","basePath","routes","router","firestore","scope","orgId","visibleTo","members","companyFields","views","section","state","filters","filter","find","clause","setFilter","request","setFilters","rows","loaded","status","hasMore","page","setPage","pageSize","setPageSize","pageLimit","constraints","companies","idField","nowMs","Date","now","createOpen","setCreateOpen","selectedIds","setSelectedIds","csvOptions","ownerEmail","emailFor","handleExport","openCompany","id","push","company","columns","field","headerName","flex","minWidth","valueGetter","_value","row","String","name","renderCell","sx","justifyContent","height","lineHeight","variant","industry","color","noWrap","filterable","sortable","domain","width","align","headerAlign","Number","contactsCount","toLocaleString","tags","join","length","direction","spacing","alignItems","overflow","slice","map","tag","size","label","type","valueOptions","options","option","value","uid","filterOperators","operator","ownerUid","ready","labelFor","updatedAt","seconds","toLocaleDateString","active","grid","filterModel","items","op","newCompanyButton","disabled","startIcon","path","onClick","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","controller","allLabel","onChange","flexWrap","rowGap","selected","onSelectedChange","csv","columnOrder","rowHeight","slots","selectable","noRowsLabel","noRowsDescription","noRowsAction","onOpen","loading","filterMode","onFilterModelChange","model","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","hideFooter","rowCount","onPageChange","onPageSizeChange","open","onClose","onSaved","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAEEA,eAAe,EAEfC,oBAAoB,EACpBC,sBAAsB,EACtBC,cAAc,EACdC,oBAAoB,QACf,eAAc;AACrB,SAASC,OAAO,QAAQ,yBAAwB;AAChD,SAASC,WAAW,EAAEC,OAAO,QAAQ,uBAAsB;AAC3D,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,SAASC,SAAS,QAAQ,uDAAsD;AAChF,SACEC,iBAAiB,EACjBC,gBAAgB,QAEX,yCAAwC;AAC/C,SAASC,eAAe,QAAQ,iCAA6B;AAC7D,SAASC,cAAc,QAAQ,gCAA4B;AAC3D,SAASC,cAAc,EAAEC,sBAAsB,QAAQ,uBAAmB;AAC1E,SAASC,oBAAoB,EAAEC,kBAAkB,QAAQ,gCAA4B;AACrF,OAAOC,qBAAqB,yBAAqB;AACjD,SAASC,cAAc,QAAQ,wBAAoB;AACnD,SAASC,gBAAgB,QAAQ,8CAA6C;AAC9E,SACEC,qBAAqB,EACrBC,kBAAkB,EAClBC,YAAY,QACP,iCAAgC;AACvC,SAASC,MAAM,EAAEC,IAAI,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AAC/D,SACEC,4BAA4B,QAEvB,mBAAkB;AACzB,SAASC,UAAU,EAAEC,KAAK,EAAEC,OAAO,EAAEC,KAAK,QAAe,qBAAoB;AAC7E,SAASC,SAAS,QAAQ,kBAAiB;AAC3C,SAASC,WAAW,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AACtD,SAASC,0BAA0B,QAAQ,kCAA8B;AACzE,SAASC,0BAA0B,QAAQ,4CAAwC;AACnF,SAASC,kBAAkB,EAAEC,WAAW,QAAQ,4BAAwB;AACxE,SAASC,mBAAmB,QAAQ,qCAAiC;AACrE,SAAiCC,YAAY,QAAQ,4BAAwB;AAC7E,SAASC,gBAAgB,QAAQ,2BAAuB;AACxD,SAASC,SAAS,QAAQ,yBAAqB;AAC/C,OAAOC,sBAAsB,0BAAsB;AACnD,OAAOC,uBAAuB,2BAAuB;AACrD,SAASC,mBAAmB,QAAQ,6BAAyB;AAC7D,SAASC,kBAAkB,QAAQ,8BAA0B;AAY7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCC,GACD,OAAO,SAASC,iBAAiBC,KAA4B;QA8BzDC;IA7BF,MAAM,EAAEC,MAAM,EAAEC,GAAG,EAAEC,QAAQ,EAAE,GAAGJ;IAClC,MAAMK,SAASX,UAAUU;IACzB,MAAME,SAASvB;IACf,MAAMwB,YAAYlC;IAClB,MAAM,EAAEmC,KAAK,EAAEC,KAAK,EAAEC,SAAS,EAAE,GAAGpB,YAAY;QAAEY;QAAQC;IAAI;IAC9D;;;;GAIC,GACD,MAAMQ,UAAUpB,oBAAoBkB;IACpC,uEAAuE;IACvE,MAAMG,gBAAgBxB,2BAA2BqB,OAAO;IAExD;;;;;GAKC,GACD,MAAMI,QAAQnD,gBAAgB;QAAEoD,SAAS;QAAaZ;QAAQC;QAAKC;IAAS;IAC5E;;;;;GAKC,GACD,MAAMH,cAAcY,MAAME,KAAK,CAACC,OAAO;IACvC,MAAMC,UACJhB,oBAAAA,YAAYiB,IAAI,CAAC,CAACC,SAAW,CAACnE,uBAAuBmE,oBAArDlB,oBAAiE;IACnE,MAAMmB,YAAYpC,YAChB,CAACqC,UACCR,MAAMS,UAAU,CAAC;eACXD,UAAU;gBAACA;aAAQ,GAAG,EAAE;eACzBpB,YAAYgB,MAAM,CAACjE;SACvB,GACH;QAAC6D,MAAMS,UAAU;QAAErB;KAAY;IAGjC,MAAM,EACJsB,MAAMC,MAAM,EACZC,MAAM,EACNC,OAAO,EACPC,IAAI,EACJC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACZ,GAAG1D,mBACF,CAAC2D;QACC,IAAI,CAACvB,OAAO,OAAO;QACnB,MAAMwB,cAAc7D,sBAClBgB,4BACA8B;QAEF,OAAOnC,MACLH,WAAW4B,WAAWC,KAAK,CAAC,EAAE,EAAEA,KAAK,CAAC,EAAE,EAAE1D,gBAAgBmF,SAAS,GACnE,4EAA4E;WACzE5C,mBAAmBqB,eAClBsB,sBAAAA,cAAe;YAACnD,QAAQ,aAAa;SAAQ,EACjDD,MAAMmD;IAEV,GACA;QAACxB;QAAWC;QAAOE;QAAWO;KAAO,EACrC;QAAEiB,SAAS;IAAM;IAEnB,MAAMD,YAAYhD,QAChB,IAAMlC,qBAAqByE,QAAQvB,cACnC;QAACuB;QAAQvB;KAAY;IAEvB,MAAMkC,QAAQlD,QAAQ,IAAMmD,KAAKC,GAAG,IAAI,EAAE;IAE1C,MAAM,CAACC,YAAYC,cAAc,GAAGrD,SAAS;IAC7C,MAAM,CAACsD,aAAaC,eAAe,GAAGvD,SAAmB,EAAE;IAC3D;;;;GAIC,GACD,MAAMwD,aAAgCzD,QACpC,IAAO,CAAA;YAAE0D,YAAYhC,QAAQiC,QAAQ;QAAC,CAAA,GACtC;QAACjC,QAAQiC,QAAQ;KAAC;IAEpB,MAAMC,eAAe7D,YAAY;QAC/BS,iBAAiB,iBAAiB,YAAYD,aAAayC,WAAWS;IACxE,GAAG;QAACT;QAAWS;KAAW;IAC1B,MAAMI,cAAc9D,YAClB,CAAC+D,KAAezC,OAAO0C,IAAI,CAAC3C,OAAO4C,OAAO,CAACF,MAC3C;QAACzC;QAAQD;KAAO;IAGlB,MAAM6C,UAAwBjE,QAC5B,IAAM;YACJ;gBACEkE,OAAO;gBACPC,YAAY;gBACZC,MAAM;gBACNC,UAAU;eACP7F,iBAAiB0B,4BAA4B;gBAChDoE,aAAa,CAACC,QAAQC;wBAA2BA;2BAAPC,QAAOD,YAAAA,IAAIE,IAAI,YAARF,YAAY;;gBAC7DG,YAAY,CAAC,EAAEH,GAAG,EAAuB,iBACvC,MAACjF;wBACCqF,IAAI;4BAAEC,gBAAgB;4BAAUC,QAAQ;4BAAQC,YAAY;wBAAK;;0CAEjE,KAACvF;gCAAWwF,SAAQ;gCAAQJ,IAAI;oCAAEG,YAAY;gCAAK;0CAChDP,IAAIE,IAAI;;4BAEVF,IAAIS,QAAQ,iBACX,KAACzF;gCACCwF,SAAQ;gCACRE,OAAM;gCACNN,IAAI;oCAAEG,YAAY;gCAAK;gCACvBI,MAAM;0CAELX,IAAIS,QAAQ;iCAEb;;;;YAIV;gBACEf,OAAO;gBACPC,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACV,oEAAoE;gBACpE,iEAAiE;gBACjE,iEAAiE;gBACjE,4CAA4C;gBAC5Ce,YAAY;gBACZC,UAAU;gBACVf,aAAa,CAACC,QAAQC;wBAA2BA;2BAAPC,QAAOD,cAAAA,IAAIc,MAAM,YAAVd,cAAc;;gBAC/DG,YAAY,CAAC,EAAEH,GAAG,EAAuB,GACvCA,IAAIc,MAAM,iBACR,KAAC9F;wBAAWwF,SAAQ;kCAASR,IAAIc,MAAM;uCAEvC,KAAC9F;wBAAWwF,SAAQ;wBAAQE,OAAM;kCAC/B;;YAGT;YACA;gBACEhB,OAAO;gBACPC,YAAY;gBACZoB,OAAO;gBACPC,OAAO;gBACPC,aAAa;gBACb;;;;;SAKC,GACDL,YAAY;gBACZC,UAAU;gBACVf,aAAa,CAACC,QAAQC;wBAA2BA;2BAAPkB,QAAOlB,qBAAAA,IAAImB,aAAa,YAAjBnB,qBAAqB;;gBACtEG,YAAY,CAAC,EAAEH,GAAG,EAAuB;wBAK7BA;yCAJV,KAAChF;wBACCwF,SAAQ;wBACRE,OAAOV,IAAImB,aAAa,GAAG,iBAAiB;kCAE3CD,QAAOlB,qBAAAA,IAAImB,aAAa,YAAjBnB,qBAAqB,GAAGoB,cAAc;;;YAGpD;YACA;gBACE1B,OAAO;gBACPC,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACV,oEAAoE;gBACpE,kEAAkE;gBAClE,sCAAsC;gBACtCe,YAAY;gBACZC,UAAU;gBACVf,aAAa,CAACC,QAAQC;wBAAqBA;2BAAD,EAACA,YAAAA,IAAIqB,IAAI,YAARrB,YAAY,EAAE,EAAEsB,IAAI,CAAC;;gBAChEnB,YAAY,CAAC,EAAEH,GAAG,EAAuB;wBACvCA;2BAAAA,EAAAA,YAAAA,IAAIqB,IAAI,qBAARrB,UAAUuB,MAAM,kBACd,MAACxG;wBAAMyG,WAAU;wBAAMC,SAAS;wBAAKrB,IAAI;4BAAEsB,YAAY;4BAAUC,UAAU;wBAAS;;4BACjF3B,IAAIqB,IAAI,CAACO,KAAK,CAAC,GAAG,GAAGC,GAAG,CAAC,CAACC,oBACzB,KAAChH;oCAAeiH,MAAK;oCAAQvB,SAAQ;oCAAWwB,OAAOF;mCAA5CA;4BAEZ9B,IAAIqB,IAAI,CAACE,MAAM,GAAG,kBACjB,KAACvG;gCAAWwF,SAAQ;gCAAUE,OAAM;0CACjC,CAAC,CAAC,EAAEV,IAAIqB,IAAI,CAACE,MAAM,GAAG,GAAG;iCAE1B;;uCAGN,KAACvG;wBAAWwF,SAAQ;wBAAQE,OAAM;kCAC/B;;;YAGT;YACA;gBACEhB,OAAO;gBACPC,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACVgB,UAAU;gBACV;;;;;SAKC,GACDoB,MAAM;gBACNC,cAAchF,QAAQiF,OAAO,CAACN,GAAG,CAAC,CAACO,SAAY,CAAA;wBAC7CC,OAAOD,OAAOE,GAAG;wBACjBN,OAAOvI,qBAAqB2I;oBAC9B,CAAA;gBACAxB,YAAY1D,QAAQiF,OAAO,CAACZ,MAAM,GAAG;gBACrCgB,iBAAiBtH,+BAA+BuC,MAAM,CACpD,CAACgF,WAAaA,SAASH,KAAK,KAAK;gBAEnCvC,aAAa,CAACC,QAAQC;wBAA2BA;2BAAPC,QAAOD,gBAAAA,IAAIyC,QAAQ,YAAZzC,gBAAgB;;gBACjEG,YAAY,CAAC,EAAEH,GAAG,EAAuB,iBACvC,KAAChF;wBACCwF,SAAQ;wBACRE,OAAOV,IAAIyC,QAAQ,GAAG,iBAAiB;kCAEtCzC,IAAIyC,QAAQ,GACTvF,QAAQwF,KAAK,GACXxF,QAAQyF,QAAQ,CAAC3C,IAAIyC,QAAQ,IAC7B,MACF;;YAGV;YACA;gBACE/C,OAAO;gBACPC,YAAY;gBACZC,MAAM;gBACNC,UAAU;gBACVe,YAAY;gBACZC,UAAU;gBACVf,aAAa,CAACC,QAAQC;wBACpBA;2BAAAA,EAAAA,iBAAAA,IAAI4C,SAAS,qBAAb5C,eAAe6C,OAAO,IAAG,IAAIlE,KAAKqB,IAAI4C,SAAS,CAACC,OAAO,GAAG,QAAQ;;gBACpE1C,YAAY,CAAC,EAAEH,GAAG,EAAuB;wBAEpCA;yCADH,KAAChF;wBAAWwF,SAAQ;wBAAUE,OAAM;kCACjCV,EAAAA,iBAAAA,IAAI4C,SAAS,qBAAb5C,eAAe6C,OAAO,IACnB,IAAIlE,KAAKqB,IAAI4C,SAAS,CAACC,OAAO,GAAG,MAAMC,kBAAkB,KACzD;;;YAGV;YACA,qEAAqE;YACrExI,mBAAmBoE;YACnB,oEAAoE;YACpE,sEAAsE;eACnErC,mBAAmBc,cAAc4F,MAAM;SAC3C,EACD;QAAC7F;QAASC,cAAc4F,MAAM;QAAErE;KAAM;IAGxC;;;;;GAKC,GACD,MAAMsE,OAAO9I,eAAekD,OAAOqC;IACnC,MAAMwD,cAAczH,QAClB,IAAO,CAAA;YACL0H,OAAO1F,SACH;gBACE;oBACE8B,IAAI;oBACJI,OAAOlC,OAAOkC,KAAK;oBACnB8C,UACEhF,OAAOkC,KAAK,KAAK,cAAclC,OAAO2F,EAAE,KAAK,WAAW,OAAO3F,OAAO2F,EAAE;oBAC1Ed,OAAO7E,OAAO6E,KAAK;gBACrB;aACD,GACD,EAAE;QACR,CAAA,GACA;QAAC7E;KAAO;IAGV,MAAM4F,iCACJ,KAACvI;QACCkH,MAAK;QACLvB,SAAQ;QACRE,OAAM;QACN2C,UAAU,CAACtG;QACXuG,yBAAW,KAAC1J;YAAQ2J,MAAM7J,QAAQ6J,IAAI;YAAExB,MAAM;;QAC9CyB,SAAS,IAAM1E,cAAc;kBAE5B;;IAIL,qBACE,MAACnF;QACC8J,QAAQ;QACRC,MAAMlK,eAAe,aAAa;YAAEmK,QAAQ;QAAsB;QAClEC,cAAc;QACdC,cAAc;QACdC,iBAAgB;QAChBC,aAAa;YACXC,sBACE,MAACxJ;;kCACC,KAACD;wBAAgB0J,YAAY7G;wBAAO8G,UAAS;;kCAC7C,KAAC7J;wBAAqBkD,SAASf;wBAAa2H,UAAU/G,MAAMS,UAAU;;oBACrEuF;;;QAGP;;0BAEA,MAACrI;gBAAM0G,SAAS;;kCACd,MAAC1G;wBACCyG,WAAU;wBACVC,SAAS;wBACTrB,IAAI;4BAAEsB,YAAY;4BAAU0C,UAAU;4BAAQC,QAAQ;wBAAE;;0CAExD,KAACrJ;gCAAWwF,SAAQ;gCAAQE,OAAM;gCAAiBN,IAAI;oCAAER,MAAM;gCAAE;0CAC9D,oEACC,gEACA;;0CAEJ,KAACxD;gCAAoBK,QAAQA;gCAAQC,KAAKA;;0CAC1C,KAAC7B;gCAAOkH,MAAK;gCAAQyB,SAASpE;gCAAciE,UAAU,CAAC7E,UAAU+C,MAAM;0CACpE;;;;kCAGL,KAACrF;wBACCO,QAAQA;wBACRM,OAAOA;wBACPe,MAAMU;wBACN8F,UAAUvF;wBACVwF,kBAAkBvF;wBAClB9B,SAASA;wBACTsH,KAAKvF;;kCAEP,KAAC7E;wBAAuBiI,OAAOW,KAAKyB,WAAW;kCAC7C,cAAA,KAAC3K;4BACC4K,WAAWjK;4BACXgF,SAASuD,KAAKvD,OAAO;4BACrBkF,OAAOxK;4BACP2D,MAAMU;4BACNoG,YAAY;gCAAEN,UAAUvF;gCAAaoF,UAAUnF;4BAAe;4BAC9D6F,aAAY;4BACZC,mBAAkB;4BAClBC,cAAc3B;4BACd4B,QAAQ,CAAC1F,KAAOD,YAAYY,OAAOX;4BACnC,gEAAgE;4BAChE,8CAA8C;4BAC9C2F,SAAS,CAAClI,SAASiB,WAAW;4BAC9B;;;aAGC,GACDkH,YAAW;4BACXjC,aAAaA;4BACbkC,qBAAqB,CAACC;gCACpB,MAAMxH,UAAU7D,kBAAkBqL;gCAClCzH,UACEC,WAAWA,QAAQ8B,KAAK,KAAK,cAAc9B,QAAQuF,EAAE,KAAK,OACtD,aAAKvF;oCAASuF,IAAI;qCAClBvF;4BAER;4BACA,0DAA0D;4BAC1DyH,uBAAuBrC,KAAKqC,qBAAqB;4BACjDC,+BAA+BtC,KAAKsC,6BAA6B;4BACjEC,WAAWvC,KAAKuC,SAAS;4BACzBC,mBAAmBxC,KAAKwC,iBAAiB;4BACzC,8DAA8D;4BAC9DC,UAAU;;;kCAGd,KAAC5L;wBACCqE,MAAMA;wBACNE,UAAUA;wBACVsH,UAAUlH,UAAU+C,MAAM;wBAC1BtD,SAASA;wBACT0H,cAAcxH;wBACdyH,kBAAkBvH;;;;YAOrBQ,2BACC,KAAC1C;gBACC0J,IAAI;gBACJC,SAAS,IAAMhH,cAAc;gBAC7BrC,QAAQA;gBACRC,KAAKA;gBACLQ,SAASA;gBACT6I,SAAS1G;iBAET;;;AAGV;AACA/C,iBAAiB0J,WAAW,GAAG;AAE/B,eAAe1J,iBAAgB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/companies-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 AglynOrgBilling,\n CRM_COLLECTIONS,\n type CrmCompany,\n filterByNextActivity,\n isNoNextActivityClause,\n pluginDocsHelp,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n listFilterColumn,\n type ListFilterRequest,\n} 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 { 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 { CrmListActions, CrmListToolbar } from './crm-list-toolbar'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport {\n listFilterConstraints,\n usePagedCollection,\n useFirestore,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport {\n getGridSingleSelectOperators,\n type GridColDef,\n} from '@mui/x-data-grid'\nimport { collection, limit, orderBy, query, where } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { COMPANY_LIST_FILTER_FIELDS } from '../constants/company-filters'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { crmVisibleToClause, useCrmScope } from '../hooks/use-crm-scope'\nimport { useOrgMemberOptions } from '../hooks/use-org-member-options'\nimport { type CompanyCsvOptions, companiesCsv } from '../model/companies-csv'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport CompaniesBulkBar from './companies-bulk-bar'\nimport CompanyEditDrawer from './company-edit-drawer'\nimport { CompanyImportButton } from './company-import-drawer'\nimport { customFieldColumns } from './contact-custom-columns'\n\nexport interface CompaniesSectionProps {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling>\n /** The CRM hub URL, which every company route hangs beneath. */\n basePath: string\n}\n\ntype CompanyRow = Partial<CrmCompany> & { $id: string; updatedAt?: any }\n\n/** What the companies grid's panel offers: the served fields, and \"No next activity\". */\nconst COMPANY_GRID_FILTER_FIELDS = [\n ...COMPANY_LIST_FILTER_FIELDS,\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n]\nconst COMPANY_GRID_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Company',\n ownerUid: 'Owner',\n [CRM_NEXT_ACTIVITY_FILTER_FIELD.column]: CRM_NEXT_ACTIVITY_FILTER_HEADER,\n}\n\n/**\n * `/crm/companies` — the organizations behind the people (AGL-2597).\n *\n * A section of its own rather than a column on the contact list, because a\n * company is known by several contacts and carries records of its own: a\n * domain the email suggestion keys on, an address, an owner, and the deals\n * and tasks filed against it. The row opens `/crm/companies/{id}`, where all\n * of that lives; this list is the cheaper surface and reads nothing a row\n * does not show.\n *\n * ## What the listener carries\n *\n * Paged and ordered by the server — `updatedAt desc`, so a company somebody\n * just touched is on page one — and SCOPED by the same `visibleTo\n * array-contains-any` predicate the contact list runs. That predicate is what\n * the rules evaluate: a filtered query is provable per document, and an\n * unfiltered one is refused rather than quietly returning the whole org's\n * accounts to a site that may see one client's.\n *\n * ## What a filter may be\n *\n * The scope predicate is an array clause, and Firestore carries one per\n * query, so the word-prefix search the contact list offers cannot run here.\n * The name is a prefix range and an exact match instead, and the owner is a\n * choice from the roster — each reaching the whole collection, which is the\n * property a search must have: a company on page four is found, not reported\n * missing. `COMPANY_LIST_FILTER_FIELDS` states what the indexes serve.\n *\n * ## Creating is a drawer\n *\n * \"New company\" opens the same eight-field drawer the company's page edits\n * with, and the new record's page opens when it is saved. A form above a\n * list has nowhere to grow, and this one has an address in it.\n *\n * ## Selection, the bar and the file (AGL-2621)\n *\n * The rows are selectable, and a selection raises `CompaniesBulkBar` over\n * the table. Export CSV writes the loaded page through `companiesCsv()` —\n * the same file the bar writes over the selection, headed as the import\n * reads it — and Import CSV opens the companies drawer beside it.\n */\nexport function CompaniesSection(props: CompaniesSectionProps) {\n const { hostId, org, basePath } = props\n const routes = crmRoutes(basePath)\n const router = useRouter()\n const firestore = useFirestore()\n const { scope, orgId, visibleTo } = useCrmScope({ hostId, org })\n /*\n * The team, read once for this surface: the Owner column names a person\n * from a uid, and the create drawer picks from the same roster. One fetch\n * serves both, which is why the drawer takes it as a prop.\n */\n const members = useOrgMemberOptions(orgId)\n // The org's company fields, for the optional columns below (AGL-2661).\n const companyFields = useContactFieldDefinitions(orgId, 'company')\n\n /*\n * The column filter is the saved VIEW'S first clause (AGL-2617): this\n * grid's panel holds one, and a view of companies holds that one beside\n * the columns and the sort. Declared BEFORE the listener that reads it,\n * as the filter always was — the query is rebuilt from it.\n */\n const views = useCrmSavedView({ section: 'companies', hostId, org, basePath })\n /*\n * ...beside the \"No next activity\" clause (AGL-2661), which is not a\n * query the listener can run — absence has no index — and narrows the\n * loaded page below instead. The grid's one clause is the first that is\n * not it, and setting the grid's clause keeps it.\n */\n const viewFilters = views.state.filters\n const filter: ListFilterRequest | null =\n viewFilters.find((clause) => !isNoNextActivityClause(clause)) ?? null\n const setFilter = useCallback(\n (request: ListFilterRequest | null) =>\n views.setFilters([\n ...(request ? [request] : []),\n ...viewFilters.filter(isNoNextActivityClause),\n ]),\n [views.setFilters, viewFilters],\n )\n\n const {\n rows: loaded,\n status,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<CompanyRow>(\n (pageLimit) => {\n if (!scope) return null\n const constraints = listFilterConstraints(\n COMPANY_LIST_FILTER_FIELDS,\n filter,\n )\n return query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies),\n // Absent at the organization level, where the tokens are `null` (AGL-2630).\n ...crmVisibleToClause(visibleTo),\n ...(constraints ?? [orderBy('updatedAt', 'desc')]),\n limit(pageLimit),\n )\n },\n [firestore, scope, visibleTo, filter],\n { idField: '$id' },\n )\n const companies = useMemo(\n () => filterByNextActivity(loaded, viewFilters),\n [loaded, viewFilters],\n )\n const nowMs = useMemo(() => Date.now(), [])\n\n const [createOpen, setCreateOpen] = useState(false)\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n /*\n * The owner is written by ADDRESS, because the companies import resolves\n * an owner by email; the same options reach the bar, so the page's file\n * and the selection's are one format.\n */\n const csvOptions: CompanyCsvOptions = useMemo(\n () => ({ ownerEmail: members.emailFor }),\n [members.emailFor],\n )\n const handleExport = useCallback(() => {\n downloadTextFile('companies.csv', 'text/csv', companiesCsv(companies, csvOptions))\n }, [companies, csvOptions])\n const openCompany = useCallback(\n (id: string) => router.push(routes.company(id)),\n [router, routes],\n )\n\n const columns: GridColDef[] = useMemo(\n () => [\n {\n field: 'name',\n headerName: 'Company',\n flex: 1.6,\n minWidth: 240,\n ...listFilterColumn(COMPANY_LIST_FILTER_FIELDS, 'name'),\n valueGetter: (_value, row: CompanyRow) => String(row.name ?? ''),\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Stack\n sx={{ justifyContent: 'center', height: '100%', lineHeight: 1.25 }}\n >\n <Typography variant=\"body2\" sx={{ lineHeight: 1.25 }}>\n {row.name}\n </Typography>\n {row.industry ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ lineHeight: 1.25 }}\n noWrap\n >\n {row.industry}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'domain',\n headerName: 'Domain',\n flex: 1,\n minWidth: 160,\n // Stored normalized, so it could be filtered exactly — but a domain\n // lookup is what the company suggestion on a contact does, and a\n // filter here would need an index of its own for one more way to\n // find a row the name filter already finds.\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) => String(row.domain ?? ''),\n renderCell: ({ row }: { row: CompanyRow }) =>\n row.domain ? (\n <Typography variant=\"body2\">{row.domain}</Typography>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n field: 'contactsCount',\n headerName: 'Contacts',\n width: 110,\n align: 'right',\n headerAlign: 'right',\n /*\n * The denormalized count every link moves (AGL-2613) — a stored\n * number, so a page of companies costs no read per row. Absent on a\n * company nobody has linked since the counter existed, which reads\n * as zero; the company's own page takes the live aggregate.\n */\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) => Number(row.contactsCount ?? 0),\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Typography\n variant=\"body2\"\n color={row.contactsCount ? 'text.primary' : 'text.secondary'}\n >\n {Number(row.contactsCount ?? 0).toLocaleString()}\n </Typography>\n ),\n },\n {\n field: 'tags',\n headerName: 'Tags',\n flex: 1,\n minWidth: 140,\n // Written by the drawer and the bulk bar (AGL-2621); read here, not\n // filtered — a tag filter would need an `array-contains` index of\n // its own beside the scope predicate.\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) => (row.tags ?? []).join(', '),\n renderCell: ({ row }: { row: CompanyRow }) =>\n row.tags?.length ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ alignItems: 'center', overflow: 'hidden' }}>\n {row.tags.slice(0, 3).map((tag) => (\n <Chip key={tag} size=\"small\" variant=\"outlined\" label={tag} />\n ))}\n {row.tags.length > 3 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`+${row.tags.length - 3}`}\n </Typography>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 1,\n minWidth: 160,\n sortable: false,\n /*\n * A CHOICE from the roster, not a text box for a uid. The grid's\n * single-select `is` operator is what the panel offers; the handler\n * below maps it onto the declared field's `equals`, which the\n * translator turns into the equality the index serves.\n */\n type: 'singleSelect',\n valueOptions: members.options.map((option) => ({\n value: option.uid,\n label: crmMemberPickerLabel(option),\n })),\n filterable: members.options.length > 0,\n filterOperators: getGridSingleSelectOperators().filter(\n (operator) => operator.value === 'is',\n ),\n valueGetter: (_value, row: CompanyRow) => String(row.ownerUid ?? ''),\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Typography\n variant=\"body2\"\n color={row.ownerUid ? 'text.primary' : 'text.secondary'}\n >\n {row.ownerUid\n ? members.ready\n ? members.labelFor(row.ownerUid)\n : '…'\n : '—'}\n </Typography>\n ),\n },\n {\n field: 'updatedAt',\n headerName: 'Updated',\n flex: 0.8,\n minWidth: 140,\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) =>\n row.updatedAt?.seconds ? new Date(row.updatedAt.seconds * 1000) : null,\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {row.updatedAt?.seconds\n ? new Date(row.updatedAt.seconds * 1000).toLocaleDateString()\n : '—'}\n </Typography>\n ),\n },\n // When the earliest open task against the company is due (AGL-2661).\n nextActivityColumn(nowMs),\n // The org's company fields as optional columns (AGL-2661), read off\n // the row's own `custom` map the way the contacts list reads its own.\n ...customFieldColumns(companyFields.active),\n ],\n [members, companyFields.active, nowMs],\n )\n\n /*\n * The grid's own Filters panel and quick search edit the view's clauses\n * (AGL-3313). The query serves ONE clause — a paged list has no window to\n * narrow beyond the page — so the panel's clause replaces the last, and\n * \"No next activity\", which narrows the loaded page, stands beside it.\n * The search box is the name's prefix: the one text search the indexes\n * serve here, so typing in it is the same clause as \"Company starts with\".\n */\n const ownerOptions = useMemo(\n () =>\n members.options.map((option) => ({\n value: option.uid,\n label: crmMemberPickerLabel(option),\n })),\n [members.options],\n )\n const filterColumns = useMemo(\n () =>\n crmFilterColumns(columns, COMPANY_GRID_FILTER_FIELDS, { ownerUid: ownerOptions }, COMPANY_GRID_FILTER_HEADERS),\n [columns, ownerOptions],\n )\n const searchWords = useMemo(\n () => (filter?.field === 'name' && filter.op === 'startsWith' ? [filter.value] : []),\n [filter],\n )\n const setSearchWords = useCallback(\n (words: string[]) => {\n const term = words.join(' ').trim()\n if (term) setFilter({ field: 'name', op: 'startsWith', value: term })\n else if (filter?.field === 'name' && filter.op === 'startsWith') setFilter(null)\n },\n [filter, setFilter],\n )\n const gridFilter = useCrmGridFilter({\n clauses: viewFilters,\n onChange: views.setFilters,\n selectFields: ['ownerUid'],\n single: true,\n keepAlongside: isNoNextActivityClause,\n search: { words: searchWords, onChange: setSearchWords },\n })\n /*\n * The grid's models are the view's (AGL-2617). The filter model shows the\n * view's clause in the panel as a typed one would appear — the owner's\n * stored `equals` back as the single-select `is` the panel offers — so a\n * view opened from its address reads as filtered, not as a mystery.\n */\n const grid = useCrmViewGrid(views, filterColumns)\n\n const newCompanyButton = (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={!scope}\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n onClick={() => setCreateOpen(true)}\n >\n {'New company'}\n </Button>\n )\n\n return (\n <CardDisplay\n header={'Companies'}\n help={pluginDocsHelp('companies', { anchor: '#the-companies-list' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n // The record actions, top right and never clipped (AGL-3311).\n action: (\n <CrmListActions>\n <CompanyImportButton hostId={hostId} org={org} />\n <Button size=\"small\" onClick={handleExport} disabled={!companies.length}>\n {'Export CSV'}\n </Button>\n {newCompanyButton}\n </CrmListActions>\n ),\n }}\n >\n <Stack spacing={1.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The organizations your contacts belong to. Open one to see its ' +\n 'people, its deals and its open tasks, or to link a contact ' +\n 'to it.'}\n </Typography>\n {/* The saved view, and the clauses narrowing it as chips (AGL-3313). */}\n <CrmListToolbar label=\"Company filters\">\n <CrmViewsControl controller={views} allLabel=\"All companies\" />\n <CrmFilterBar\n fields={COMPANY_GRID_FILTER_FIELDS}\n headers={COMPANY_GRID_FILTER_HEADERS}\n clauses={viewFilters}\n onChange={views.setFilters}\n options={{ ownerUid: ownerOptions }}\n servedField={filter?.field ?? null}\n />\n </CrmListToolbar>\n <CompaniesBulkBar\n hostId={hostId}\n scope={scope}\n rows={companies}\n selected={selectedIds}\n onSelectedChange={setSelectedIds}\n members={members}\n csv={csvOptions}\n />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rowHeight={TABLE_ROW_HEIGHT}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n rows={companies}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n noRowsLabel=\"No companies yet\"\n noRowsDescription=\"A company groups the contacts who work at one business, with its domain, owner and address. Create the first one, or link a contact to a company from their page.\"\n noRowsAction={newCompanyButton}\n onOpen={(id) => openCompany(String(id))}\n // An empty table while the read is in flight reads as \"you have\n // none\" rather than \"these are on their way\".\n loading={!scope || status === 'loading'}\n /*\n * The grid must NOT also filter. The query answers it, so a client\n * pass could only drop rows the query already matched.\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 // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n </CrmColumnOrderProvider>\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={companies.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </Stack>\n {/*\n Mounted only while it is open, so the list pays for none of the\n drawer's state — and a fresh mount is what seeds an empty form.\n */}\n {createOpen ? (\n <CompanyEditDrawer\n open\n onClose={() => setCreateOpen(false)}\n hostId={hostId}\n org={org}\n members={members}\n onSaved={openCompany}\n />\n ) : null}\n </CardDisplay>\n )\n}\nCompaniesSection.displayName = 'CompaniesSection'\n\nexport default CompaniesSection\n"],"names":["CRM_COLLECTIONS","filterByNextActivity","isNoNextActivityClause","pluginDocsHelp","crmMemberPickerLabel","mdiPlus","CardDisplay","MdiIcon","ListPagination","ListTable","listFilterColumn","useCrmSavedView","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","CRM_NEXT_ACTIVITY_FILTER_FIELD","CRM_NEXT_ACTIVITY_FILTER_HEADER","nextActivityColumn","CrmFilterBar","crmFilterColumns","useCrmGridFilter","CrmViewsControl","CrmListActions","CrmListToolbar","TABLE_ROW_HEIGHT","listFilterConstraints","usePagedCollection","useFirestore","Button","Chip","Stack","Typography","getGridSingleSelectOperators","collection","limit","orderBy","query","useRouter","useCallback","useMemo","useState","COMPANY_LIST_FILTER_FIELDS","useContactFieldDefinitions","crmVisibleToClause","useCrmScope","useOrgMemberOptions","companiesCsv","downloadTextFile","crmRoutes","CompaniesBulkBar","CompanyEditDrawer","CompanyImportButton","customFieldColumns","COMPANY_GRID_FILTER_FIELDS","COMPANY_GRID_FILTER_HEADERS","name","ownerUid","column","CompaniesSection","props","viewFilters","hostId","org","basePath","routes","router","firestore","scope","orgId","visibleTo","members","companyFields","views","section","state","filters","filter","find","clause","setFilter","request","setFilters","rows","loaded","status","hasMore","page","setPage","pageSize","setPageSize","pageLimit","constraints","companies","idField","nowMs","Date","now","createOpen","setCreateOpen","selectedIds","setSelectedIds","csvOptions","ownerEmail","emailFor","handleExport","openCompany","id","push","company","columns","field","headerName","flex","minWidth","valueGetter","_value","row","String","renderCell","sx","justifyContent","height","lineHeight","variant","industry","color","noWrap","filterable","sortable","domain","width","align","headerAlign","Number","contactsCount","toLocaleString","tags","join","length","direction","spacing","alignItems","overflow","slice","map","tag","size","label","type","valueOptions","options","option","value","uid","filterOperators","operator","ready","labelFor","updatedAt","seconds","toLocaleDateString","active","ownerOptions","filterColumns","searchWords","op","setSearchWords","words","term","trim","gridFilter","clauses","onChange","selectFields","single","keepAlongside","search","grid","newCompanyButton","disabled","startIcon","path","onClick","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","controller","allLabel","fields","headers","servedField","selected","onSelectedChange","csv","columnOrder","rowHeight","slots","selectable","noRowsLabel","noRowsDescription","noRowsAction","onOpen","loading","filterMode","filterModel","onFilterModelChange","quickFilter","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","hideFooter","rowCount","onPageChange","onPageSizeChange","open","onClose","onSaved","displayName"],"mappings":"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"}
@@ -48,9 +48,10 @@ import { customFieldColumns } from "./contact-custom-columns.js";
48
48
  import { useCrmSavedView } from "../hooks/use-crm-saved-view.js";
49
49
  import { useCrmViewGrid } from "../hooks/use-crm-view-grid.js";
50
50
  import { CRM_LIST_SLOTS, CrmColumnOrderProvider } from "./crm-column-menu.js";
51
- import { NoNextActivityToggle } from "./crm-next-activity-column.js";
52
51
  import { useCompanyOptions } from "./company-picker.js";
53
52
  import CrmFilterBar from "./crm-filter-bar.js";
53
+ import { crmFilterColumns, crmRowMatchesSearch } from "../model/crm-grid-filter.js";
54
+ import { useCrmGridFilter } from "../hooks/use-crm-grid-filter.js";
54
55
  import CrmViewsControl from "./crm-views-control.js";
55
56
  import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-suite-lock.js";
56
57
  /**
@@ -59,6 +60,13 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
59
60
  * The map lives beside the `ContactSource` union so the dynamic-list rule
60
61
  * editor and this filter cannot disagree about what `order` is called.
61
62
  */ const SOURCE_LABELS = CONTACT_SOURCE_LABELS;
63
+ /** What the grid's quick search reads on a contact: who they are, and their tags. */ const CONTACT_SEARCH_FIELDS = [
64
+ 'name',
65
+ 'email',
66
+ 'phone',
67
+ 'company',
68
+ 'tags'
69
+ ];
62
70
  /**
63
71
  * What the list keeps out of sight until a view says otherwise: the
64
72
  * filter-only columns, which never show, and the optional columns
@@ -504,10 +512,16 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
504
512
  * not.
505
513
  */ const uid = (_ref6 = user == null ? void 0 : user.uid) != null ? _ref6 : '';
506
514
  const windowClauses = plan.window;
507
- const visible = useMemo(()=>windowClauses.length ? contacts.filter((contact)=>windowClauses.every((clause)=>matchListFilter(contact, filterFields, clause))) : contacts, [
515
+ // The quick search is the grid's box, answered here over the same window.
516
+ const [searchWords, setSearchWords] = useState([]);
517
+ const searchKey = searchWords.join(' ');
518
+ const visible = useMemo(()=>windowClauses.length || searchWords.length ? contacts.filter((contact)=>windowClauses.every((clause)=>matchListFilter(contact, filterFields, clause)) && crmRowMatchesSearch(contact, CONTACT_SEARCH_FIELDS, searchWords)) : contacts, // `searchKey` stands for the words, which are a new array each render.
519
+ // eslint-disable-next-line react-hooks/exhaustive-deps
520
+ [
508
521
  contacts,
509
522
  windowClauses,
510
- filterFields
523
+ filterFields,
524
+ searchKey
511
525
  ]);
512
526
  /*
513
527
  * The choices the filter bar picks from. The companies are read only once
@@ -587,7 +601,27 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
587
601
  * columns, never the custom ones, which are shown — stay hidden whatever
588
602
  * a view says, so a saved arrangement cannot unhide a strip of blank
589
603
  * cells.
590
- */ const grid = useCrmViewGrid(views, contactColumns, HIDDEN_COLUMNS);
604
+ */ /*
605
+ * The grid's own Filters panel and quick search edit the view's clauses
606
+ * (AGL-3313). The picked fields — owner, stage, source, company, email
607
+ * verdict, a custom select — are select columns over the same choices the
608
+ * chips name them by; the rest keep the operators the grammar declares.
609
+ */ const filterColumns = useMemo(()=>crmFilterColumns(contactColumns, filterFields, filterOptions, filterHeaders), [
610
+ contactColumns,
611
+ filterFields,
612
+ filterOptions,
613
+ filterHeaders
614
+ ]);
615
+ const gridFilter = useCrmGridFilter({
616
+ clauses: views.state.filters,
617
+ onChange: views.setFilters,
618
+ selectFields: Object.keys(filterOptions),
619
+ search: {
620
+ words: searchWords,
621
+ onChange: setSearchWords
622
+ }
623
+ });
624
+ const grid = useCrmViewGrid(views, filterColumns, HIDDEN_COLUMNS);
591
625
  /*
592
626
  * A SEGMENT IS A VIEW'S TAG AND SOURCE CLAUSES, kept where a campaign
593
627
  * can reach them (AGL-2617).
@@ -811,15 +845,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
811
845
  clauses: views.state.filters,
812
846
  onChange: views.setFilters,
813
847
  options: filterOptions,
814
- servedField: (_ref7 = (_plan_served = plan.served) == null ? void 0 : _plan_served.field) != null ? _ref7 : null,
815
- onOpen: ()=>setFilterOpened(true)
816
- }),
817
- /*#__PURE__*/ _jsx(Stack, {
818
- direction: "row",
819
- children: /*#__PURE__*/ _jsx(NoNextActivityToggle, {
820
- filters: views.state.filters,
821
- onChange: views.setFilters
822
- })
848
+ servedField: (_ref7 = (_plan_served = plan.served) == null ? void 0 : _plan_served.field) != null ? _ref7 : null
823
849
  })
824
850
  ]
825
851
  }),
@@ -931,12 +957,18 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
931
957
  },
932
958
  onOpen: (id)=>router.push(routes.contact(id)),
933
959
  /*
934
- * The grid must NOT also filter: the bar above is the one
935
- * editor of the clauses, and the grid's own filter panel
936
- * holds one item where a view holds several. Columns and
937
- * sort are the view's, controlled so a saved arrangement
938
- * is what the grid shows and a change is what it saves.
939
- */ disableColumnFilter: true,
960
+ * The grid's Filters panel and quick search edit the view's
961
+ * clauses; the list answers them, onto the query where it
962
+ * can and over the loaded window where it cannot, so the
963
+ * grid must not filter again itself (AGL-3313). Opening the
964
+ * panel is what reads the roster and the companies its
965
+ * pickers name. Columns and sort are the view's, controlled
966
+ * so a saved arrangement is what the grid shows.
967
+ */ filterMode: "server",
968
+ filterModel: gridFilter.filterModel,
969
+ onFilterModelChange: gridFilter.onFilterModelChange,
970
+ quickFilter: true,
971
+ onPreferencePanelOpen: ()=>setFilterOpened(true),
940
972
  columnVisibilityModel: grid.columnVisibilityModel,
941
973
  onColumnVisibilityModelChange: grid.onColumnVisibilityModelChange,
942
974
  sortModel: grid.sortModel,