@aglyn/plugins-crm 1.0.0-beta.198 → 1.0.0-beta.200
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/lib/components/companies-section.d.ts +6 -6
- package/src/lib/components/companies-section.js +54 -84
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +17 -4
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-picker.js +6 -2
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/contact-detail-page.js +14 -2
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -5
- package/src/lib/components/contact-list-columns.js +6 -8
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.js +10 -1
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +115 -163
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.d.ts +4 -2
- package/src/lib/components/crm-next-activity-column.js +6 -3
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +8 -4
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deals-section.d.ts +2 -2
- package/src/lib/components/deals-section.js +59 -84
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.js +11 -3
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +9 -1
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +16 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.d.ts +2 -1
- package/src/lib/components/leads-section.js +110 -148
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/tasks-section.js +128 -121
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +13 -0
- package/src/lib/constants/company-filters.js +47 -17
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +61 -10
- package/src/lib/constants/contact-filters.js +373 -141
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +44 -0
- package/src/lib/constants/deal-filters.js +78 -0
- package/src/lib/constants/deal-filters.js.map +1 -0
- package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
- package/src/lib/hooks/use-crm-list-query.js +182 -0
- package/src/lib/hooks/use-crm-list-query.js.map +1 -0
- package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
- package/src/lib/hooks/use-crm-tasks.js +44 -42
- package/src/lib/hooks/use-crm-tasks.js.map +1 -1
- package/src/lib/hooks/use-deals.d.ts +2 -34
- package/src/lib/hooks/use-deals.js +3 -62
- package/src/lib/hooks/use-deals.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +91 -0
- package/src/lib/model/crm-list-query.js +182 -0
- package/src/lib/model/crm-list-query.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +49 -43
- package/src/lib/model/lead-filters.js +194 -88
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/list-fields-api.d.ts +26 -0
- package/src/lib/model/list-fields-api.js +47 -0
- package/src/lib/model/list-fields-api.js.map +1 -0
- package/src/lib/model/list-fields.d.ts +47 -0
- package/src/lib/model/list-fields.js +39 -0
- package/src/lib/model/list-fields.js.map +1 -0
- package/src/lib/model/task-views.d.ts +15 -10
- package/src/lib/model/task-views.js +68 -9
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/companies-import.js +3 -3
- package/src/lib/server/companies-import.js.map +1 -1
- package/src/lib/server/company-delete.js +3 -1
- package/src/lib/server/company-delete.js.map +1 -1
- package/src/lib/server/consent-group-participant.js +4 -0
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-update.js +14 -8
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.js +4 -2
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/deals-import.js +4 -2
- package/src/lib/server/deals-import.js.map +1 -1
- package/src/lib/server/erase-person.js +2 -1
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/lead-create.js +4 -1
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-source-values.js +9 -5
- package/src/lib/server/lead-source-values.js.map +1 -1
- package/src/lib/server/leads-import.js +4 -1
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server/list-fields-routes.d.ts +29 -0
- package/src/lib/server/list-fields-routes.js +85 -0
- package/src/lib/server/list-fields-routes.js.map +1 -0
- package/src/lib/server/record-email-state.js +5 -2
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-timeline.js +3 -2
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/task-routes.js +6 -3
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server/tasks-import.js +5 -2
- package/src/lib/server/tasks-import.js.map +1 -1
- package/src/lib/server.js +10 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/hooks/use-org-leads.d.ts +0 -50
- package/src/lib/hooks/use-org-leads.js +0 -104
- package/src/lib/hooks/use-org-leads.js.map +0 -1
|
@@ -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
|
|
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 {
|
|
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 {
|
|
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";
|
|
@@ -33,7 +37,7 @@ import EmptyStateComponent from "@aglyn/shared-ui-jsx/components/empty-state.com
|
|
|
33
37
|
import ContactsBulkBar from "./contacts-bulk-bar.js";
|
|
34
38
|
import { ContactImportButton } from "./contact-import-drawer.js";
|
|
35
39
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
36
|
-
import {
|
|
40
|
+
import { useFirestore, useFirestoreCollection, useFirestoreDoc, useUser } from "@aglyn/tenant-feature-instance";
|
|
37
41
|
import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField, Typography } from "@mui/material";
|
|
38
42
|
import { addDoc, collection, deleteDoc, doc, limit, orderBy, query } from "firebase/firestore";
|
|
39
43
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
@@ -50,7 +54,7 @@ import { useCrmViewGrid } from "../hooks/use-crm-view-grid.js";
|
|
|
50
54
|
import { CRM_LIST_SLOTS, CrmColumnOrderProvider } from "./crm-column-menu.js";
|
|
51
55
|
import { useCompanyOptions } from "./company-picker.js";
|
|
52
56
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
53
|
-
import { listFilterGridColumns
|
|
57
|
+
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
54
58
|
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
55
59
|
import CrmViewsControl from "./crm-views-control.js";
|
|
56
60
|
import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-suite-lock.js";
|
|
@@ -60,13 +64,6 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
60
64
|
* The map lives beside the `ContactSource` union so the dynamic-list rule
|
|
61
65
|
* editor and this filter cannot disagree about what `order` is called.
|
|
62
66
|
*/ 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
67
|
/**
|
|
71
68
|
* What the list keeps out of sight until a view says otherwise: the
|
|
72
69
|
* filter-only columns, which never show, and the optional columns
|
|
@@ -108,8 +105,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
108
105
|
* body is the v1 page unchanged, and it takes the shell's full prop bag so
|
|
109
106
|
* nothing the v1 page read has to be threaded through the hub by name.
|
|
110
107
|
*/ export function ContactsPeopleSection(props) {
|
|
111
|
-
var _ref, _ref1, _ref2,
|
|
112
|
-
var _plan_served;
|
|
108
|
+
var _ref, _ref1, _ref2, _ref3, _records_contactsCount, _ref4, _ref5, _ref6, _ref7;
|
|
113
109
|
const { hostId, org, releaseFlag, basePath } = props;
|
|
114
110
|
/*
|
|
115
111
|
* A row is a LINK now (AGL-2596): opening a person navigates to their own
|
|
@@ -211,7 +207,9 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
211
207
|
...seed.filter ? [
|
|
212
208
|
seed.filter
|
|
213
209
|
] : [],
|
|
214
|
-
|
|
210
|
+
// A form's captures all came in through a form: the source beside it
|
|
211
|
+
// would ask a second array clause of the one query (AGL-3321).
|
|
212
|
+
...seed.source && !seed.formId ? [
|
|
215
213
|
{
|
|
216
214
|
field: CRM_CONTACT_VIEW_FIELDS.source,
|
|
217
215
|
op: 'equals',
|
|
@@ -247,97 +245,47 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
247
245
|
customFields.active
|
|
248
246
|
]);
|
|
249
247
|
/*
|
|
250
|
-
*
|
|
248
|
+
* EVERY CLAUSE AND THE SEARCH ON ONE QUERY (AGL-3321).
|
|
251
249
|
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
|
|
258
|
-
* Under a site the
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*/ const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
} : {}), [
|
|
270
|
-
filterFields,
|
|
250
|
+
* Each stored clause is asked through the field its writers keep
|
|
251
|
+
* (`contactQueryClause`): a holder's owner, stage, source, company, tags,
|
|
252
|
+
* custom value or "has bought" as that holder's facet key, the verdict as
|
|
253
|
+
* `emailStatus`, the rest as stored. The query pages the list newest
|
|
254
|
+
* change first and carries all of it; what one query cannot hold is
|
|
255
|
+
* refused by name above the list, and nothing is matched over the rows a
|
|
256
|
+
* page happened to load. Under a site the scope clause is the query's one
|
|
257
|
+
* array clause — the search folds into it, and a facet or form clause
|
|
258
|
+
* stands in its place, for a reader whose access is not limited to
|
|
259
|
+
* particular sites.
|
|
260
|
+
*/ const foldsScope = useCrmFoldsScope(orgId, visibleToTokens);
|
|
261
|
+
const groupId = (_ref3 = consentGroup == null ? void 0 : consentGroup.groupId) != null ? _ref3 : null;
|
|
262
|
+
const asked = useMemo(()=>crmAskClauses(views.state.filters, (clause)=>contactQueryClause(clause, {
|
|
263
|
+
groupId,
|
|
264
|
+
foldsScope
|
|
265
|
+
})), [
|
|
271
266
|
views.state.filters,
|
|
272
|
-
|
|
267
|
+
groupId,
|
|
268
|
+
foldsScope
|
|
273
269
|
]);
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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'
|
|
270
|
+
const impliesScope = useMemo(()=>contactClauseImpliesScope(groupId), [
|
|
271
|
+
groupId
|
|
272
|
+
]);
|
|
273
|
+
// The quick search is the grid's box, answered by the same query.
|
|
274
|
+
const [searchWords, setSearchWords] = useState([]);
|
|
275
|
+
const paged = useCrmListQuery({
|
|
276
|
+
scope: dataScope,
|
|
277
|
+
collection: 'contacts',
|
|
278
|
+
visibleTo: visibleToTokens,
|
|
279
|
+
foldsScope,
|
|
280
|
+
declaration: CONTACT_LIST_DECLARATION,
|
|
281
|
+
clauses: asked.clauses,
|
|
282
|
+
search: searchWords,
|
|
283
|
+
impliesScope,
|
|
284
|
+
soloClause: contactSoloClause,
|
|
285
|
+
prefixSearch: CONTACT_PREFIX_SEARCH
|
|
340
286
|
});
|
|
287
|
+
const contactDocs = paged.rows;
|
|
288
|
+
const contactsStatus = paged.status;
|
|
341
289
|
/*
|
|
342
290
|
* OPENED FOR ONE ADDRESS: move straight on to the record (AGL-2612).
|
|
343
291
|
*
|
|
@@ -353,15 +301,17 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
353
301
|
useEffect(()=>{
|
|
354
302
|
if (openedByEmail.current || !seed.openEmail) return;
|
|
355
303
|
if (contactsStatus !== 'success') return;
|
|
356
|
-
|
|
357
|
-
|
|
304
|
+
const served = paged.plan.served.some((clause)=>clause.field === 'email' && clause.op === 'equals' && clause.value === seed.openEmail);
|
|
305
|
+
if (!served) return;
|
|
306
|
+
const only = contactDocs.length === 1 && !paged.hasMore ? contactDocs[0] : null;
|
|
358
307
|
if (!only) return;
|
|
359
308
|
openedByEmail.current = true;
|
|
360
309
|
router.replace(routes.contact(String(only.$id)));
|
|
361
310
|
}, [
|
|
362
311
|
seed.openEmail,
|
|
363
312
|
contactsStatus,
|
|
364
|
-
|
|
313
|
+
paged.plan,
|
|
314
|
+
paged.hasMore,
|
|
365
315
|
contactDocs,
|
|
366
316
|
router,
|
|
367
317
|
routes
|
|
@@ -377,7 +327,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
377
327
|
* The canonical `name` is the fallback and the shared identity; a holder's
|
|
378
328
|
* own override wins, so one business renaming a person cannot change what
|
|
379
329
|
* an unrelated business sees.
|
|
380
|
-
*/ const contacts = useMemo(()=>
|
|
330
|
+
*/ const contacts = useMemo(()=>contactDocs.map((row)=>contactRecordFromDoc(row, consentGroup != null ? consentGroup : contactPrimaryGroup(row, org))), [
|
|
381
331
|
contactDocs,
|
|
382
332
|
consentGroup,
|
|
383
333
|
org
|
|
@@ -399,10 +349,10 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
399
349
|
/**
|
|
400
350
|
* The HEAD-COUNT, read as a server-side aggregate (AGL-1706).
|
|
401
351
|
*
|
|
402
|
-
* The
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* saturated at 1,000, which is *exactly* the smallest paid included band
|
|
352
|
+
* The list above reads a page at a time (AGL-3321) — nobody needs 40,000
|
|
353
|
+
* rows streamed into a table. What it must not do is answer "how many
|
|
354
|
+
* contacts does this org have", and it did when it was a `limit(1000)`
|
|
355
|
+
* window: `contacts.length` saturated at 1,000, which is *exactly* the smallest paid included band
|
|
406
356
|
* (`starter`). So `overageContacts = max(0, used − included)` was 0 on
|
|
407
357
|
* every stock plan and the alert below could not render at all.
|
|
408
358
|
*
|
|
@@ -450,7 +400,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
450
400
|
firestore,
|
|
451
401
|
hostId
|
|
452
402
|
]);
|
|
453
|
-
const droppedTotal = Number((
|
|
403
|
+
const droppedTotal = Number((_ref4 = droppedCounter == null ? void 0 : droppedCounter['total']) != null ? _ref4 : 0);
|
|
454
404
|
/*==========================================
|
|
455
405
|
* REFUNDS THAT REACHED NO CONTACT (AGL-2329).
|
|
456
406
|
*
|
|
@@ -470,10 +420,12 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
470
420
|
firestore,
|
|
471
421
|
hostId
|
|
472
422
|
]);
|
|
473
|
-
const unmatchedRefundTotal = Number((
|
|
474
|
-
const unmatchedRefundReason = String((
|
|
423
|
+
const unmatchedRefundTotal = Number((_ref5 = unmatchedRefundCounter == null ? void 0 : unmatchedRefundCounter['total']) != null ? _ref5 : 0);
|
|
424
|
+
const unmatchedRefundReason = String((_ref6 = unmatchedRefundCounter == null ? void 0 : unmatchedRefundCounter['lastReason']) != null ? _ref6 : '');
|
|
475
425
|
// Saved segments (AGL-199): reusable audience filters.
|
|
476
|
-
const { data: segmentDocs } = useFirestoreCollection(()=>dataScope ? query(collection(firestore, dataScope[0], dataScope[1], 'contactSegments'),
|
|
426
|
+
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
|
|
427
|
+
// whichever fifty the ids happen to sort first.
|
|
428
|
+
orderBy('name'), limit(50)) : null, [
|
|
477
429
|
firestore,
|
|
478
430
|
dataScope
|
|
479
431
|
], {
|
|
@@ -510,32 +462,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
510
462
|
dataScope,
|
|
511
463
|
firestore
|
|
512
464
|
]);
|
|
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
|
-
]);
|
|
465
|
+
const uid = (_ref7 = user == null ? void 0 : user.uid) != null ? _ref7 : '';
|
|
539
466
|
/*
|
|
540
467
|
* The choices the filter bar picks from. The companies are read only once
|
|
541
468
|
* the reader has reached for a filter or is looking at a view that names
|
|
@@ -595,6 +522,18 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
595
522
|
companies.options,
|
|
596
523
|
customFields.active
|
|
597
524
|
]);
|
|
525
|
+
// What the query could not hold, named by the clause the reader set.
|
|
526
|
+
const refused = useMemo(()=>crmQueryRefusals(paged.plan, asked, {
|
|
527
|
+
fields: filterFields,
|
|
528
|
+
headers: filterHeaders,
|
|
529
|
+
options: filterOptions
|
|
530
|
+
}), [
|
|
531
|
+
paged.plan,
|
|
532
|
+
asked,
|
|
533
|
+
filterFields,
|
|
534
|
+
filterHeaders,
|
|
535
|
+
filterOptions
|
|
536
|
+
]);
|
|
598
537
|
/* One row grammar, the console's (AGL-2501) — the same table everywhere. */ const contactColumns = useMemo(()=>[
|
|
599
538
|
...contactListColumns({
|
|
600
539
|
memberName: members.memberName,
|
|
@@ -688,6 +627,12 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
688
627
|
enqueueSnackbar
|
|
689
628
|
]);
|
|
690
629
|
/** The rows ticked for a bulk action (AGL-2603); the bar above the table acts on them. */ const [selectedIds, setSelectedIds] = useState([]);
|
|
630
|
+
// A filter, a search or a page is a different set of rows; a selection
|
|
631
|
+
// made on the last one would be a count over rows no longer on screen.
|
|
632
|
+
useEffect(()=>setSelectedIds([]), [
|
|
633
|
+
paged.plan,
|
|
634
|
+
paged.page
|
|
635
|
+
]);
|
|
691
636
|
/*==========================================
|
|
692
637
|
* ADDING ONE PERSON BY HAND (AGL-2596).
|
|
693
638
|
*
|
|
@@ -778,9 +723,9 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
778
723
|
customFields.active
|
|
779
724
|
]);
|
|
780
725
|
const handleExport = useCallback(()=>{
|
|
781
|
-
downloadTextFile('contacts.csv', 'text/csv', contactsCsv(
|
|
726
|
+
downloadTextFile('contacts.csv', 'text/csv', contactsCsv(contacts, csvOptions));
|
|
782
727
|
}, [
|
|
783
|
-
|
|
728
|
+
contacts,
|
|
784
729
|
csvOptions
|
|
785
730
|
]);
|
|
786
731
|
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
@@ -821,7 +766,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
821
766
|
/*#__PURE__*/ _jsx(Button, {
|
|
822
767
|
size: "small",
|
|
823
768
|
onClick: handleExport,
|
|
824
|
-
disabled: !
|
|
769
|
+
disabled: !contacts.length,
|
|
825
770
|
children: 'Export CSV'
|
|
826
771
|
}),
|
|
827
772
|
suiteIncluded ? /*#__PURE__*/ _jsx(Button, {
|
|
@@ -860,7 +805,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
860
805
|
clauses: views.state.filters,
|
|
861
806
|
onChange: views.setFilters,
|
|
862
807
|
options: filterOptions,
|
|
863
|
-
|
|
808
|
+
marksServed: false
|
|
864
809
|
})
|
|
865
810
|
]
|
|
866
811
|
}),
|
|
@@ -898,10 +843,11 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
898
843
|
severity: "warning",
|
|
899
844
|
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
845
|
}) : null,
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
})
|
|
846
|
+
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
847
|
+
refused: refused,
|
|
848
|
+
notices: paged.plan.notices
|
|
849
|
+
}),
|
|
850
|
+
contacts.length === 0 && !views.state.filters.length && !searchWords.some((word)=>word.trim()) ? /*#__PURE__*/ _jsx(EmptyStateComponent, {
|
|
905
851
|
label: contactsStatus === 'loading' ? 'Loading contacts…' : 'No contacts yet',
|
|
906
852
|
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
853
|
action: contactsStatus === 'loading' ? undefined : !suiteIncluded ? /*#__PURE__*/ _jsxs(Stack, {
|
|
@@ -940,17 +886,12 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
940
886
|
})
|
|
941
887
|
}) : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
942
888
|
children: [
|
|
943
|
-
windowClauses.length ? /*#__PURE__*/ _jsx(Typography, {
|
|
944
|
-
variant: "caption",
|
|
945
|
-
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.'
|
|
947
|
-
}) : null,
|
|
948
889
|
/*#__PURE__*/ _jsx(ContactsBulkBar, {
|
|
949
890
|
hostId: hostId,
|
|
950
891
|
org: org,
|
|
951
892
|
scope: dataScope,
|
|
952
893
|
consentGroup: consentGroup,
|
|
953
|
-
rows:
|
|
894
|
+
rows: contacts,
|
|
954
895
|
selected: selectedIds,
|
|
955
896
|
onSelectedChange: setSelectedIds,
|
|
956
897
|
csv: csvOptions,
|
|
@@ -959,7 +900,7 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
959
900
|
/*#__PURE__*/ _jsx(CrmColumnOrderProvider, {
|
|
960
901
|
value: grid.columnOrder,
|
|
961
902
|
children: /*#__PURE__*/ _jsx(ListTable, {
|
|
962
|
-
rows:
|
|
903
|
+
rows: contacts,
|
|
963
904
|
columns: grid.columns,
|
|
964
905
|
slots: CRM_LIST_SLOTS,
|
|
965
906
|
selectable: {
|
|
@@ -969,12 +910,12 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
969
910
|
onOpen: (id)=>router.push(routes.contact(id)),
|
|
970
911
|
/*
|
|
971
912
|
* The grid's Filters panel and quick search edit the view's
|
|
972
|
-
* clauses; the list answers them,
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
913
|
+
* clauses; the list's query answers them (AGL-3321), so the
|
|
914
|
+
* grid must not filter again itself. Opening the panel is
|
|
915
|
+
* what reads the roster and the companies its pickers name.
|
|
916
|
+
* Columns are the view's, controlled so a saved arrangement
|
|
917
|
+
* is what the grid shows; the query orders the rows —
|
|
918
|
+
* newest change first — so the grid sorts nothing itself.
|
|
978
919
|
*/ filterMode: "server",
|
|
979
920
|
filterModel: gridFilter.filterModel,
|
|
980
921
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
@@ -985,9 +926,20 @@ import { CrmSuiteLockedButton, CrmSuiteNotice, crmSuiteIncluded } from "./crm-su
|
|
|
985
926
|
noRowsLabel: contactsStatus === 'loading' ? 'Loading…' : 'No contacts match these filters',
|
|
986
927
|
columnVisibilityModel: grid.columnVisibilityModel,
|
|
987
928
|
onColumnVisibilityModelChange: grid.onColumnVisibilityModelChange,
|
|
988
|
-
|
|
989
|
-
|
|
929
|
+
sortingMode: "server",
|
|
930
|
+
disableColumnSorting: true,
|
|
931
|
+
loading: contactsStatus === 'loading',
|
|
932
|
+
// Paged by the footer below, so the grid must not also slice.
|
|
933
|
+
hideFooter: true
|
|
990
934
|
})
|
|
935
|
+
}),
|
|
936
|
+
/*#__PURE__*/ _jsx(ListPagination, {
|
|
937
|
+
page: paged.page,
|
|
938
|
+
pageSize: paged.pageSize,
|
|
939
|
+
rowCount: contacts.length,
|
|
940
|
+
hasMore: paged.hasMore,
|
|
941
|
+
onPageChange: paged.setPage,
|
|
942
|
+
onPageSizeChange: paged.setPageSize
|
|
991
943
|
})
|
|
992
944
|
]
|
|
993
945
|
}),
|