@aglyn/plugins-crm 1.0.0-beta.199 → 1.0.0-beta.201

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 (169) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/activity-list.d.ts +36 -5
  3. package/src/lib/components/activity-list.js +159 -137
  4. package/src/lib/components/activity-list.js.map +1 -1
  5. package/src/lib/components/activity-timeline.d.ts +84 -0
  6. package/src/lib/components/activity-timeline.js +224 -0
  7. package/src/lib/components/activity-timeline.js.map +1 -0
  8. package/src/lib/components/companies-section.d.ts +6 -6
  9. package/src/lib/components/companies-section.js +54 -84
  10. package/src/lib/components/companies-section.js.map +1 -1
  11. package/src/lib/components/company-edit-drawer.js +17 -4
  12. package/src/lib/components/company-edit-drawer.js.map +1 -1
  13. package/src/lib/components/company-picker.js +6 -2
  14. package/src/lib/components/company-picker.js.map +1 -1
  15. package/src/lib/components/company-properties-card.js +53 -36
  16. package/src/lib/components/company-properties-card.js.map +1 -1
  17. package/src/lib/components/contact-associations-card.d.ts +3 -2
  18. package/src/lib/components/contact-associations-card.js +12 -12
  19. package/src/lib/components/contact-associations-card.js.map +1 -1
  20. package/src/lib/components/contact-detail-page.d.ts +4 -3
  21. package/src/lib/components/contact-detail-page.js +18 -12
  22. package/src/lib/components/contact-detail-page.js.map +1 -1
  23. package/src/lib/components/contact-list-columns.d.ts +3 -5
  24. package/src/lib/components/contact-list-columns.js +6 -8
  25. package/src/lib/components/contact-list-columns.js.map +1 -1
  26. package/src/lib/components/contact-properties-card.d.ts +21 -4
  27. package/src/lib/components/contact-properties-card.js +129 -29
  28. package/src/lib/components/contact-properties-card.js.map +1 -1
  29. package/src/lib/components/contact-timeline-card.d.ts +12 -6
  30. package/src/lib/components/contact-timeline-card.js +132 -174
  31. package/src/lib/components/contact-timeline-card.js.map +1 -1
  32. package/src/lib/components/contacts-bulk-bar.js +10 -1
  33. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  34. package/src/lib/components/contacts-section.js +282 -331
  35. package/src/lib/components/contacts-section.js.map +1 -1
  36. package/src/lib/components/crm-email-state-chip.js +4 -1
  37. package/src/lib/components/crm-email-state-chip.js.map +1 -1
  38. package/src/lib/components/crm-next-activity-column.d.ts +4 -2
  39. package/src/lib/components/crm-next-activity-column.js +6 -3
  40. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  41. package/src/lib/components/deal-edit-drawer.js +8 -4
  42. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  43. package/src/lib/components/deal-products-card.js +15 -13
  44. package/src/lib/components/deal-products-card.js.map +1 -1
  45. package/src/lib/components/deal-properties-card.js +7 -0
  46. package/src/lib/components/deal-properties-card.js.map +1 -1
  47. package/src/lib/components/deal-stage-card.js +31 -29
  48. package/src/lib/components/deal-stage-card.js.map +1 -1
  49. package/src/lib/components/deals-section.d.ts +2 -2
  50. package/src/lib/components/deals-section.js +67 -97
  51. package/src/lib/components/deals-section.js.map +1 -1
  52. package/src/lib/components/fields-section.d.ts +4 -0
  53. package/src/lib/components/fields-section.js +368 -154
  54. package/src/lib/components/fields-section.js.map +1 -1
  55. package/src/lib/components/form-contact-fields-card.js +24 -20
  56. package/src/lib/components/form-contact-fields-card.js.map +1 -1
  57. package/src/lib/components/lead-campaigns-card.d.ts +1 -1
  58. package/src/lib/components/lead-campaigns-card.js +43 -38
  59. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  60. package/src/lib/components/lead-properties-card.d.ts +14 -6
  61. package/src/lib/components/lead-properties-card.js +323 -311
  62. package/src/lib/components/lead-properties-card.js.map +1 -1
  63. package/src/lib/components/lead-surfaces-note.js +13 -2
  64. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  65. package/src/lib/components/leads-bulk-bar.js +16 -3
  66. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  67. package/src/lib/components/leads-section.d.ts +2 -1
  68. package/src/lib/components/leads-section.js +110 -148
  69. package/src/lib/components/leads-section.js.map +1 -1
  70. package/src/lib/components/linked-deals-card.js +12 -10
  71. package/src/lib/components/linked-deals-card.js.map +1 -1
  72. package/src/lib/components/recent-activity-feed.d.ts +12 -8
  73. package/src/lib/components/recent-activity-feed.js +54 -29
  74. package/src/lib/components/recent-activity-feed.js.map +1 -1
  75. package/src/lib/components/record-activity-card.d.ts +4 -2
  76. package/src/lib/components/record-activity-card.js +29 -11
  77. package/src/lib/components/record-activity-card.js.map +1 -1
  78. package/src/lib/components/reports/activity-card.js +18 -12
  79. package/src/lib/components/reports/activity-card.js.map +1 -1
  80. package/src/lib/components/reports/lead-funnel-card.js +47 -37
  81. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  82. package/src/lib/components/reports/pipeline-card.js +22 -16
  83. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  84. package/src/lib/components/reports/report-export.d.ts +4 -11
  85. package/src/lib/components/reports/report-export.js +16 -33
  86. package/src/lib/components/reports/report-export.js.map +1 -1
  87. package/src/lib/components/reports/source-conversion-card.js +18 -12
  88. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  89. package/src/lib/components/reports/tasks-card.js +35 -25
  90. package/src/lib/components/reports/tasks-card.js.map +1 -1
  91. package/src/lib/components/tasks-section.js +151 -144
  92. package/src/lib/components/tasks-section.js.map +1 -1
  93. package/src/lib/constants/company-filters.d.ts +13 -0
  94. package/src/lib/constants/company-filters.js +47 -17
  95. package/src/lib/constants/company-filters.js.map +1 -1
  96. package/src/lib/constants/contact-filters.d.ts +61 -10
  97. package/src/lib/constants/contact-filters.js +373 -141
  98. package/src/lib/constants/contact-filters.js.map +1 -1
  99. package/src/lib/constants/deal-filters.d.ts +44 -0
  100. package/src/lib/constants/deal-filters.js +78 -0
  101. package/src/lib/constants/deal-filters.js.map +1 -0
  102. package/src/lib/constants/field-list-query.d.ts +23 -0
  103. package/src/lib/constants/field-list-query.js +80 -0
  104. package/src/lib/constants/field-list-query.js.map +1 -0
  105. package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
  106. package/src/lib/hooks/use-crm-list-query.js +182 -0
  107. package/src/lib/hooks/use-crm-list-query.js.map +1 -0
  108. package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
  109. package/src/lib/hooks/use-crm-tasks.js +44 -42
  110. package/src/lib/hooks/use-crm-tasks.js.map +1 -1
  111. package/src/lib/hooks/use-deals.d.ts +2 -34
  112. package/src/lib/hooks/use-deals.js +3 -62
  113. package/src/lib/hooks/use-deals.js.map +1 -1
  114. package/src/lib/model/crm-list-query.d.ts +91 -0
  115. package/src/lib/model/crm-list-query.js +182 -0
  116. package/src/lib/model/crm-list-query.js.map +1 -0
  117. package/src/lib/model/lead-filters.d.ts +50 -44
  118. package/src/lib/model/lead-filters.js +194 -88
  119. package/src/lib/model/lead-filters.js.map +1 -1
  120. package/src/lib/model/list-fields-api.d.ts +26 -0
  121. package/src/lib/model/list-fields-api.js +47 -0
  122. package/src/lib/model/list-fields-api.js.map +1 -0
  123. package/src/lib/model/list-fields.d.ts +47 -0
  124. package/src/lib/model/list-fields.js +39 -0
  125. package/src/lib/model/list-fields.js.map +1 -0
  126. package/src/lib/model/task-views.d.ts +15 -10
  127. package/src/lib/model/task-views.js +68 -9
  128. package/src/lib/model/task-views.js.map +1 -1
  129. package/src/lib/server/capture-contact.js +7 -5
  130. package/src/lib/server/capture-contact.js.map +1 -1
  131. package/src/lib/server/companies-import.js +3 -3
  132. package/src/lib/server/companies-import.js.map +1 -1
  133. package/src/lib/server/company-delete.js +3 -1
  134. package/src/lib/server/company-delete.js.map +1 -1
  135. package/src/lib/server/consent-group-participant.js +4 -0
  136. package/src/lib/server/consent-group-participant.js.map +1 -1
  137. package/src/lib/server/contact-update.js +14 -8
  138. package/src/lib/server/contact-update.js.map +1 -1
  139. package/src/lib/server/contacts-import.js +4 -2
  140. package/src/lib/server/contacts-import.js.map +1 -1
  141. package/src/lib/server/deals-import.js +4 -2
  142. package/src/lib/server/deals-import.js.map +1 -1
  143. package/src/lib/server/erase-person.js +2 -1
  144. package/src/lib/server/erase-person.js.map +1 -1
  145. package/src/lib/server/lead-create.js +4 -1
  146. package/src/lib/server/lead-create.js.map +1 -1
  147. package/src/lib/server/lead-source-values.js +9 -5
  148. package/src/lib/server/lead-source-values.js.map +1 -1
  149. package/src/lib/server/leads-import.js +4 -1
  150. package/src/lib/server/leads-import.js.map +1 -1
  151. package/src/lib/server/list-fields-routes.d.ts +29 -0
  152. package/src/lib/server/list-fields-routes.js +85 -0
  153. package/src/lib/server/list-fields-routes.js.map +1 -0
  154. package/src/lib/server/record-email-state.js +5 -2
  155. package/src/lib/server/record-email-state.js.map +1 -1
  156. package/src/lib/server/record-timeline.js +3 -2
  157. package/src/lib/server/record-timeline.js.map +1 -1
  158. package/src/lib/server/task-routes.js +6 -3
  159. package/src/lib/server/task-routes.js.map +1 -1
  160. package/src/lib/server/tasks-import.js +5 -2
  161. package/src/lib/server/tasks-import.js.map +1 -1
  162. package/src/lib/server.js +10 -1
  163. package/src/lib/server.js.map +1 -1
  164. package/src/lib/components/contact-custom-fields-card.d.ts +0 -39
  165. package/src/lib/components/contact-custom-fields-card.js +0 -204
  166. package/src/lib/components/contact-custom-fields-card.js.map +0 -1
  167. package/src/lib/hooks/use-org-leads.d.ts +0 -50
  168. package/src/lib/hooks/use-org-leads.js +0 -104
  169. package/src/lib/hooks/use-org-leads.js.map +0 -1
@@ -18,12 +18,16 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
19
  import * as Aglyn from "@aglyn/aglyn";
20
20
  import { CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_LIFECYCLE_STAGES, CONTACT_SOURCE_LABELS, CRM_CONTACT_VIEW_FIELDS, crmContactCustomColumn, crmViewFiltersFromSegment, crmViewSegmentFilters, newResourceScopeFields, ORG_SCOPE_TOKEN, pluginDocsHelp } from "@aglyn/aglyn";
21
- import { hiddenFilterVisibility, matchListFilter } from "@aglyn/shared-ui-jsx/const/list-filter";
21
+ import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
22
22
  import { ListTable } from "@aglyn/shared-ui-jsx/components/list-table.component";
23
- import { CONTACT_LIST_FILTER_FIELDS, CONTACT_LIST_FILTER_HEADERS, contactCustomFilterFields, contactCustomFilterHeaders } from "../constants/contact-filters.js";
23
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
24
+ import ListQueryNotices from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
25
+ import { CONTACT_LIST_DECLARATION, CONTACT_LIST_FILTER_FIELDS, CONTACT_LIST_FILTER_HEADERS, CONTACT_PREFIX_SEARCH, contactClauseImpliesScope, contactSoloClause, contactCustomFilterFields, contactCustomFilterHeaders, contactQueryClause } from "../constants/contact-filters.js";
26
+ import { crmAskClauses, crmQueryRefusals } from "../model/crm-list-query.js";
27
+ import { useCrmFoldsScope, useCrmListQuery } from "../hooks/use-crm-list-query.js";
24
28
  import { contactPrimaryGroup, contactRecordFromDoc } from "../model/contact-record.js";
25
29
  import { useCrmOrgMount } from "../hooks/use-crm-org-mount.js";
26
- import { crmVisibleToClause, useCrmScope } from "../hooks/use-crm-scope.js";
30
+ import { useCrmScope } from "../hooks/use-crm-scope.js";
27
31
  import { contactsListSeed } from "../model/contacts-list-seed.js";
28
32
  import { crmRoutes } from "../model/crm-routes.js";
29
33
  import { contactsCsv, downloadTextFile } from "../model/contacts-csv.js";
@@ -31,9 +35,10 @@ import { useCrmRecordsQuota } from "../hooks/use-crm-records-quota.js";
31
35
  import { CardDisplay } from "@aglyn/shared-ui-jsx";
32
36
  import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.component";
33
37
  import ContactsBulkBar from "./contacts-bulk-bar.js";
38
+ import { CrmListActions } from "./crm-list-toolbar.js";
34
39
  import { ContactImportButton } from "./contact-import-drawer.js";
35
40
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
36
- import { listFilterPlan, useFirestore, useFirestoreCollection, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
41
+ import { useFirestore, useFirestoreCollection, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
37
42
  import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField, Typography } from "@mui/material";
38
43
  import { addDoc, collection, deleteDoc, doc, limit, orderBy, query } from "firebase/firestore";
39
44
  import { useRouter, useSearchParams } from "next/navigation";
@@ -50,7 +55,7 @@ import { useCrmViewGrid } from "../hooks/use-crm-view-grid.js";
50
55
  import { CRM_LIST_SLOTS, CrmColumnOrderProvider } from "./crm-column-menu.js";
51
56
  import { useCompanyOptions } from "./company-picker.js";
52
57
  import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
53
- import { listFilterGridColumns, listRowMatchesSearch } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
58
+ import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
54
59
  import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
55
60
  import CrmViewsControl from "./crm-views-control.js";
56
61
  import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-suite-lock.js";
@@ -60,13 +65,6 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
60
65
  * The map lives beside the `ContactSource` union so the dynamic-list rule
61
66
  * editor and this filter cannot disagree about what `order` is called.
62
67
  */ 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
- ];
70
68
  /**
71
69
  * What the list keeps out of sight until a view says otherwise: the
72
70
  * filter-only columns, which never show, and the optional columns
@@ -108,8 +106,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
108
106
  * body is the v1 page unchanged, and it takes the shell's full prop bag so
109
107
  * nothing the v1 page read has to be threaded through the hub by name.
110
108
  */ export function ContactsPeopleSection(props) {
111
- var _ref, _ref1, _ref2, _records_contactsCount, _ref3, _ref4, _ref5, _ref6, _ref7, _filterHeaders_windowClauses__field, _filterHeaders_plan_served_field;
112
- var _plan_served;
109
+ var _ref, _ref1, _ref2, _ref3, _records_contactsCount, _ref4, _ref5, _ref6, _ref7;
113
110
  const { hostId, org, releaseFlag, basePath } = props;
114
111
  /*
115
112
  * A row is a LINK now (AGL-2596): opening a person navigates to their own
@@ -211,7 +208,9 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
211
208
  ...seed.filter ? [
212
209
  seed.filter
213
210
  ] : [],
214
- ...seed.source ? [
211
+ // A form's captures all came in through a form: the source beside it
212
+ // would ask a second array clause of the one query (AGL-3321).
213
+ ...seed.source && !seed.formId ? [
215
214
  {
216
215
  field: CRM_CONTACT_VIEW_FIELDS.source,
217
216
  op: 'equals',
@@ -247,97 +246,47 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
247
246
  customFields.active
248
247
  ]);
249
248
  /*
250
- * ONE clause on the query, the rest over the window.
249
+ * EVERY CLAUSE AND THE SEARCH ON ONE QUERY (AGL-3321).
251
250
  *
252
- * Firestore takes one array clause per query and composes the rest only
253
- * through indexes nobody built for every pair of columns, so the first
254
- * clause the translator can serve is the one that reaches every contact
255
- * and every other narrows the thousand the query returned. The caption
256
- * under the bar says which is which; the chips mark the served one.
257
- */ /*
258
- * Under a site the listener already carries the `visibleTo`
259
- * `array-contains-any`, so the translator serves no second array clause
260
- * beside it and keeps an `in` inside the disjunction budget the two share
261
- * (AGL-3321). `formIds` is exempt: its listener drops the scope clause.
262
- */ const plan = useMemo(()=>listFilterPlan(filterFields, views.state.filters, visibleToTokens ? {
263
- arrayScope: {
264
- values: visibleToTokens.length,
265
- exempt: [
266
- 'formIds'
267
- ]
268
- }
269
- } : {}), [
270
- filterFields,
251
+ * Each stored clause is asked through the field its writers keep
252
+ * (`contactQueryClause`): a holder's owner, stage, source, company, tags,
253
+ * custom value or "has bought" as that holder's facet key, the verdict as
254
+ * `emailStatus`, the rest as stored. The query pages the list newest
255
+ * change first and carries all of it; what one query cannot hold is
256
+ * refused by name above the list, and nothing is matched over the rows a
257
+ * page happened to load. Under a site the scope clause is the query's one
258
+ * array clause — the search folds into it, and a facet or form clause
259
+ * stands in its place, for a reader whose access is not limited to
260
+ * particular sites.
261
+ */ const foldsScope = useCrmFoldsScope(orgId, visibleToTokens);
262
+ const groupId = (_ref3 = consentGroup == null ? void 0 : consentGroup.groupId) != null ? _ref3 : null;
263
+ const asked = useMemo(()=>crmAskClauses(views.state.filters, (clause)=>contactQueryClause(clause, {
264
+ groupId,
265
+ foldsScope
266
+ })), [
271
267
  views.state.filters,
272
- visibleToTokens
268
+ groupId,
269
+ foldsScope
273
270
  ]);
274
- const filter = plan.served;
275
- /**
276
- * The one filter that cannot carry the scope clause.
277
- *
278
- * `formIds` is an `array-contains` on the mirror, and Firestore takes one
279
- * array clause per query — so this listener drops its `visibleTo`
280
- * predicate for it, exactly as the company contacts card does for
281
- * `companyIds`, and the rules then admit the read to an org-wide member
282
- * only. A scoped member's listener errors instead of listing, and the
283
- * alert below says so rather than showing an empty form.
284
- */ const byForm = (filter == null ? void 0 : filter.field) === 'formIds';
285
- const { data: contactDocs, status: contactsStatus, /**
286
- * The rows the profile drawer is seeded from are unconfirmed by the
287
- * server (AGL-1358). This payload is narrower than most sites in this
288
- * issue — `email`, `sources` and `interactions` are not in it — but the
289
- * two fields that are, `tags` and `notes`, are BOTH written on every
290
- * save and both come off the seed. Edit the notes against a cached read
291
- * and the tags go back with them, and tags are what
292
- * `contactMatchesSegment` runs on: a saved segment is a campaign
293
- * audience, so a rollback here silently changes who gets emailed.
294
- */ fromCache: contactsFromCache } = useFirestoreCollection(()=>{
295
- if (!dataScope) return null;
296
- /*
297
- * ORDERED, and filtered by the QUERY (AGL-2501, AGL-2292).
298
- *
299
- * Two bugs shared this one line. `limit(1000)` with no `orderBy` returns
300
- * documents in ID order — contacts are created with `.add()` and
301
- * `createResourceUid()`, so that is a pseudo-random SAMPLE of a thousand,
302
- * and the client `.sort()` below made it look reliably newest-first. An
303
- * org with forty thousand contacts saw a thousand arbitrary ones,
304
- * convincingly sorted.
305
- *
306
- * The search then ran over that sample, so a name on the wrong side of
307
- * the cap answered "no contacts match" — the answer a search must never
308
- * give wrongly, on the list a merchant uses to find one person.
309
- *
310
- * The cap STAYS: nobody needs forty thousand rows streamed into a table,
311
- * and the head-count has been a server aggregate since AGL-1706. What
312
- * changes is that the thousand are now the newest thousand, and that a
313
- * filter reaches the whole collection before the cap applies.
314
- */ const constraints = plan.constraints;
315
- /*
316
- * SCOPED, and this is the leak it closes.
317
- *
318
- * The listener had no `where()` at all: `hostId` reached it only to
319
- * resolve which ORG owns the data, so every site in the account listed
320
- * every contact in the account. An agency's client opened Contacts and
321
- * read the other clients' customers.
322
- *
323
- * `array-contains-any` over the group's tokens is the same predicate
324
- * the rules evaluate with `hasAny`, so a filtered query is provable
325
- * per-document and an UNFILTERED one is now permission-denied rather
326
- * than quietly returning everything.
327
- */ return query(collection(firestore, dataScope[0], dataScope[1], 'contacts'), // Dropped for the form mirror alone — see `byForm` — and absent at
328
- // the organization level, where the tokens are `null` (AGL-2630).
329
- ...byForm ? [] : crmVisibleToClause(visibleToTokens), ...constraints != null ? constraints : [
330
- orderBy('updatedAt', 'desc')
331
- ], limit(1000));
332
- }, [
333
- firestore,
334
- dataScope,
335
- plan.constraints,
336
- byForm,
337
- visibleToTokens
338
- ], {
339
- idField: '$id'
271
+ const impliesScope = useMemo(()=>contactClauseImpliesScope(groupId), [
272
+ groupId
273
+ ]);
274
+ // The quick search is the grid's box, answered by the same query.
275
+ const [searchWords, setSearchWords] = useState([]);
276
+ const paged = useCrmListQuery({
277
+ scope: dataScope,
278
+ collection: 'contacts',
279
+ visibleTo: visibleToTokens,
280
+ foldsScope,
281
+ declaration: CONTACT_LIST_DECLARATION,
282
+ clauses: asked.clauses,
283
+ search: searchWords,
284
+ impliesScope,
285
+ soloClause: contactSoloClause,
286
+ prefixSearch: CONTACT_PREFIX_SEARCH
340
287
  });
288
+ const contactDocs = paged.rows;
289
+ const contactsStatus = paged.status;
341
290
  /*
342
291
  * OPENED FOR ONE ADDRESS: move straight on to the record (AGL-2612).
343
292
  *
@@ -353,15 +302,17 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
353
302
  useEffect(()=>{
354
303
  if (openedByEmail.current || !seed.openEmail) return;
355
304
  if (contactsStatus !== 'success') return;
356
- if ((filter == null ? void 0 : filter.field) !== 'email' || filter.value !== seed.openEmail) return;
357
- const only = (contactDocs != null ? contactDocs : []).length === 1 ? contactDocs == null ? void 0 : contactDocs[0] : null;
305
+ const served = paged.plan.served.some((clause)=>clause.field === 'email' && clause.op === 'equals' && clause.value === seed.openEmail);
306
+ if (!served) return;
307
+ const only = contactDocs.length === 1 && !paged.hasMore ? contactDocs[0] : null;
358
308
  if (!only) return;
359
309
  openedByEmail.current = true;
360
310
  router.replace(routes.contact(String(only.$id)));
361
311
  }, [
362
312
  seed.openEmail,
363
313
  contactsStatus,
364
- filter,
314
+ paged.plan,
315
+ paged.hasMore,
365
316
  contactDocs,
366
317
  router,
367
318
  routes
@@ -377,7 +328,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
377
328
  * The canonical `name` is the fallback and the shared identity; a holder's
378
329
  * own override wins, so one business renaming a person cannot change what
379
330
  * an unrelated business sees.
380
- */ const contacts = useMemo(()=>(contactDocs != null ? contactDocs : []).map((row)=>contactRecordFromDoc(row, consentGroup != null ? consentGroup : contactPrimaryGroup(row, org))), [
331
+ */ const contacts = useMemo(()=>contactDocs.map((row)=>contactRecordFromDoc(row, consentGroup != null ? consentGroup : contactPrimaryGroup(row, org))), [
381
332
  contactDocs,
382
333
  consentGroup,
383
334
  org
@@ -399,10 +350,10 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
399
350
  /**
400
351
  * The HEAD-COUNT, read as a server-side aggregate (AGL-1706).
401
352
  *
402
- * The listener above is `limit(1000)` and always will be — nobody needs
403
- * 40,000 rows streamed into a table. What it must not do is answer "how
404
- * many contacts does this org have", and it did: `contacts.length`
405
- * saturated at 1,000, which is *exactly* the smallest paid included band
353
+ * The list above reads a page at a time (AGL-3321) — nobody needs 40,000
354
+ * rows streamed into a table. What it must not do is answer "how many
355
+ * contacts does this org have", and it did when it was a `limit(1000)`
356
+ * window: `contacts.length` saturated at 1,000, which is *exactly* the smallest paid included band
406
357
  * (`starter`). So `overageContacts = max(0, used − included)` was 0 on
407
358
  * every stock plan and the alert below could not render at all.
408
359
  *
@@ -450,7 +401,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
450
401
  firestore,
451
402
  hostId
452
403
  ]);
453
- const droppedTotal = Number((_ref3 = droppedCounter == null ? void 0 : droppedCounter['total']) != null ? _ref3 : 0);
404
+ const droppedTotal = Number((_ref4 = droppedCounter == null ? void 0 : droppedCounter['total']) != null ? _ref4 : 0);
454
405
  /*==========================================
455
406
  * REFUNDS THAT REACHED NO CONTACT (AGL-2329).
456
407
  *
@@ -470,10 +421,12 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
470
421
  firestore,
471
422
  hostId
472
423
  ]);
473
- const unmatchedRefundTotal = Number((_ref4 = unmatchedRefundCounter == null ? void 0 : unmatchedRefundCounter['total']) != null ? _ref4 : 0);
474
- const unmatchedRefundReason = String((_ref5 = unmatchedRefundCounter == null ? void 0 : unmatchedRefundCounter['lastReason']) != null ? _ref5 : '');
424
+ const unmatchedRefundTotal = Number((_ref5 = unmatchedRefundCounter == null ? void 0 : unmatchedRefundCounter['total']) != null ? _ref5 : 0);
425
+ const unmatchedRefundReason = String((_ref6 = unmatchedRefundCounter == null ? void 0 : unmatchedRefundCounter['lastReason']) != null ? _ref6 : '');
475
426
  // Saved segments (AGL-199): reusable audience filters.
476
- const { data: segmentDocs } = useFirestoreCollection(()=>dataScope ? query(collection(firestore, dataScope[0], dataScope[1], 'contactSegments'), limit(50)) : null, [
427
+ const { data: segmentDocs } = useFirestoreCollection(()=>dataScope ? query(collection(firestore, dataScope[0], dataScope[1], 'contactSegments'), // The first fifty by name — the menu's own order — rather than
428
+ // whichever fifty the ids happen to sort first.
429
+ orderBy('name'), limit(50)) : null, [
477
430
  firestore,
478
431
  dataScope
479
432
  ], {
@@ -510,32 +463,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
510
463
  dataScope,
511
464
  firestore
512
465
  ]);
513
- /*
514
- * THE WINDOW CLAUSES narrow in the browser, and the caption says so.
515
- *
516
- * An owner, a stage, a source, a company and a custom value all live on
517
- * the viewing group's FACET, and a facet path is per group, so none of
518
- * them is a field the query grammar can reach without an index per
519
- * group; a second tag clause, or "any of these tags", is an array clause
520
- * the scope predicate already occupies. So every clause the plan could
521
- * not serve refines the ordered window rather than the collection —
522
- * through the same matcher the grammar declares, so a chip and the rows
523
- * under it agree. Ordinary orgs load their whole list into the window
524
- * anyway; the caption below says what these narrow for the ones that do
525
- * not.
526
- */ const uid = (_ref6 = user == null ? void 0 : user.uid) != null ? _ref6 : '';
527
- const windowClauses = plan.window;
528
- // The quick search is the grid's box, answered here over the same window.
529
- const [searchWords, setSearchWords] = useState([]);
530
- const searchKey = searchWords.join(' ');
531
- const visible = useMemo(()=>windowClauses.length || searchWords.length ? contacts.filter((contact)=>windowClauses.every((clause)=>matchListFilter(contact, filterFields, clause)) && listRowMatchesSearch(contact, CONTACT_SEARCH_FIELDS, searchWords)) : contacts, // `searchKey` stands for the words, which are a new array each render.
532
- // eslint-disable-next-line react-hooks/exhaustive-deps
533
- [
534
- contacts,
535
- windowClauses,
536
- filterFields,
537
- searchKey
538
- ]);
466
+ const uid = (_ref7 = user == null ? void 0 : user.uid) != null ? _ref7 : '';
539
467
  /*
540
468
  * The choices the filter bar picks from. The companies are read only once
541
469
  * the reader has reached for a filter or is looking at a view that names
@@ -595,6 +523,18 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
595
523
  companies.options,
596
524
  customFields.active
597
525
  ]);
526
+ // What the query could not hold, named by the clause the reader set.
527
+ const refused = useMemo(()=>crmQueryRefusals(paged.plan, asked, {
528
+ fields: filterFields,
529
+ headers: filterHeaders,
530
+ options: filterOptions
531
+ }), [
532
+ paged.plan,
533
+ asked,
534
+ filterFields,
535
+ filterHeaders,
536
+ filterOptions
537
+ ]);
598
538
  /* One row grammar, the console's (AGL-2501) — the same table everywhere. */ const contactColumns = useMemo(()=>[
599
539
  ...contactListColumns({
600
540
  memberName: members.memberName,
@@ -688,6 +628,12 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
688
628
  enqueueSnackbar
689
629
  ]);
690
630
  /** The rows ticked for a bulk action (AGL-2603); the bar above the table acts on them. */ const [selectedIds, setSelectedIds] = useState([]);
631
+ // A filter, a search or a page is a different set of rows; a selection
632
+ // made on the last one would be a count over rows no longer on screen.
633
+ useEffect(()=>setSelectedIds([]), [
634
+ paged.plan,
635
+ paged.page
636
+ ]);
691
637
  /*==========================================
692
638
  * ADDING ONE PERSON BY HAND (AGL-2596).
693
639
  *
@@ -778,150 +724,40 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
778
724
  customFields.active
779
725
  ]);
780
726
  const handleExport = useCallback(()=>{
781
- downloadTextFile('contacts.csv', 'text/csv', contactsCsv(visible, csvOptions));
727
+ downloadTextFile('contacts.csv', 'text/csv', contactsCsv(contacts, csvOptions));
782
728
  }, [
783
- visible,
729
+ contacts,
784
730
  csvOptions
785
731
  ]);
786
732
  return /*#__PURE__*/ _jsxs(_Fragment, {
787
733
  children: [
788
- /*#__PURE__*/ _jsx(CardDisplay, {
789
- header: 'Contacts',
790
- help: pluginDocsHelp('contacts', {
791
- anchor: '#the-contacts-page'
792
- }),
793
- contentGutterX: true,
794
- contentGutterY: true,
795
- children: /*#__PURE__*/ _jsxs(Stack, {
796
- spacing: 2,
797
- children: [
798
- /*#__PURE__*/ _jsxs(Stack, {
799
- direction: "row",
800
- spacing: 1,
801
- sx: {
802
- alignItems: 'center',
803
- flexWrap: 'wrap',
804
- rowGap: 1
805
- },
806
- children: [
807
- /*#__PURE__*/ _jsx(Typography, {
808
- variant: "body2",
809
- color: "text.secondary",
810
- sx: {
811
- flex: 1
812
- },
813
- children: `${contactCount.toLocaleString()} contacts · ${Number.isFinite(quota.included) ? `${quota.used.toLocaleString()} of ${quota.included.toLocaleString()} CRM records` : `${quota.used.toLocaleString()} CRM records`}`
814
- }),
815
- suiteIncluded ? /*#__PURE__*/ _jsx(ContactImportButton, {
816
- hostId: hostId,
817
- org: org
818
- }) : /*#__PURE__*/ _jsx(CrmSuiteLockedButton, {
819
- children: 'Import CSV'
820
- }),
821
- /*#__PURE__*/ _jsx(Button, {
822
- size: "small",
823
- onClick: handleExport,
824
- disabled: !visible.length,
825
- children: 'Export CSV'
826
- }),
827
- suiteIncluded ? /*#__PURE__*/ _jsx(Button, {
828
- size: "small",
829
- variant: "contained",
830
- color: "primary",
831
- disabled: !dataScope,
832
- onClick: ()=>{
833
- setCreateError(null);
834
- setCreateOpen(true);
835
- },
836
- children: 'New contact'
837
- }) : /*#__PURE__*/ _jsx(CrmSuiteLockedButton, {
838
- variant: "contained",
839
- color: "primary",
840
- children: 'New contact'
841
- })
842
- ]
843
- }),
844
- suiteIncluded ? null : /*#__PURE__*/ _jsx(CrmSuiteNotice, {
845
- children: 'Contacts arrive here on their own from your forms, sign-ups, ' + 'orders and bookings. Adding one by hand, importing a CSV, ' + "saved views, and a contact's owner, stage and company are " + 'part of the CRM.'
846
- }),
847
- /*#__PURE__*/ _jsxs(Stack, {
848
- spacing: 1,
849
- children: [
850
- /*#__PURE__*/ _jsx(CrmViewsControl, {
851
- controller: views,
852
- allLabel: "All contacts",
853
- suiteLocked: !suiteIncluded,
854
- presets: segmentPresets,
855
- onSaveAsSegment: segmentFilters && dataScope ? ()=>setSegmentName('') : null
856
- }),
857
- /*#__PURE__*/ _jsx(ListFilterChips, {
858
- fields: filterFields,
859
- headers: filterHeaders,
860
- clauses: views.state.filters,
861
- onChange: views.setFilters,
862
- options: filterOptions,
863
- servedField: (_ref7 = (_plan_served = plan.served) == null ? void 0 : _plan_served.field) != null ? _ref7 : null
864
- })
865
- ]
734
+ /*#__PURE__*/ _jsxs(Stack, {
735
+ spacing: 3,
736
+ children: [
737
+ /*#__PURE__*/ _jsx(CardDisplay, {
738
+ header: 'Contacts',
739
+ help: pluginDocsHelp('contacts', {
740
+ anchor: '#the-contacts-page'
866
741
  }),
867
- !quota.allowed ? /*#__PURE__*/ _jsx(Alert, {
868
- severity: "warning",
869
- children: 'CRM records limit reached — new visitors are no longer ' + 'captured' + (droppedTotal > 0 ? ` (${droppedTotal.toLocaleString()} missed so far)` : '') + '. Upgrade in Billing to keep collecting.'
870
- }) : quota.overageRecords > 0 && quota.overageRateUsd != null && // No claim about money until the verdict that decides it has
871
- // settled (AGL-1662). Before Remote Config activation the flag
872
- // reads its registry default, which is no org's verdict, so an
873
- // ungated alert could assert the wrong wording for one paint.
874
- releaseFlagsReady ? /*#__PURE__*/ _jsx(Alert, {
875
- severity: "info",
876
- children: contactsBilled ? // basis (AGL-2399): the count here is LIVE, the invoice
877
- // charges the last reading before the month closes, so the
878
- // dollar figure is a projection until the month ends. Staff
879
- // and customer must not read different sentences about the
880
- // same org's money — that applies to WHEN it is measured as
881
- // much as to how much.
882
- `${quota.overageRecords.toLocaleString()} CRM records over ` + `your plan's included ${quota.included.toLocaleString()} — ` + `metered at $${quota.overageRateUsd}/1,000 per month ` + `(≈$${quota.overageMonthlyUsd.toFixed(2)} if your list ends ` + 'the month at this size). ' + 'Upgrade in Billing for a larger included audience.' : // itself the wording `1a2aed5cb` published to
883
- // `billing-and-plans/overview.md` (AGL-1601/1603). Staff and
884
- // customer must not read different sentences about the same
885
- // org's money.
886
- //
887
- // THE COUNT STAYS, THE TOTAL GOES: the head-count is real —
888
- // ingestion captured those records — and is not a claim
889
- // about money. The upgrade nudge goes with the total, since
890
- // it prompts a purchase premised on a charge that is not
891
- // happening.
892
- `${quota.overageRecords.toLocaleString()} CRM records over ` + `your plan's included ${quota.included.toLocaleString()} — ` + 'not billed while the CRM is unavailable. ' + `The $${quota.overageRateUsd}/1,000 rate applies once ` + 'Contacts opens.'
893
- }) : droppedTotal > 0 ? /*#__PURE__*/ _jsx(Alert, {
894
- severity: "info",
895
- children: `${droppedTotal.toLocaleString()} earlier visitor${droppedTotal === 1 ? ' was' : 's were'} not captured while your CRM records band was full.`
896
- }) : null,
897
- unmatchedRefundTotal > 0 ? /*#__PURE__*/ _jsx(Alert, {
898
- severity: "warning",
899
- children: `${unmatchedRefundTotal.toLocaleString()} refund${unmatchedRefundTotal === 1 ? '' : 's'} could not be recorded against a contact${UNMATCHED_REFUND_REASON[unmatchedRefundReason] ? ` — most recently because ${UNMATCHED_REFUND_REASON[unmatchedRefundReason]}` : ''}. The money moved; the customer's timeline does not show it.`
900
- }) : null,
901
- byForm && contactsStatus === 'error' ? /*#__PURE__*/ _jsx(Alert, {
902
- severity: "info",
903
- 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.'
904
- }) : contacts.length === 0 && !views.state.filters.length ? /*#__PURE__*/ _jsx(EmptyStateComponent, {
905
- label: contactsStatus === 'loading' ? 'Loading contacts…' : 'No contacts yet',
906
- 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.',
907
- action: contactsStatus === 'loading' ? undefined : !suiteIncluded ? /*#__PURE__*/ _jsxs(Stack, {
908
- direction: "row",
909
- spacing: 1,
742
+ contentGutterX: true,
743
+ contentGutterY: true,
744
+ HeaderProps: {
745
+ // The record actions, top right and never clipped (AGL-3311).
746
+ action: /*#__PURE__*/ _jsxs(CrmListActions, {
910
747
  children: [
911
- /*#__PURE__*/ _jsx(CrmSuiteLockedButton, {
912
- variant: "contained",
913
- color: "primary",
914
- children: 'New contact'
915
- }),
916
- /*#__PURE__*/ _jsx(CrmSuiteLockedButton, {
748
+ suiteIncluded ? /*#__PURE__*/ _jsx(ContactImportButton, {
749
+ hostId: hostId,
750
+ org: org
751
+ }) : /*#__PURE__*/ _jsx(CrmSuiteLockedButton, {
917
752
  children: 'Import CSV'
918
- })
919
- ]
920
- }) : /*#__PURE__*/ _jsxs(Stack, {
921
- direction: "row",
922
- spacing: 1,
923
- children: [
753
+ }),
924
754
  /*#__PURE__*/ _jsx(Button, {
755
+ size: "small",
756
+ onClick: handleExport,
757
+ disabled: !contacts.length,
758
+ children: 'Export CSV'
759
+ }),
760
+ suiteIncluded ? /*#__PURE__*/ _jsx(Button, {
925
761
  size: "small",
926
762
  variant: "contained",
927
763
  color: "primary",
@@ -931,73 +767,188 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
931
767
  setCreateOpen(true);
932
768
  },
933
769
  children: 'New contact'
934
- }),
935
- /*#__PURE__*/ _jsx(ContactImportButton, {
936
- hostId: hostId,
937
- org: org
770
+ }) : /*#__PURE__*/ _jsx(CrmSuiteLockedButton, {
771
+ variant: "contained",
772
+ color: "primary",
773
+ children: 'New contact'
938
774
  })
939
775
  ]
940
776
  })
941
- }) : /*#__PURE__*/ _jsxs(_Fragment, {
777
+ },
778
+ children: /*#__PURE__*/ _jsxs(Stack, {
779
+ spacing: 2,
942
780
  children: [
943
- windowClauses.length ? /*#__PURE__*/ _jsx(Typography, {
944
- variant: "caption",
781
+ /*#__PURE__*/ _jsx(Typography, {
782
+ variant: "body2",
945
783
  color: "text.secondary",
946
- children: (windowClauses.length === 1 ? `${(_filterHeaders_windowClauses__field = filterHeaders[windowClauses[0].field]) != null ? _filterHeaders_windowClauses__field : windowClauses[0].field} narrows ` : `${windowClauses.length} of the filters narrow `) + 'the loaded window — the newest 1,000 contacts' + (plan.served ? ` matching ${(_filterHeaders_plan_served_field = filterHeaders[plan.served.field]) != null ? _filterHeaders_plan_served_field : plan.served.field}, which reached every contact.` : '.') + ' Sorting reorders that window.'
784
+ children: `${contactCount.toLocaleString()} contacts · ${Number.isFinite(quota.included) ? `${quota.used.toLocaleString()} of ${quota.included.toLocaleString()} CRM records` : `${quota.used.toLocaleString()} CRM records`}`
785
+ }),
786
+ suiteIncluded ? null : /*#__PURE__*/ _jsx(CrmSuiteNotice, {
787
+ children: 'Contacts arrive here on their own from your forms, sign-ups, ' + 'orders and bookings. Adding one by hand, importing a CSV, ' + "saved views, and a contact's owner, stage and company are " + 'part of the CRM.'
788
+ }),
789
+ /*#__PURE__*/ _jsxs(Stack, {
790
+ spacing: 1,
791
+ children: [
792
+ /*#__PURE__*/ _jsx(CrmViewsControl, {
793
+ controller: views,
794
+ allLabel: "All contacts",
795
+ suiteLocked: !suiteIncluded,
796
+ presets: segmentPresets,
797
+ onSaveAsSegment: segmentFilters && dataScope ? ()=>setSegmentName('') : null
798
+ }),
799
+ /*#__PURE__*/ _jsx(ListFilterChips, {
800
+ fields: filterFields,
801
+ headers: filterHeaders,
802
+ clauses: views.state.filters,
803
+ onChange: views.setFilters,
804
+ options: filterOptions,
805
+ marksServed: false
806
+ })
807
+ ]
808
+ }),
809
+ !quota.allowed ? /*#__PURE__*/ _jsx(Alert, {
810
+ severity: "warning",
811
+ children: 'CRM records limit reached — new visitors are no longer ' + 'captured' + (droppedTotal > 0 ? ` (${droppedTotal.toLocaleString()} missed so far)` : '') + '. Upgrade in Billing to keep collecting.'
812
+ }) : quota.overageRecords > 0 && quota.overageRateUsd != null && // No claim about money until the verdict that decides it has
813
+ // settled (AGL-1662). Before Remote Config activation the flag
814
+ // reads its registry default, which is no org's verdict, so an
815
+ // ungated alert could assert the wrong wording for one paint.
816
+ releaseFlagsReady ? /*#__PURE__*/ _jsx(Alert, {
817
+ severity: "info",
818
+ children: contactsBilled ? // basis (AGL-2399): the count here is LIVE, the invoice
819
+ // charges the last reading before the month closes, so the
820
+ // dollar figure is a projection until the month ends. Staff
821
+ // and customer must not read different sentences about the
822
+ // same org's money — that applies to WHEN it is measured as
823
+ // much as to how much.
824
+ `${quota.overageRecords.toLocaleString()} CRM records over ` + `your plan's included ${quota.included.toLocaleString()} — ` + `metered at $${quota.overageRateUsd}/1,000 per month ` + `(≈$${quota.overageMonthlyUsd.toFixed(2)} if your list ends ` + 'the month at this size). ' + 'Upgrade in Billing for a larger included audience.' : // itself the wording `1a2aed5cb` published to
825
+ // `billing-and-plans/overview.md` (AGL-1601/1603). Staff and
826
+ // customer must not read different sentences about the same
827
+ // org's money.
828
+ //
829
+ // THE COUNT STAYS, THE TOTAL GOES: the head-count is real —
830
+ // ingestion captured those records — and is not a claim
831
+ // about money. The upgrade nudge goes with the total, since
832
+ // it prompts a purchase premised on a charge that is not
833
+ // happening.
834
+ `${quota.overageRecords.toLocaleString()} CRM records over ` + `your plan's included ${quota.included.toLocaleString()} — ` + 'not billed while the CRM is unavailable. ' + `The $${quota.overageRateUsd}/1,000 rate applies once ` + 'Contacts opens.'
835
+ }) : droppedTotal > 0 ? /*#__PURE__*/ _jsx(Alert, {
836
+ severity: "info",
837
+ children: `${droppedTotal.toLocaleString()} earlier visitor${droppedTotal === 1 ? ' was' : 's were'} not captured while your CRM records band was full.`
838
+ }) : null,
839
+ unmatchedRefundTotal > 0 ? /*#__PURE__*/ _jsx(Alert, {
840
+ severity: "warning",
841
+ children: `${unmatchedRefundTotal.toLocaleString()} refund${unmatchedRefundTotal === 1 ? '' : 's'} could not be recorded against a contact${UNMATCHED_REFUND_REASON[unmatchedRefundReason] ? ` — most recently because ${UNMATCHED_REFUND_REASON[unmatchedRefundReason]}` : ''}. The money moved; the customer's timeline does not show it.`
947
842
  }) : null,
948
- /*#__PURE__*/ _jsx(ContactsBulkBar, {
949
- hostId: hostId,
950
- org: org,
951
- scope: dataScope,
952
- consentGroup: consentGroup,
953
- rows: visible,
954
- selected: selectedIds,
955
- onSelectedChange: setSelectedIds,
956
- csv: csvOptions,
957
- suiteLocked: !suiteIncluded
843
+ /*#__PURE__*/ _jsx(ListQueryNotices, {
844
+ refused: refused,
845
+ notices: paged.plan.notices
958
846
  }),
959
- /*#__PURE__*/ _jsx(CrmColumnOrderProvider, {
960
- value: grid.columnOrder,
961
- children: /*#__PURE__*/ _jsx(ListTable, {
962
- rows: visible,
963
- columns: grid.columns,
964
- slots: CRM_LIST_SLOTS,
965
- selectable: {
966
- selected: selectedIds,
967
- onChange: setSelectedIds
968
- },
969
- onOpen: (id)=>router.push(routes.contact(id)),
970
- /*
971
- * The grid's Filters panel and quick search edit the view's
972
- * clauses; the list answers them, onto the query where it
973
- * can and over the loaded window where it cannot, so the
974
- * grid must not filter again itself (AGL-3313). Opening the
975
- * panel is what reads the roster and the companies its
976
- * pickers name. Columns and sort are the view's, controlled
977
- * so a saved arrangement is what the grid shows.
978
- */ filterMode: "server",
979
- filterModel: gridFilter.filterModel,
980
- onFilterModelChange: gridFilter.onFilterModelChange,
981
- quickFilter: true,
982
- onPreferencePanelOpen: ()=>setFilterOpened(true),
983
- // A filter that matched nothing is said inside the grid,
984
- // so the toolbar that set it stays to change it.
985
- noRowsLabel: contactsStatus === 'loading' ? 'Loading…' : 'No contacts match these filters',
986
- columnVisibilityModel: grid.columnVisibilityModel,
987
- onColumnVisibilityModelChange: grid.onColumnVisibilityModelChange,
988
- sortModel: grid.sortModel,
989
- onSortModelChange: grid.onSortModelChange
847
+ contacts.length === 0 && !views.state.filters.length && !searchWords.some((word)=>word.trim()) ? /*#__PURE__*/ _jsx(EmptyStateComponent, {
848
+ label: contactsStatus === 'loading' ? 'Loading contacts…' : 'No contacts yet',
849
+ 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.',
850
+ action: contactsStatus === 'loading' ? undefined : !suiteIncluded ? /*#__PURE__*/ _jsxs(Stack, {
851
+ direction: "row",
852
+ spacing: 1,
853
+ children: [
854
+ /*#__PURE__*/ _jsx(CrmSuiteLockedButton, {
855
+ variant: "contained",
856
+ color: "primary",
857
+ children: 'New contact'
858
+ }),
859
+ /*#__PURE__*/ _jsx(CrmSuiteLockedButton, {
860
+ children: 'Import CSV'
861
+ })
862
+ ]
863
+ }) : /*#__PURE__*/ _jsxs(Stack, {
864
+ direction: "row",
865
+ spacing: 1,
866
+ children: [
867
+ /*#__PURE__*/ _jsx(Button, {
868
+ size: "small",
869
+ variant: "contained",
870
+ color: "primary",
871
+ disabled: !dataScope,
872
+ onClick: ()=>{
873
+ setCreateError(null);
874
+ setCreateOpen(true);
875
+ },
876
+ children: 'New contact'
877
+ }),
878
+ /*#__PURE__*/ _jsx(ContactImportButton, {
879
+ hostId: hostId,
880
+ org: org
881
+ })
882
+ ]
990
883
  })
884
+ }) : /*#__PURE__*/ _jsxs(_Fragment, {
885
+ children: [
886
+ /*#__PURE__*/ _jsx(ContactsBulkBar, {
887
+ hostId: hostId,
888
+ org: org,
889
+ scope: dataScope,
890
+ consentGroup: consentGroup,
891
+ rows: contacts,
892
+ selected: selectedIds,
893
+ onSelectedChange: setSelectedIds,
894
+ csv: csvOptions,
895
+ suiteLocked: !suiteIncluded
896
+ }),
897
+ /*#__PURE__*/ _jsx(CrmColumnOrderProvider, {
898
+ value: grid.columnOrder,
899
+ children: /*#__PURE__*/ _jsx(ListTable, {
900
+ rows: contacts,
901
+ columns: grid.columns,
902
+ slots: CRM_LIST_SLOTS,
903
+ selectable: {
904
+ selected: selectedIds,
905
+ onChange: setSelectedIds
906
+ },
907
+ onOpen: (id)=>router.push(routes.contact(id)),
908
+ /*
909
+ * The grid's Filters panel and quick search edit the view's
910
+ * clauses; the list's query answers them (AGL-3321), so the
911
+ * grid must not filter again itself. Opening the panel is
912
+ * what reads the roster and the companies its pickers name.
913
+ * Columns are the view's, controlled so a saved arrangement
914
+ * is what the grid shows; the query orders the rows —
915
+ * newest change first — so the grid sorts nothing itself.
916
+ */ filterMode: "server",
917
+ filterModel: gridFilter.filterModel,
918
+ onFilterModelChange: gridFilter.onFilterModelChange,
919
+ quickFilter: true,
920
+ onPreferencePanelOpen: ()=>setFilterOpened(true),
921
+ // A filter that matched nothing is said inside the grid,
922
+ // so the toolbar that set it stays to change it.
923
+ noRowsLabel: contactsStatus === 'loading' ? 'Loading…' : 'No contacts match these filters',
924
+ columnVisibilityModel: grid.columnVisibilityModel,
925
+ onColumnVisibilityModelChange: grid.onColumnVisibilityModelChange,
926
+ sortingMode: "server",
927
+ disableColumnSorting: true,
928
+ loading: contactsStatus === 'loading',
929
+ // Paged by the footer below, so the grid must not also slice.
930
+ hideFooter: true
931
+ })
932
+ }),
933
+ /*#__PURE__*/ _jsx(ListPagination, {
934
+ page: paged.page,
935
+ pageSize: paged.pageSize,
936
+ rowCount: contacts.length,
937
+ hasMore: paged.hasMore,
938
+ onPageChange: paged.setPage,
939
+ onPageSizeChange: paged.setPageSize
940
+ })
941
+ ]
991
942
  })
992
943
  ]
993
- }),
994
- /*#__PURE__*/ _jsx(RecentActivityFeed, {
995
- hostId: hostId,
996
- org: org,
997
- basePath: props.basePath
998
944
  })
999
- ]
1000
- })
945
+ }),
946
+ /*#__PURE__*/ _jsx(RecentActivityFeed, {
947
+ hostId: hostId,
948
+ org: org,
949
+ basePath: props.basePath
950
+ })
951
+ ]
1001
952
  }),
1002
953
  /*#__PURE__*/ _jsxs(Dialog, {
1003
954
  open: segmentName !== null,