@aglyn/plugins-crm 1.0.0-beta.199 → 1.0.0-beta.200
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/lib/components/companies-section.d.ts +6 -6
- package/src/lib/components/companies-section.js +54 -84
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +17 -4
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-picker.js +6 -2
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/contact-detail-page.js +14 -2
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -5
- package/src/lib/components/contact-list-columns.js +6 -8
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.js +10 -1
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +115 -163
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.d.ts +4 -2
- package/src/lib/components/crm-next-activity-column.js +6 -3
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +8 -4
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deals-section.d.ts +2 -2
- package/src/lib/components/deals-section.js +59 -84
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.js +11 -3
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +9 -1
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +16 -3
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.d.ts +2 -1
- package/src/lib/components/leads-section.js +110 -148
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/tasks-section.js +128 -121
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +13 -0
- package/src/lib/constants/company-filters.js +47 -17
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +61 -10
- package/src/lib/constants/contact-filters.js +373 -141
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +44 -0
- package/src/lib/constants/deal-filters.js +78 -0
- package/src/lib/constants/deal-filters.js.map +1 -0
- package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
- package/src/lib/hooks/use-crm-list-query.js +182 -0
- package/src/lib/hooks/use-crm-list-query.js.map +1 -0
- package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
- package/src/lib/hooks/use-crm-tasks.js +44 -42
- package/src/lib/hooks/use-crm-tasks.js.map +1 -1
- package/src/lib/hooks/use-deals.d.ts +2 -34
- package/src/lib/hooks/use-deals.js +3 -62
- package/src/lib/hooks/use-deals.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +91 -0
- package/src/lib/model/crm-list-query.js +182 -0
- package/src/lib/model/crm-list-query.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +49 -43
- package/src/lib/model/lead-filters.js +194 -88
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/list-fields-api.d.ts +26 -0
- package/src/lib/model/list-fields-api.js +47 -0
- package/src/lib/model/list-fields-api.js.map +1 -0
- package/src/lib/model/list-fields.d.ts +47 -0
- package/src/lib/model/list-fields.js +39 -0
- package/src/lib/model/list-fields.js.map +1 -0
- package/src/lib/model/task-views.d.ts +15 -10
- package/src/lib/model/task-views.js +68 -9
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/companies-import.js +3 -3
- package/src/lib/server/companies-import.js.map +1 -1
- package/src/lib/server/company-delete.js +3 -1
- package/src/lib/server/company-delete.js.map +1 -1
- package/src/lib/server/consent-group-participant.js +4 -0
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-update.js +14 -8
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.js +4 -2
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/deals-import.js +4 -2
- package/src/lib/server/deals-import.js.map +1 -1
- package/src/lib/server/erase-person.js +2 -1
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/lead-create.js +4 -1
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-source-values.js +9 -5
- package/src/lib/server/lead-source-values.js.map +1 -1
- package/src/lib/server/leads-import.js +4 -1
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server/list-fields-routes.d.ts +29 -0
- package/src/lib/server/list-fields-routes.js +85 -0
- package/src/lib/server/list-fields-routes.js.map +1 -0
- package/src/lib/server/record-email-state.js +5 -2
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-timeline.js +3 -2
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/task-routes.js +6 -3
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server/tasks-import.js +5 -2
- package/src/lib/server/tasks-import.js.map +1 -1
- package/src/lib/server.js +10 -1
- package/src/lib/server.js.map +1 -1
- package/src/lib/hooks/use-org-leads.d.ts +0 -50
- package/src/lib/hooks/use-org-leads.js +0 -104
- package/src/lib/hooks/use-org-leads.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/crm-list-query.ts"],"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\nimport {\n CRM_SCOPED_SEARCH_TOKENS_FIELD,\n CRM_SEARCH_TOKENS_FIELD,\n type CrmListCollection,\n crmListFields,\n} from '@aglyn/aglyn'\nimport { scopedSearch } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { listQueryRefusals } from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport type {\n ListQueryDeclaration,\n ListQueryFilter,\n ListQueryPlan,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\n\n/*\n * THE CRM LISTS ON THEIR QUERIES (AGL-3321).\n *\n * Every CRM list — Leads, Contacts, Companies, Deals, Tasks — declares its\n * filterable fields, its one order and its search as a\n * `ListQueryDeclaration`, and `planListQuery` puts every clause and the\n * search word on ONE Firestore query paged by `useListQuery`. What a query\n * cannot hold is refused by name (`ListQueryNotices`), never matched over\n * the rows a page happened to load.\n *\n * This module is what the five share: the search every CRM record carries\n * (`searchTokens`, and `scopedSearchTokens` under a site — see\n * `crmListFields` in `@aglyn/aglyn`), the scope clause as the plan's base,\n * a stored clause translated to the one its query asks, and the refusals\n * read back through the clause the reader set.\n */\n\n/** Every CRM list's quick search: the tokens every writer stamps. */\nexport const CRM_LIST_SEARCH: NonNullable<ListQueryDeclaration['search']> = {\n tokensPath: CRM_SEARCH_TOKENS_FIELD,\n scoped: scopedSearch(CRM_SCOPED_SEARCH_TOKENS_FIELD),\n}\n\n/**\n * The scope clause as the plan's base: `visibleTo array-contains-any` over\n * the reader's tokens under a site, nothing at the organization level\n * (`null` tokens), where an org-wide member's listeners carry none.\n */\nexport function crmListBase(visibleTo: readonly string[] | null): ListQueryFilter[] {\n return visibleTo ? [{ path: 'visibleTo', op: 'array-contains-any', value: [...visibleTo] }] : []\n}\n\n/** The base's shape, for `listQueryIndexes`: the scope clause is an array clause. */\nexport const CRM_LIST_BASE_INDEX = [{ path: 'visibleTo', array: true }] as const\n\n/**\n * The declaration a reader may run.\n *\n * Under a site the search folds into the scope clause (`search.scoped`),\n * and a query without `visibleTo` is one the rules can prove only for an\n * ORG-WIDE member — `canReadScoped()` short-circuits on it, and for anybody\n * else inspects `visibleTo`, which the folded query no longer constrains.\n * A member whose reach is some sites keeps the scope clause, and the plan\n * refuses their search by name rather than sending a query the rules deny.\n */\nexport function crmListDeclarationFor(\n declaration: ListQueryDeclaration,\n foldsScope: boolean,\n): ListQueryDeclaration {\n if (foldsScope || !declaration.search?.scoped) return declaration\n return { ...declaration, search: { tokensPath: declaration.search.tokensPath } }\n}\n\n/*------------------------------------------\n * WHAT A LIST STORES, AND WHAT IT ASKS.\n *\n * A saved view stores clauses in the grid's grammar — \"Status is Open\",\n * \"Owner is Dana\", \"Lead source is none\" — and some of them name no stored\n * field one query can ask as it is: Open is two statuses, an owner under a\n * site is that holder's facet key, a lead source is compared by its key. A\n * list translates each stored clause into the clause its query asks, plans\n * those, and reads the plan's refusals back through the stored clause, so\n * the notice names what the reader set (\"Status is Open is not applied\")\n * and never the field it was translated to.\n *-----------------------------------------*/\n\n/** How one stored clause is asked: a query clause, or why it cannot be. */\nexport type CrmClauseAsked = ListFilterRequest | { refused: string }\n\nexport interface CrmAskedClauses {\n /** What the plan is asked, in the order the reader set them. */\n clauses: ListFilterRequest[]\n /** The stored clause each asked one stands for, index for index. */\n origins: ListFilterRequest[]\n /** Stored clauses refused before the plan saw them, with why. */\n refused: Array<{ clause: ListFilterRequest; reason: string }>\n}\n\n/** Every stored clause through `ask`, keeping where each came from. */\nexport function crmAskClauses(\n stored: readonly ListFilterRequest[],\n ask: (clause: ListFilterRequest) => CrmClauseAsked,\n): CrmAskedClauses {\n const clauses: ListFilterRequest[] = []\n const origins: ListFilterRequest[] = []\n const refused: CrmAskedClauses['refused'] = []\n for (const clause of stored) {\n const asked = ask(clause)\n if ('refused' in asked) {\n refused.push({ clause, reason: asked.refused })\n } else {\n clauses.push(asked)\n origins.push(clause)\n }\n }\n return { clauses, origins, refused }\n}\n\nconst sameClause = (a: ListFilterRequest, b: ListFilterRequest): boolean =>\n a.field === b.field && a.op === b.op && a.value === b.value\n\n/**\n * The plan's refusals and the translation's, each named by the clause the\n * reader SET, as `ListQueryNotices` takes them — through the platform's one\n * wording (`listQueryRefusals`), so a notice and its chip read alike.\n */\nexport function crmQueryRefusals(\n plan: Pick<ListQueryPlan, 'refused'>,\n asked: CrmAskedClauses,\n context: {\n fields: readonly ListFilterField[]\n headers?: Readonly<Record<string, string>>\n options?: Readonly<Record<string, readonly ListFilterOption[]>>\n },\n): Array<{ label: string; reason: string }> {\n const planned = plan.refused.map((entry) => {\n if (entry.clause === 'search') return entry\n const clause = entry.clause\n const at = asked.clauses.findIndex((candidate) => sameClause(candidate, clause))\n return { clause: at >= 0 ? asked.origins[at] : clause, reason: entry.reason }\n })\n return listQueryRefusals([...asked.refused, ...planned], context)\n}\n\n/** The comma-separated values of an `isAnyOf`, or the one value of anything else. */\nexport function crmClauseValues(clause: Pick<ListFilterRequest, 'op' | 'value'>): string[] {\n const raw = clause.op === 'isAnyOf' ? clause.value.split(',') : [clause.value]\n return [...new Set(raw.map((value) => value.trim()).filter(Boolean))]\n}\n\n/** One value as `equals`, several as `isAnyOf` — the shape the plan takes a list in. */\nexport function crmAnyOf(field: string, values: readonly string[]): CrmClauseAsked {\n const unique = [...new Set(values)]\n if (!unique.length) return { refused: 'no value yet' }\n return unique.length === 1\n ? { field, op: 'equals', value: unique[0] }\n : { field, op: 'isAnyOf', value: unique.join(',') }\n}\n\n/** A `select` field: equality and \"any of\" over stored values, nothing else. */\nexport function crmSelectField(column: string, path: string = column): ListFilterField {\n return { column, kind: 'exact', path, operators: ['equals', 'isAnyOf'] }\n}\n\n/**\n * The list fields a CLIENT edit writes beside its patch (AGL-3321): the\n * record as the listener holds it with the patch laid over — a `null` or a\n * sentinel in the patch reads as the field removed — through\n * `crmListFields`, narrowed to `fields`. A client never writes a field the\n * rules keep for the server (a lead's `emailStatus`), which is why the\n * caller names the ones it may.\n */\nexport function crmClientListFields(\n collection: CrmListCollection,\n record: Readonly<Record<string, unknown>>,\n patch: Readonly<Record<string, unknown>>,\n fields: readonly string[],\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...record }\n for (const [key, value] of Object.entries(patch)) {\n // A dotted path is a map entry (`custom.tier`), never a searched field.\n if (key.includes('.')) continue\n merged[key] = value === null || isSentinel(value) ? undefined : value\n }\n const computed = crmListFields(collection, merged)\n return Object.fromEntries(fields.filter((field) => field in computed).map((field) => [field, computed[field]]))\n}\n\n/** A web-SDK `FieldValue` — `deleteField()`, the one a client edit of these fields sends. */\nconst isSentinel = (value: unknown): boolean =>\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date) &&\n typeof (value as { isEqual?: unknown }).isEqual === 'function'\n\n/** The search fields every CRM record carries — what a client edit of searched text rewrites. */\nexport const CRM_CLIENT_SEARCH_FIELDS = ['searchTokens', 'scopedSearchTokens'] as const\n\n"],"names":["CRM_SCOPED_SEARCH_TOKENS_FIELD","CRM_SEARCH_TOKENS_FIELD","crmListFields","scopedSearch","listQueryRefusals","CRM_LIST_SEARCH","tokensPath","scoped","crmListBase","visibleTo","path","op","value","CRM_LIST_BASE_INDEX","array","crmListDeclarationFor","declaration","foldsScope","search","crmAskClauses","stored","ask","clauses","origins","refused","clause","asked","push","reason","sameClause","a","b","field","crmQueryRefusals","plan","context","planned","map","entry","at","findIndex","candidate","crmClauseValues","raw","split","Set","trim","filter","Boolean","crmAnyOf","values","unique","length","join","crmSelectField","column","kind","operators","crmClientListFields","collection","record","patch","fields","merged","key","Object","entries","includes","isSentinel","undefined","computed","fromEntries","Array","isArray","Date","isEqual","CRM_CLIENT_SEARCH_FIELDS"],"mappings":"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"}
|
|
@@ -14,22 +14,22 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type EmailStateStatus, type
|
|
17
|
+
import { type EmailStateStatus, type CrmLeadStatus } from '@aglyn/aglyn';
|
|
18
18
|
import type { CrmViewFilterClause } from '@aglyn/aglyn';
|
|
19
|
-
import type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter';
|
|
19
|
+
import type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter';
|
|
20
20
|
import { type ListGridFilterCodec } from '@aglyn/shared-ui-jsx/const/list-grid-filter';
|
|
21
|
+
import type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan';
|
|
22
|
+
import { type CrmClauseAsked } from './crm-list-query';
|
|
21
23
|
/**
|
|
22
24
|
* What the Leads section's `Show` control offers (AGL-2608): the two
|
|
23
25
|
* aggregate views and each status on its own. `open` is the default — the
|
|
24
|
-
* list opens on the work —
|
|
25
|
-
*
|
|
26
|
-
*
|
|
26
|
+
* list opens on the work — asked of the query as `status in [new, working]`,
|
|
27
|
+
* which every lead answers because every lead writer stores a status, `new`
|
|
28
|
+
* for one nobody has touched (`crmLeadListFields`, AGL-3321).
|
|
27
29
|
*/
|
|
28
30
|
export type LeadFilter = 'open' | 'all' | CrmLeadStatus;
|
|
29
31
|
export declare const LEAD_FILTERS: readonly LeadFilter[];
|
|
30
32
|
export declare const LEAD_FILTER_LABELS: Record<LeadFilter, string>;
|
|
31
|
-
/** Whether a lead belongs in a filter's view. An absent status is `new`. */
|
|
32
|
-
export declare function leadMatchesFilter(lead: Pick<CrmLeadFields, 'status'>, filter: LeadFilter): boolean;
|
|
33
33
|
/**
|
|
34
34
|
* The Leads section's `Email` control (AGL-3245): every lead, the ones a
|
|
35
35
|
* member must not email — any verdict but `ok` — the ones nothing has been
|
|
@@ -40,46 +40,12 @@ export declare function leadMatchesFilter(lead: Pick<CrmLeadFields, 'status'>, f
|
|
|
40
40
|
export type LeadEmailFilter = 'any' | 'problem' | 'none' | EmailStateStatus;
|
|
41
41
|
export declare const LEAD_EMAIL_FILTERS: readonly LeadEmailFilter[];
|
|
42
42
|
export declare const LEAD_EMAIL_FILTER_LABELS: Record<LeadEmailFilter, string>;
|
|
43
|
-
/** Whether a lead belongs in an email filter's view. */
|
|
44
|
-
export declare function leadMatchesEmailFilter(lead: Readonly<Record<string, unknown>>, filter: LeadEmailFilter): boolean;
|
|
45
|
-
/**
|
|
46
|
-
* The Leads section's `Campaign` control (AGL-3254): every lead, or the
|
|
47
|
-
* ones filed under one campaign — read off the lead's own `campaignIds`,
|
|
48
|
-
* the field every campaign member carries. `''` is every lead, which is
|
|
49
|
-
* what the control opens on; the ids are the site's containers', and the
|
|
50
|
-
* section resolves their names for the menu.
|
|
51
|
-
*/
|
|
52
|
-
export declare function leadMatchesCampaignFilter(lead: Readonly<Record<string, unknown>>, campaignId: string): boolean;
|
|
53
|
-
/**
|
|
54
|
-
* The fields the Leads section's search box reads (AGL-3246): who the lead
|
|
55
|
-
* is and where they work — the words a person types to find one — plus the
|
|
56
|
-
* tags, which are how an import names its batch (`sal-15`). `name` and
|
|
57
|
-
* `email` are the capture door's fields rather than the CRM's, so the row is
|
|
58
|
-
* read as a record rather than through `CrmLeadFields`.
|
|
59
|
-
*/
|
|
60
|
-
export declare const LEAD_SEARCH_FIELDS: readonly ["name", "email", "company", "jobTitle", "tags"];
|
|
61
|
-
/**
|
|
62
|
-
* Whether a lead answers a search term.
|
|
63
|
-
*
|
|
64
|
-
* Case-insensitive, and every whitespace-separated word of the term must
|
|
65
|
-
* appear in SOME searched field: `morgan lamphere` and `lamphere sal-15`
|
|
66
|
-
* both find Morgan Lamphere, `morgan smith` does not. A blank term matches
|
|
67
|
-
* every lead, so the box emptied is the list unfiltered.
|
|
68
|
-
*/
|
|
69
|
-
export declare function leadMatchesSearch(lead: Readonly<Record<string, unknown>>, term: string): boolean;
|
|
70
43
|
/**
|
|
71
44
|
* The `Lead source` control's "no lead source" choice (AGL-3298). Held in
|
|
72
45
|
* the saved view as an `isEmpty` clause, never as this string; it is only
|
|
73
46
|
* the select's own value for the choice.
|
|
74
47
|
*/
|
|
75
48
|
export declare const LEAD_SOURCE_FILTER_NONE = "\0none";
|
|
76
|
-
/**
|
|
77
|
-
* The Leads section's `Lead source` control (AGL-3298): every lead (`''`),
|
|
78
|
-
* the ones holding one value — compared the way the picklist compares
|
|
79
|
-
* labels, so a value typed in another case before the list existed is
|
|
80
|
-
* still found — or the ones holding none.
|
|
81
|
-
*/
|
|
82
|
-
export declare function leadMatchesLeadSourceFilter(lead: Readonly<Record<string, unknown>>, filter: string): boolean;
|
|
83
49
|
/** The fields the grid's panel offers on Leads. */
|
|
84
50
|
export declare const LEAD_LIST_FILTER_FIELDS: readonly ListFilterField[];
|
|
85
51
|
/** What each filter field reads as on a chip. */
|
|
@@ -103,11 +69,51 @@ export declare const LEAD_EMAIL_FILTER_OPTIONS: {
|
|
|
103
69
|
export declare function leadClausesForGrid(stored: readonly CrmViewFilterClause[]): CrmViewFilterClause[];
|
|
104
70
|
/** The inverse of {@link leadClausesForGrid}: what the view stores. */
|
|
105
71
|
export declare function leadClausesToStore(shown: readonly CrmViewFilterClause[]): CrmViewFilterClause[];
|
|
106
|
-
/** Whether a lead answers every clause the grid shows. */
|
|
107
|
-
export declare function leadMatchesClauses(lead: Readonly<Record<string, unknown>> & Pick<CrmLeadFields, 'status'>, clauses: readonly CrmViewFilterClause[]): boolean;
|
|
108
72
|
/** Campaign clauses were stored as `contains`; the panel's select says `is`. */
|
|
109
73
|
export declare const LEAD_CAMPAIGN_CODEC: ListGridFilterCodec;
|
|
110
74
|
/** "No lead source" is an `isEmpty` clause, shown as a choice of the select. */
|
|
111
75
|
export declare const LEAD_SOURCE_CODEC: ListGridFilterCodec;
|
|
112
76
|
/** The Leads fields whose stored clauses translate their own way. */
|
|
113
77
|
export declare const LEAD_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>>;
|
|
78
|
+
/** The one order the Leads list takes: newest seen first. */
|
|
79
|
+
export declare const LEAD_LIST_SORTS: readonly ListQuerySort[];
|
|
80
|
+
/** The fields the Leads query asks — each a stored field, as named. */
|
|
81
|
+
export declare const LEAD_QUERY_FIELDS: readonly ListFilterField[];
|
|
82
|
+
export declare const LEAD_LIST_DECLARATION: ListQueryDeclaration;
|
|
83
|
+
/**
|
|
84
|
+
* A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the
|
|
85
|
+
* start of the address, beside the scope clause — the `(visibleTo, email)`
|
|
86
|
+
* composite the Inbox's leads list reads too.
|
|
87
|
+
*/
|
|
88
|
+
export declare const LEAD_PREFIX_SEARCH: {
|
|
89
|
+
field: {
|
|
90
|
+
column: string;
|
|
91
|
+
kind: "text";
|
|
92
|
+
path: string;
|
|
93
|
+
lowerPath: string;
|
|
94
|
+
presence: "always";
|
|
95
|
+
operators: string[];
|
|
96
|
+
};
|
|
97
|
+
notice: string;
|
|
98
|
+
};
|
|
99
|
+
/** Whether an asked clause stands in for the scope clause: a campaign behind the site's scope. */
|
|
100
|
+
export declare const leadClauseImpliesScope: (clause: ListFilterRequest) => boolean;
|
|
101
|
+
/** Who is asking, for the one clause whose shape depends on it: the Campaign. */
|
|
102
|
+
export interface LeadQueryReader {
|
|
103
|
+
/** The site's scope tokens, or `null` at the organization level. */
|
|
104
|
+
scopeTokens: readonly string[] | null;
|
|
105
|
+
/** Whether the reader may run a query without the scope clause (`useCrmFoldsScope`). */
|
|
106
|
+
foldsScope: boolean;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* One grid clause as the clause the Leads query asks — see the block above.
|
|
110
|
+
* A value the list does not know is refused by name rather than dropped, so
|
|
111
|
+
* a stored view that names a retired choice says so.
|
|
112
|
+
*/
|
|
113
|
+
export declare function leadQueryClause(clause: CrmViewFilterClause | ListFilterRequest, reader?: LeadQueryReader): CrmClauseAsked;
|
|
114
|
+
/**
|
|
115
|
+
* The query shape the Leads list can send beyond its own declaration, for
|
|
116
|
+
* the index pin (AGL-3321): a collaborator's address prefix beside the
|
|
117
|
+
* equalities they may set.
|
|
118
|
+
*/
|
|
119
|
+
export declare function leadIndexShapes(): ListQueryDeclaration[];
|
|
@@ -14,8 +14,10 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { EMAIL_STATE_LABELS, EMAIL_STATE_STATUSES, CRM_LEAD_STATUS_LABELS,
|
|
18
|
-
import {
|
|
17
|
+
*/ import { EMAIL_STATE_LABELS, EMAIL_STATE_STATUSES, CRM_LEAD_STATUS_LABELS, CRM_LEAD_SCOPED_CAMPAIGNS_FIELD, CRM_LEAD_STATUSES, crmLeadSourceKey } from "@aglyn/aglyn";
|
|
18
|
+
import { SCOPED_SEARCH_JOIN } from "@aglyn/aglyn/app-utils/name-search";
|
|
19
|
+
import { listSelectCodec } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
20
|
+
import { CRM_LIST_SEARCH, crmAnyOf, crmClauseValues, crmSelectField } from "./crm-list-query.js";
|
|
19
21
|
export const LEAD_FILTERS = [
|
|
20
22
|
'open',
|
|
21
23
|
'new',
|
|
@@ -32,11 +34,6 @@ export const LEAD_FILTER_LABELS = {
|
|
|
32
34
|
qualified: CRM_LEAD_STATUS_LABELS.qualified,
|
|
33
35
|
unqualified: CRM_LEAD_STATUS_LABELS.unqualified
|
|
34
36
|
};
|
|
35
|
-
/** Whether a lead belongs in a filter's view. An absent status is `new`. */ export function leadMatchesFilter(lead, filter) {
|
|
36
|
-
if (filter === 'all') return true;
|
|
37
|
-
if (filter === 'open') return isCrmLeadOpen(lead);
|
|
38
|
-
return crmLeadStatus(lead) === filter;
|
|
39
|
-
}
|
|
40
37
|
export const LEAD_EMAIL_FILTERS = [
|
|
41
38
|
'any',
|
|
42
39
|
'problem',
|
|
@@ -48,66 +45,11 @@ export const LEAD_EMAIL_FILTER_LABELS = _extends({
|
|
|
48
45
|
problem: 'Cannot be emailed',
|
|
49
46
|
none: 'Nothing known'
|
|
50
47
|
}, EMAIL_STATE_LABELS);
|
|
51
|
-
/** Whether a lead belongs in an email filter's view. */ export function leadMatchesEmailFilter(lead, filter) {
|
|
52
|
-
if (filter === 'any') return true;
|
|
53
|
-
const state = readEmailState(lead);
|
|
54
|
-
if (filter === 'none') return state === null;
|
|
55
|
-
if (filter === 'problem') return emailStateForbidsEmail(state);
|
|
56
|
-
return (state == null ? void 0 : state.status) === filter;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* The Leads section's `Campaign` control (AGL-3254): every lead, or the
|
|
60
|
-
* ones filed under one campaign — read off the lead's own `campaignIds`,
|
|
61
|
-
* the field every campaign member carries. `''` is every lead, which is
|
|
62
|
-
* what the control opens on; the ids are the site's containers', and the
|
|
63
|
-
* section resolves their names for the menu.
|
|
64
|
-
*/ export function leadMatchesCampaignFilter(lead, campaignId) {
|
|
65
|
-
if (!campaignId) return true;
|
|
66
|
-
return readCampaignIds(lead).includes(campaignId);
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* The fields the Leads section's search box reads (AGL-3246): who the lead
|
|
70
|
-
* is and where they work — the words a person types to find one — plus the
|
|
71
|
-
* tags, which are how an import names its batch (`sal-15`). `name` and
|
|
72
|
-
* `email` are the capture door's fields rather than the CRM's, so the row is
|
|
73
|
-
* read as a record rather than through `CrmLeadFields`.
|
|
74
|
-
*/ export const LEAD_SEARCH_FIELDS = [
|
|
75
|
-
'name',
|
|
76
|
-
'email',
|
|
77
|
-
'company',
|
|
78
|
-
'jobTitle',
|
|
79
|
-
'tags'
|
|
80
|
-
];
|
|
81
|
-
/**
|
|
82
|
-
* Whether a lead answers a search term.
|
|
83
|
-
*
|
|
84
|
-
* Case-insensitive, and every whitespace-separated word of the term must
|
|
85
|
-
* appear in SOME searched field: `morgan lamphere` and `lamphere sal-15`
|
|
86
|
-
* both find Morgan Lamphere, `morgan smith` does not. A blank term matches
|
|
87
|
-
* every lead, so the box emptied is the list unfiltered.
|
|
88
|
-
*/ export function leadMatchesSearch(lead, term) {
|
|
89
|
-
// Tags are a list; a stray string there is no tag of the lead's.
|
|
90
|
-
const tags = Array.isArray(lead['tags']) ? lead['tags'] : undefined;
|
|
91
|
-
return listRowMatchesSearch(_extends({}, lead, {
|
|
92
|
-
tags
|
|
93
|
-
}), LEAD_SEARCH_FIELDS, term.trim().split(/\s+/));
|
|
94
|
-
}
|
|
95
48
|
/**
|
|
96
49
|
* The `Lead source` control's "no lead source" choice (AGL-3298). Held in
|
|
97
50
|
* the saved view as an `isEmpty` clause, never as this string; it is only
|
|
98
51
|
* the select's own value for the choice.
|
|
99
52
|
*/ export const LEAD_SOURCE_FILTER_NONE = '\u0000none';
|
|
100
|
-
/**
|
|
101
|
-
* The Leads section's `Lead source` control (AGL-3298): every lead (`''`),
|
|
102
|
-
* the ones holding one value — compared the way the picklist compares
|
|
103
|
-
* labels, so a value typed in another case before the list existed is
|
|
104
|
-
* still found — or the ones holding none.
|
|
105
|
-
*/ export function leadMatchesLeadSourceFilter(lead, filter) {
|
|
106
|
-
if (!filter) return true;
|
|
107
|
-
const held = normalizeCrmPicklistLabel(lead['leadSource']).toLowerCase();
|
|
108
|
-
if (filter === LEAD_SOURCE_FILTER_NONE) return held === '';
|
|
109
|
-
return held === normalizeCrmPicklistLabel(filter).toLowerCase();
|
|
110
|
-
}
|
|
111
53
|
/*==========================================
|
|
112
54
|
* THE LEADS GRID'S FILTERS (AGL-3313).
|
|
113
55
|
*
|
|
@@ -120,8 +62,8 @@ export const LEAD_EMAIL_FILTER_LABELS = _extends({
|
|
|
120
62
|
* campaignIds `contains <campaign id>`
|
|
121
63
|
* leadSource `equals <label>`, or `isEmpty` for the leads holding none
|
|
122
64
|
*
|
|
123
|
-
* Every one
|
|
124
|
-
*
|
|
65
|
+
* Every one is asked of the Leads query (AGL-3321) through the field a
|
|
66
|
+
* writer stores for it — see {@link leadQueryClause}.
|
|
125
67
|
*=========================================*/ /** The fields the grid's panel offers on Leads. */ export const LEAD_LIST_FILTER_FIELDS = [
|
|
126
68
|
{
|
|
127
69
|
column: 'status',
|
|
@@ -224,30 +166,6 @@ const STATUS_ALL = {
|
|
|
224
166
|
...shown
|
|
225
167
|
];
|
|
226
168
|
}
|
|
227
|
-
const listed = (clause)=>clause.op === 'isAnyOf' ? clause.value.split(',').map((entry)=>entry.trim()).filter(Boolean) : [
|
|
228
|
-
clause.value
|
|
229
|
-
];
|
|
230
|
-
/** Whether a lead answers every clause the grid shows. */ export function leadMatchesClauses(lead, clauses) {
|
|
231
|
-
return clauses.every((clause)=>{
|
|
232
|
-
switch(clause.field){
|
|
233
|
-
case 'status':
|
|
234
|
-
return listed(clause).some((value)=>LEAD_FILTERS.includes(value) ? leadMatchesFilter(lead, value) : true);
|
|
235
|
-
case 'emailState':
|
|
236
|
-
return listed(clause).some((value)=>LEAD_EMAIL_FILTERS.includes(value) ? leadMatchesEmailFilter(lead, value) : true);
|
|
237
|
-
case 'campaignIds':
|
|
238
|
-
return listed(clause).some((value)=>leadMatchesCampaignFilter(lead, value));
|
|
239
|
-
case 'leadSource':
|
|
240
|
-
return clause.op === 'isEmpty' ? leadMatchesLeadSourceFilter(lead, LEAD_SOURCE_FILTER_NONE) : listed(clause).some((value)=>leadMatchesLeadSourceFilter(lead, value));
|
|
241
|
-
case 'ownerUid':
|
|
242
|
-
var _lead_ownerUid;
|
|
243
|
-
return listed(clause).includes(String((_lead_ownerUid = lead['ownerUid']) != null ? _lead_ownerUid : ''));
|
|
244
|
-
default:
|
|
245
|
-
// A clause on a field this list does not answer narrows nothing,
|
|
246
|
-
// rather than emptying the list.
|
|
247
|
-
return true;
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
169
|
/** Campaign clauses were stored as `contains`; the panel's select says `is`. */ export const LEAD_CAMPAIGN_CODEC = {
|
|
252
170
|
toItem: (clause)=>clause.op === 'contains' || clause.op === 'equals' ? {
|
|
253
171
|
operator: 'is',
|
|
@@ -281,5 +199,193 @@ const listed = (clause)=>clause.op === 'isAnyOf' ? clause.value.split(',').map((
|
|
|
281
199
|
campaignIds: LEAD_CAMPAIGN_CODEC,
|
|
282
200
|
leadSource: LEAD_SOURCE_CODEC
|
|
283
201
|
};
|
|
202
|
+
/*==========================================
|
|
203
|
+
* THE LEADS QUERY (AGL-3321).
|
|
204
|
+
*
|
|
205
|
+
* `orgs/{orgId}/leads`, newest seen first, under a site narrowed by the
|
|
206
|
+
* `visibleTo` scope clause. Each grid clause is asked through a field every
|
|
207
|
+
* lead writer stores (`crmLeadListFields` in `@aglyn/aglyn`):
|
|
208
|
+
*
|
|
209
|
+
* status `status`, stored `new` on a lead nobody has touched, so
|
|
210
|
+
* Open is `status in [new, working]`
|
|
211
|
+
* emailState `emailStatus`, the verdict's status or `none`; "Cannot be
|
|
212
|
+
* emailed" is every verdict but `ok`
|
|
213
|
+
* leadSource `leadSourceKey`, the label as the picklist compares it, or
|
|
214
|
+
* `null` for none
|
|
215
|
+
* ownerUid as stored
|
|
216
|
+
* campaignIds `array-contains` on the lead's own campaigns; under a site,
|
|
217
|
+
* `scopedCampaignIds` — the campaign behind each of the
|
|
218
|
+
* site's scope tokens — which stands in the scope clause's
|
|
219
|
+
* place, so the one array clause answers both
|
|
220
|
+
*
|
|
221
|
+
* and the search box reads `searchTokens` — word prefixes of the name,
|
|
222
|
+
* address, company, title and tags. Every equality rides one
|
|
223
|
+
* `(field, lastSeenAtMs DESC)` composite; the campaign and the search are
|
|
224
|
+
* array clauses, so the two do not stand together (nor under a site, where
|
|
225
|
+
* the scope clause is the query's one array clause, save the search, which
|
|
226
|
+
* folds into it for a reader who may drop it).
|
|
227
|
+
*=========================================*/ /** The one order the Leads list takes: newest seen first. */ export const LEAD_LIST_SORTS = [
|
|
228
|
+
{
|
|
229
|
+
path: 'lastSeenAtMs',
|
|
230
|
+
direction: 'desc'
|
|
231
|
+
}
|
|
232
|
+
];
|
|
233
|
+
/** The fields the Leads query asks — each a stored field, as named. */ export const LEAD_QUERY_FIELDS = [
|
|
234
|
+
crmSelectField('status'),
|
|
235
|
+
crmSelectField('emailStatus'),
|
|
236
|
+
crmSelectField('ownerUid'),
|
|
237
|
+
{
|
|
238
|
+
column: 'leadSourceKey',
|
|
239
|
+
kind: 'exact',
|
|
240
|
+
path: 'leadSourceKey',
|
|
241
|
+
presence: 'nullable',
|
|
242
|
+
operators: [
|
|
243
|
+
'equals',
|
|
244
|
+
'isAnyOf',
|
|
245
|
+
'isEmpty'
|
|
246
|
+
]
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
column: 'campaignIds',
|
|
250
|
+
kind: 'exact',
|
|
251
|
+
path: 'campaignIds',
|
|
252
|
+
tokensPath: 'campaignIds',
|
|
253
|
+
operators: [
|
|
254
|
+
'contains'
|
|
255
|
+
]
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
// A campaign under a site: the lead's campaigns behind its scope.
|
|
259
|
+
column: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,
|
|
260
|
+
kind: 'exact',
|
|
261
|
+
path: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,
|
|
262
|
+
tokensPath: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,
|
|
263
|
+
operators: [
|
|
264
|
+
'isAnyOf'
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
];
|
|
268
|
+
export const LEAD_LIST_DECLARATION = {
|
|
269
|
+
fields: LEAD_QUERY_FIELDS,
|
|
270
|
+
sorts: LEAD_LIST_SORTS,
|
|
271
|
+
search: CRM_LIST_SEARCH
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the
|
|
275
|
+
* start of the address, beside the scope clause — the `(visibleTo, email)`
|
|
276
|
+
* composite the Inbox's leads list reads too.
|
|
277
|
+
*/ export const LEAD_PREFIX_SEARCH = {
|
|
278
|
+
field: {
|
|
279
|
+
column: 'email',
|
|
280
|
+
kind: 'text',
|
|
281
|
+
path: 'email',
|
|
282
|
+
lowerPath: 'email',
|
|
283
|
+
presence: 'always',
|
|
284
|
+
operators: [
|
|
285
|
+
'startsWith'
|
|
286
|
+
]
|
|
287
|
+
},
|
|
288
|
+
notice: 'Search matches the start of a lead’s address for access limited to specific sites.'
|
|
289
|
+
};
|
|
290
|
+
/** Whether an asked clause stands in for the scope clause: a campaign behind the site's scope. */ export const leadClauseImpliesScope = (clause)=>clause.field === CRM_LEAD_SCOPED_CAMPAIGNS_FIELD;
|
|
291
|
+
/** The verdicts a member must not email to: every one but `ok`. */ const EMAIL_PROBLEMS = EMAIL_STATE_STATUSES.filter((status)=>status !== 'ok');
|
|
292
|
+
const ORG_READER = {
|
|
293
|
+
scopeTokens: null,
|
|
294
|
+
foldsScope: true
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* One grid clause as the clause the Leads query asks — see the block above.
|
|
298
|
+
* A value the list does not know is refused by name rather than dropped, so
|
|
299
|
+
* a stored view that names a retired choice says so.
|
|
300
|
+
*/ export function leadQueryClause(clause, reader = ORG_READER) {
|
|
301
|
+
const values = crmClauseValues(clause);
|
|
302
|
+
switch(clause.field){
|
|
303
|
+
case 'status':
|
|
304
|
+
{
|
|
305
|
+
const statuses = [];
|
|
306
|
+
for (const value of values){
|
|
307
|
+
if (value === 'open') statuses.push('new', 'working');
|
|
308
|
+
else if (CRM_LEAD_STATUSES.includes(value)) statuses.push(value);
|
|
309
|
+
else return {
|
|
310
|
+
refused: `${value} is not a lead status`
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return crmAnyOf('status', statuses);
|
|
314
|
+
}
|
|
315
|
+
case 'emailState':
|
|
316
|
+
{
|
|
317
|
+
const keys = [];
|
|
318
|
+
for (const value of values){
|
|
319
|
+
if (value === 'problem') keys.push(...EMAIL_PROBLEMS);
|
|
320
|
+
else if (value === 'none') keys.push('none');
|
|
321
|
+
else if (EMAIL_STATE_STATUSES.includes(value)) keys.push(value);
|
|
322
|
+
else return {
|
|
323
|
+
refused: `${value} is not an email verdict`
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return crmAnyOf('emailStatus', keys);
|
|
327
|
+
}
|
|
328
|
+
case 'leadSource':
|
|
329
|
+
{
|
|
330
|
+
if (clause.op === 'isEmpty' || values.length === 1 && values[0] === LEAD_SOURCE_FILTER_NONE) {
|
|
331
|
+
return {
|
|
332
|
+
field: 'leadSourceKey',
|
|
333
|
+
op: 'isEmpty',
|
|
334
|
+
value: ''
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (values.includes(LEAD_SOURCE_FILTER_NONE)) {
|
|
338
|
+
return {
|
|
339
|
+
refused: '"No lead source" cannot be picked beside a value'
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const keys = values.map((value)=>crmLeadSourceKey(value)).filter((key)=>Boolean(key));
|
|
343
|
+
return crmAnyOf('leadSourceKey', keys);
|
|
344
|
+
}
|
|
345
|
+
case 'ownerUid':
|
|
346
|
+
return crmAnyOf('ownerUid', values);
|
|
347
|
+
case 'campaignIds':
|
|
348
|
+
{
|
|
349
|
+
if (values.length !== 1) return {
|
|
350
|
+
refused: 'pick one campaign'
|
|
351
|
+
};
|
|
352
|
+
if (!reader.scopeTokens) return {
|
|
353
|
+
field: 'campaignIds',
|
|
354
|
+
op: 'contains',
|
|
355
|
+
value: values[0]
|
|
356
|
+
};
|
|
357
|
+
if (!reader.foldsScope) {
|
|
358
|
+
return {
|
|
359
|
+
refused: 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it'
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
field: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,
|
|
364
|
+
op: 'isAnyOf',
|
|
365
|
+
value: reader.scopeTokens.map((token)=>`${token}${SCOPED_SEARCH_JOIN}${values[0]}`).join(',')
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
default:
|
|
369
|
+
return {
|
|
370
|
+
refused: 'this list does not filter by that'
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The query shape the Leads list can send beyond its own declaration, for
|
|
376
|
+
* the index pin (AGL-3321): a collaborator's address prefix beside the
|
|
377
|
+
* equalities they may set.
|
|
378
|
+
*/ export function leadIndexShapes() {
|
|
379
|
+
const equalities = LEAD_QUERY_FIELDS.filter((field)=>!field.tokensPath);
|
|
380
|
+
return [
|
|
381
|
+
{
|
|
382
|
+
fields: [
|
|
383
|
+
LEAD_PREFIX_SEARCH.field,
|
|
384
|
+
...equalities
|
|
385
|
+
],
|
|
386
|
+
sorts: []
|
|
387
|
+
}
|
|
388
|
+
];
|
|
389
|
+
}
|
|
284
390
|
|
|
285
391
|
//# sourceMappingURL=lead-filters.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/lead-filters.ts"],"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\nimport {\n EMAIL_STATE_LABELS,\n EMAIL_STATE_STATUSES,\n CRM_LEAD_STATUS_LABELS,\n type EmailStateStatus,\n type CrmLeadFields,\n type CrmLeadStatus,\n emailStateForbidsEmail,\n crmLeadStatus,\n isCrmLeadOpen,\n normalizeCrmPicklistLabel,\n readCampaignIds,\n readEmailState,\n} from '@aglyn/aglyn'\nimport type { CrmViewFilterClause } from '@aglyn/aglyn'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { type ListGridFilterCodec, listSelectCodec, listRowMatchesSearch } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\n\n/**\n * What the Leads section's `Show` control offers (AGL-2608): the two\n * aggregate views and each status on its own. `open` is the default — the\n * list opens on the work — and it is the one view a Firestore query cannot\n * express, because a lead nobody has touched carries no status field at all;\n * see the section for why the filter runs over a loaded window.\n */\nexport type LeadFilter = 'open' | 'all' | CrmLeadStatus\n\nexport const LEAD_FILTERS: readonly LeadFilter[] = [\n 'open',\n 'new',\n 'working',\n 'qualified',\n 'unqualified',\n 'all',\n]\n\nexport const LEAD_FILTER_LABELS: Record<LeadFilter, string> = {\n open: 'Open',\n all: 'All',\n new: CRM_LEAD_STATUS_LABELS.new,\n working: CRM_LEAD_STATUS_LABELS.working,\n qualified: CRM_LEAD_STATUS_LABELS.qualified,\n unqualified: CRM_LEAD_STATUS_LABELS.unqualified,\n}\n\n/** Whether a lead belongs in a filter's view. An absent status is `new`. */\nexport function leadMatchesFilter(\n lead: Pick<CrmLeadFields, 'status'>,\n filter: LeadFilter,\n): boolean {\n if (filter === 'all') return true\n if (filter === 'open') return isCrmLeadOpen(lead)\n return crmLeadStatus(lead) === filter\n}\n\n/**\n * The Leads section's `Email` control (AGL-3245): every lead, the ones a\n * member must not email — any verdict but `ok` — the ones nothing has been\n * said about, or one verdict on its own. Beside `Show`, not inside it: a\n * bounce does not move a lead out of Open, and a queue of bounced leads is\n * a queue of people to reach some other way.\n */\nexport type LeadEmailFilter = 'any' | 'problem' | 'none' | EmailStateStatus\n\nexport const LEAD_EMAIL_FILTERS: readonly LeadEmailFilter[] = [\n 'any',\n 'problem',\n ...EMAIL_STATE_STATUSES,\n 'none',\n]\n\nexport const LEAD_EMAIL_FILTER_LABELS: Record<LeadEmailFilter, string> = {\n any: 'Any',\n problem: 'Cannot be emailed',\n none: 'Nothing known',\n ...EMAIL_STATE_LABELS,\n}\n\n/** Whether a lead belongs in an email filter's view. */\nexport function leadMatchesEmailFilter(\n lead: Readonly<Record<string, unknown>>,\n filter: LeadEmailFilter,\n): boolean {\n if (filter === 'any') return true\n const state = readEmailState(lead)\n if (filter === 'none') return state === null\n if (filter === 'problem') return emailStateForbidsEmail(state)\n return state?.status === filter\n}\n\n/**\n * The Leads section's `Campaign` control (AGL-3254): every lead, or the\n * ones filed under one campaign — read off the lead's own `campaignIds`,\n * the field every campaign member carries. `''` is every lead, which is\n * what the control opens on; the ids are the site's containers', and the\n * section resolves their names for the menu.\n */\nexport function leadMatchesCampaignFilter(\n lead: Readonly<Record<string, unknown>>,\n campaignId: string,\n): boolean {\n if (!campaignId) return true\n return readCampaignIds(lead).includes(campaignId)\n}\n\n/**\n * The fields the Leads section's search box reads (AGL-3246): who the lead\n * is and where they work — the words a person types to find one — plus the\n * tags, which are how an import names its batch (`sal-15`). `name` and\n * `email` are the capture door's fields rather than the CRM's, so the row is\n * read as a record rather than through `CrmLeadFields`.\n */\nexport const LEAD_SEARCH_FIELDS = ['name', 'email', 'company', 'jobTitle', 'tags'] as const\n\n/**\n * Whether a lead answers a search term.\n *\n * Case-insensitive, and every whitespace-separated word of the term must\n * appear in SOME searched field: `morgan lamphere` and `lamphere sal-15`\n * both find Morgan Lamphere, `morgan smith` does not. A blank term matches\n * every lead, so the box emptied is the list unfiltered.\n */\nexport function leadMatchesSearch(\n lead: Readonly<Record<string, unknown>>,\n term: string,\n): boolean {\n // Tags are a list; a stray string there is no tag of the lead's.\n const tags = Array.isArray(lead['tags']) ? lead['tags'] : undefined\n return listRowMatchesSearch({ ...lead, tags }, LEAD_SEARCH_FIELDS, term.trim().split(/\\s+/))\n}\n\n/**\n * The `Lead source` control's \"no lead source\" choice (AGL-3298). Held in\n * the saved view as an `isEmpty` clause, never as this string; it is only\n * the select's own value for the choice.\n */\nexport const LEAD_SOURCE_FILTER_NONE = '\\u0000none'\n\n/**\n * The Leads section's `Lead source` control (AGL-3298): every lead (`''`),\n * the ones holding one value — compared the way the picklist compares\n * labels, so a value typed in another case before the list existed is\n * still found — or the ones holding none.\n */\nexport function leadMatchesLeadSourceFilter(\n lead: Readonly<Record<string, unknown>>,\n filter: string,\n): boolean {\n if (!filter) return true\n const held = normalizeCrmPicklistLabel(lead['leadSource']).toLowerCase()\n if (filter === LEAD_SOURCE_FILTER_NONE) return held === ''\n return held === normalizeCrmPicklistLabel(filter).toLowerCase()\n}\n\n/*==========================================\n * THE LEADS GRID'S FILTERS (AGL-3313).\n *\n * The Show, Email, Campaign and Lead source dropdowns became columns of the\n * grid's own Filters panel. Their clauses keep the shape the dropdowns\n * stored, so every saved view of leads reads as it did:\n *\n * status `equals <filter>`; none at all is Open, `all` is every lead\n * emailState `equals <filter>`\n * campaignIds `contains <campaign id>`\n * leadSource `equals <label>`, or `isEmpty` for the leads holding none\n *\n * Every one narrows the loaded window, never the query — see the section's\n * `LEADS_WINDOW` for why a status cannot be asked of Firestore.\n *=========================================*/\n\n/** The fields the grid's panel offers on Leads. */\nexport const LEAD_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals', 'isAnyOf'] },\n { column: 'emailState', kind: 'exact', path: 'emailState', operators: ['equals', 'isAnyOf'] },\n { column: 'ownerUid', kind: 'exact', path: 'ownerUid', operators: ['equals', 'isAnyOf'] },\n { column: 'leadSource', kind: 'exact', path: 'leadSource', operators: ['equals', 'isAnyOf'] },\n { column: 'campaignIds', kind: 'exact', path: 'campaignIds', operators: ['equals'] },\n]\n\n/** What each filter field reads as on a chip. */\nexport const LEAD_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n emailState: 'Email',\n ownerUid: 'Owner',\n leadSource: 'Lead source',\n campaignIds: 'Campaign',\n}\n\n/** The choices for Status: Open (new or working) and each status on its own. */\nexport const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter(\n (option) => option !== 'all',\n).map((option) => ({\n value: option,\n label: option === 'open' ? 'Open (new or working)' : LEAD_FILTER_LABELS[option],\n}))\n\n/** The choices for Email: every verdict, plus the two aggregates. */\nexport const LEAD_EMAIL_FILTER_OPTIONS = LEAD_EMAIL_FILTERS.filter(\n (option) => option !== 'any',\n).map((option) => ({ value: option, label: LEAD_EMAIL_FILTER_LABELS[option] }))\n\nconst STATUS_OPEN: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'open' }\nconst STATUS_ALL: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'all' }\n\n/**\n * The clauses the grid shows for a view's stored ones. A stored list with\n * no status clause is Open, which the list opened on before views existed,\n * so it is shown as the clause it means; `status equals all` is every lead\n * and so no clause at all.\n */\nexport function leadClausesForGrid(\n stored: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = stored.find((clause) => clause.field === 'status')\n if (!status) return [STATUS_OPEN, ...stored]\n if (status.op === 'equals' && status.value === 'all') {\n return stored.filter((clause) => clause.field !== 'status')\n }\n return [...stored]\n}\n\n/** The inverse of {@link leadClausesForGrid}: what the view stores. */\nexport function leadClausesToStore(\n shown: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = shown.find((clause) => clause.field === 'status')\n if (!status) return [...shown, STATUS_ALL]\n if (status.op === 'equals' && status.value === 'open') {\n return shown.filter((clause) => clause.field !== 'status')\n }\n return [...shown]\n}\n\nconst listed = (clause: CrmViewFilterClause): string[] =>\n clause.op === 'isAnyOf'\n ? clause.value.split(',').map((entry) => entry.trim()).filter(Boolean)\n : [clause.value]\n\n/** Whether a lead answers every clause the grid shows. */\nexport function leadMatchesClauses(\n lead: Readonly<Record<string, unknown>> & Pick<CrmLeadFields, 'status'>,\n clauses: readonly CrmViewFilterClause[],\n): boolean {\n return clauses.every((clause) => {\n switch (clause.field) {\n case 'status':\n return listed(clause).some((value) =>\n (LEAD_FILTERS as readonly string[]).includes(value)\n ? leadMatchesFilter(lead, value as LeadFilter)\n : true,\n )\n case 'emailState':\n return listed(clause).some((value) =>\n (LEAD_EMAIL_FILTERS as readonly string[]).includes(value)\n ? leadMatchesEmailFilter(lead, value as LeadEmailFilter)\n : true,\n )\n case 'campaignIds':\n return listed(clause).some((value) => leadMatchesCampaignFilter(lead, value))\n case 'leadSource':\n return clause.op === 'isEmpty'\n ? leadMatchesLeadSourceFilter(lead, LEAD_SOURCE_FILTER_NONE)\n : listed(clause).some((value) => leadMatchesLeadSourceFilter(lead, value))\n case 'ownerUid':\n return listed(clause).includes(String(lead['ownerUid'] ?? ''))\n default:\n // A clause on a field this list does not answer narrows nothing,\n // rather than emptying the list.\n return true\n }\n })\n}\n\n/** Campaign clauses were stored as `contains`; the panel's select says `is`. */\nexport const LEAD_CAMPAIGN_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'contains' || clause.op === 'equals'\n ? { operator: 'is', value: clause.value }\n : null,\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n return clause && clause.op === 'equals' ? { ...clause, op: 'contains' } : null\n },\n}\n\n/** \"No lead source\" is an `isEmpty` clause, shown as a choice of the select. */\nexport const LEAD_SOURCE_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'isEmpty'\n ? { operator: 'is', value: LEAD_SOURCE_FILTER_NONE }\n : listSelectCodec.toItem(clause),\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n if (clause?.op === 'equals' && clause.value === LEAD_SOURCE_FILTER_NONE) {\n return { field: clause.field, op: 'isEmpty', value: '' }\n }\n return clause\n },\n}\n\n/** The Leads fields whose stored clauses translate their own way. */\nexport const LEAD_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>> = {\n campaignIds: LEAD_CAMPAIGN_CODEC,\n leadSource: LEAD_SOURCE_CODEC,\n}\n"],"names":["EMAIL_STATE_LABELS","EMAIL_STATE_STATUSES","CRM_LEAD_STATUS_LABELS","emailStateForbidsEmail","crmLeadStatus","isCrmLeadOpen","normalizeCrmPicklistLabel","readCampaignIds","readEmailState","listSelectCodec","listRowMatchesSearch","LEAD_FILTERS","LEAD_FILTER_LABELS","open","all","new","working","qualified","unqualified","leadMatchesFilter","lead","filter","LEAD_EMAIL_FILTERS","LEAD_EMAIL_FILTER_LABELS","any","problem","none","leadMatchesEmailFilter","state","status","leadMatchesCampaignFilter","campaignId","includes","LEAD_SEARCH_FIELDS","leadMatchesSearch","term","tags","Array","isArray","undefined","trim","split","LEAD_SOURCE_FILTER_NONE","leadMatchesLeadSourceFilter","held","toLowerCase","LEAD_LIST_FILTER_FIELDS","column","kind","path","operators","LEAD_LIST_FILTER_HEADERS","emailState","ownerUid","leadSource","campaignIds","LEAD_STATUS_FILTER_OPTIONS","option","map","value","label","LEAD_EMAIL_FILTER_OPTIONS","STATUS_OPEN","field","op","STATUS_ALL","leadClausesForGrid","stored","find","clause","leadClausesToStore","shown","listed","entry","Boolean","leadMatchesClauses","clauses","every","some","String","LEAD_CAMPAIGN_CODEC","toItem","operator","toClause","item","LEAD_SOURCE_CODEC","LEAD_FILTER_CODECS"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/lead-filters.ts"],"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\nimport {\n EMAIL_STATE_LABELS,\n EMAIL_STATE_STATUSES,\n CRM_LEAD_STATUS_LABELS,\n CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n CRM_LEAD_STATUSES,\n type EmailStateStatus,\n type CrmLeadStatus,\n crmLeadSourceKey,\n} from '@aglyn/aglyn'\nimport type { CrmViewFilterClause } from '@aglyn/aglyn'\nimport { SCOPED_SEARCH_JOIN } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { type ListGridFilterCodec, listSelectCodec } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n CRM_LIST_SEARCH,\n type CrmClauseAsked,\n crmAnyOf,\n crmClauseValues,\n crmSelectField,\n} from './crm-list-query'\n\n/**\n * What the Leads section's `Show` control offers (AGL-2608): the two\n * aggregate views and each status on its own. `open` is the default — the\n * list opens on the work — asked of the query as `status in [new, working]`,\n * which every lead answers because every lead writer stores a status, `new`\n * for one nobody has touched (`crmLeadListFields`, AGL-3321).\n */\nexport type LeadFilter = 'open' | 'all' | CrmLeadStatus\n\nexport const LEAD_FILTERS: readonly LeadFilter[] = [\n 'open',\n 'new',\n 'working',\n 'qualified',\n 'unqualified',\n 'all',\n]\n\nexport const LEAD_FILTER_LABELS: Record<LeadFilter, string> = {\n open: 'Open',\n all: 'All',\n new: CRM_LEAD_STATUS_LABELS.new,\n working: CRM_LEAD_STATUS_LABELS.working,\n qualified: CRM_LEAD_STATUS_LABELS.qualified,\n unqualified: CRM_LEAD_STATUS_LABELS.unqualified,\n}\n\n/**\n * The Leads section's `Email` control (AGL-3245): every lead, the ones a\n * member must not email — any verdict but `ok` — the ones nothing has been\n * said about, or one verdict on its own. Beside `Show`, not inside it: a\n * bounce does not move a lead out of Open, and a queue of bounced leads is\n * a queue of people to reach some other way.\n */\nexport type LeadEmailFilter = 'any' | 'problem' | 'none' | EmailStateStatus\n\nexport const LEAD_EMAIL_FILTERS: readonly LeadEmailFilter[] = [\n 'any',\n 'problem',\n ...EMAIL_STATE_STATUSES,\n 'none',\n]\n\nexport const LEAD_EMAIL_FILTER_LABELS: Record<LeadEmailFilter, string> = {\n any: 'Any',\n problem: 'Cannot be emailed',\n none: 'Nothing known',\n ...EMAIL_STATE_LABELS,\n}\n\n/**\n * The `Lead source` control's \"no lead source\" choice (AGL-3298). Held in\n * the saved view as an `isEmpty` clause, never as this string; it is only\n * the select's own value for the choice.\n */\nexport const LEAD_SOURCE_FILTER_NONE = '\\u0000none'\n\n/*==========================================\n * THE LEADS GRID'S FILTERS (AGL-3313).\n *\n * The Show, Email, Campaign and Lead source dropdowns became columns of the\n * grid's own Filters panel. Their clauses keep the shape the dropdowns\n * stored, so every saved view of leads reads as it did:\n *\n * status `equals <filter>`; none at all is Open, `all` is every lead\n * emailState `equals <filter>`\n * campaignIds `contains <campaign id>`\n * leadSource `equals <label>`, or `isEmpty` for the leads holding none\n *\n * Every one is asked of the Leads query (AGL-3321) through the field a\n * writer stores for it — see {@link leadQueryClause}.\n *=========================================*/\n\n/** The fields the grid's panel offers on Leads. */\nexport const LEAD_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals', 'isAnyOf'] },\n { column: 'emailState', kind: 'exact', path: 'emailState', operators: ['equals', 'isAnyOf'] },\n { column: 'ownerUid', kind: 'exact', path: 'ownerUid', operators: ['equals', 'isAnyOf'] },\n { column: 'leadSource', kind: 'exact', path: 'leadSource', operators: ['equals', 'isAnyOf'] },\n { column: 'campaignIds', kind: 'exact', path: 'campaignIds', operators: ['equals'] },\n]\n\n/** What each filter field reads as on a chip. */\nexport const LEAD_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n emailState: 'Email',\n ownerUid: 'Owner',\n leadSource: 'Lead source',\n campaignIds: 'Campaign',\n}\n\n/** The choices for Status: Open (new or working) and each status on its own. */\nexport const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter(\n (option) => option !== 'all',\n).map((option) => ({\n value: option,\n label: option === 'open' ? 'Open (new or working)' : LEAD_FILTER_LABELS[option],\n}))\n\n/** The choices for Email: every verdict, plus the two aggregates. */\nexport const LEAD_EMAIL_FILTER_OPTIONS = LEAD_EMAIL_FILTERS.filter(\n (option) => option !== 'any',\n).map((option) => ({ value: option, label: LEAD_EMAIL_FILTER_LABELS[option] }))\n\nconst STATUS_OPEN: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'open' }\nconst STATUS_ALL: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'all' }\n\n/**\n * The clauses the grid shows for a view's stored ones. A stored list with\n * no status clause is Open, which the list opened on before views existed,\n * so it is shown as the clause it means; `status equals all` is every lead\n * and so no clause at all.\n */\nexport function leadClausesForGrid(\n stored: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = stored.find((clause) => clause.field === 'status')\n if (!status) return [STATUS_OPEN, ...stored]\n if (status.op === 'equals' && status.value === 'all') {\n return stored.filter((clause) => clause.field !== 'status')\n }\n return [...stored]\n}\n\n/** The inverse of {@link leadClausesForGrid}: what the view stores. */\nexport function leadClausesToStore(\n shown: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = shown.find((clause) => clause.field === 'status')\n if (!status) return [...shown, STATUS_ALL]\n if (status.op === 'equals' && status.value === 'open') {\n return shown.filter((clause) => clause.field !== 'status')\n }\n return [...shown]\n}\n\n/** Campaign clauses were stored as `contains`; the panel's select says `is`. */\nexport const LEAD_CAMPAIGN_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'contains' || clause.op === 'equals'\n ? { operator: 'is', value: clause.value }\n : null,\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n return clause && clause.op === 'equals' ? { ...clause, op: 'contains' } : null\n },\n}\n\n/** \"No lead source\" is an `isEmpty` clause, shown as a choice of the select. */\nexport const LEAD_SOURCE_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'isEmpty'\n ? { operator: 'is', value: LEAD_SOURCE_FILTER_NONE }\n : listSelectCodec.toItem(clause),\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n if (clause?.op === 'equals' && clause.value === LEAD_SOURCE_FILTER_NONE) {\n return { field: clause.field, op: 'isEmpty', value: '' }\n }\n return clause\n },\n}\n\n/** The Leads fields whose stored clauses translate their own way. */\nexport const LEAD_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>> = {\n campaignIds: LEAD_CAMPAIGN_CODEC,\n leadSource: LEAD_SOURCE_CODEC,\n}\n\n/*==========================================\n * THE LEADS QUERY (AGL-3321).\n *\n * `orgs/{orgId}/leads`, newest seen first, under a site narrowed by the\n * `visibleTo` scope clause. Each grid clause is asked through a field every\n * lead writer stores (`crmLeadListFields` in `@aglyn/aglyn`):\n *\n * status `status`, stored `new` on a lead nobody has touched, so\n * Open is `status in [new, working]`\n * emailState `emailStatus`, the verdict's status or `none`; \"Cannot be\n * emailed\" is every verdict but `ok`\n * leadSource `leadSourceKey`, the label as the picklist compares it, or\n * `null` for none\n * ownerUid as stored\n * campaignIds `array-contains` on the lead's own campaigns; under a site,\n * `scopedCampaignIds` — the campaign behind each of the\n * site's scope tokens — which stands in the scope clause's\n * place, so the one array clause answers both\n *\n * and the search box reads `searchTokens` — word prefixes of the name,\n * address, company, title and tags. Every equality rides one\n * `(field, lastSeenAtMs DESC)` composite; the campaign and the search are\n * array clauses, so the two do not stand together (nor under a site, where\n * the scope clause is the query's one array clause, save the search, which\n * folds into it for a reader who may drop it).\n *=========================================*/\n\n/** The one order the Leads list takes: newest seen first. */\nexport const LEAD_LIST_SORTS: readonly ListQuerySort[] = [{ path: 'lastSeenAtMs', direction: 'desc' }]\n\n/** The fields the Leads query asks — each a stored field, as named. */\nexport const LEAD_QUERY_FIELDS: readonly ListFilterField[] = [\n crmSelectField('status'),\n crmSelectField('emailStatus'),\n crmSelectField('ownerUid'),\n {\n column: 'leadSourceKey',\n kind: 'exact',\n path: 'leadSourceKey',\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty'],\n },\n {\n column: 'campaignIds',\n kind: 'exact',\n path: 'campaignIds',\n tokensPath: 'campaignIds',\n operators: ['contains'],\n },\n {\n // A campaign under a site: the lead's campaigns behind its scope.\n column: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n kind: 'exact',\n path: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n tokensPath: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n operators: ['isAnyOf'],\n },\n]\n\nexport const LEAD_LIST_DECLARATION: ListQueryDeclaration = {\n fields: LEAD_QUERY_FIELDS,\n sorts: LEAD_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the address, beside the scope clause — the `(visibleTo, email)`\n * composite the Inbox's leads list reads too.\n */\nexport const LEAD_PREFIX_SEARCH = {\n field: {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['startsWith'],\n } satisfies ListFilterField,\n notice: 'Search matches the start of a lead’s address for access limited to specific sites.',\n}\n\n/** Whether an asked clause stands in for the scope clause: a campaign behind the site's scope. */\nexport const leadClauseImpliesScope = (clause: ListFilterRequest): boolean =>\n clause.field === CRM_LEAD_SCOPED_CAMPAIGNS_FIELD\n\n/** The verdicts a member must not email to: every one but `ok`. */\nconst EMAIL_PROBLEMS = EMAIL_STATE_STATUSES.filter((status) => status !== 'ok')\n\n/** Who is asking, for the one clause whose shape depends on it: the Campaign. */\nexport interface LeadQueryReader {\n /** The site's scope tokens, or `null` at the organization level. */\n scopeTokens: readonly string[] | null\n /** Whether the reader may run a query without the scope clause (`useCrmFoldsScope`). */\n foldsScope: boolean\n}\n\nconst ORG_READER: LeadQueryReader = { scopeTokens: null, foldsScope: true }\n\n/**\n * One grid clause as the clause the Leads query asks — see the block above.\n * A value the list does not know is refused by name rather than dropped, so\n * a stored view that names a retired choice says so.\n */\nexport function leadQueryClause(\n clause: CrmViewFilterClause | ListFilterRequest,\n reader: LeadQueryReader = ORG_READER,\n): CrmClauseAsked {\n const values = crmClauseValues(clause)\n switch (clause.field) {\n case 'status': {\n const statuses: string[] = []\n for (const value of values) {\n if (value === 'open') statuses.push('new', 'working')\n else if ((CRM_LEAD_STATUSES as readonly string[]).includes(value)) statuses.push(value)\n else return { refused: `${value} is not a lead status` }\n }\n return crmAnyOf('status', statuses)\n }\n case 'emailState': {\n const keys: string[] = []\n for (const value of values) {\n if (value === 'problem') keys.push(...EMAIL_PROBLEMS)\n else if (value === 'none') keys.push('none')\n else if ((EMAIL_STATE_STATUSES as readonly string[]).includes(value)) keys.push(value)\n else return { refused: `${value} is not an email verdict` }\n }\n return crmAnyOf('emailStatus', keys)\n }\n case 'leadSource': {\n if (clause.op === 'isEmpty' || (values.length === 1 && values[0] === LEAD_SOURCE_FILTER_NONE)) {\n return { field: 'leadSourceKey', op: 'isEmpty', value: '' }\n }\n if (values.includes(LEAD_SOURCE_FILTER_NONE)) {\n return { refused: '\"No lead source\" cannot be picked beside a value' }\n }\n const keys = values.map((value) => crmLeadSourceKey(value)).filter((key): key is string => Boolean(key))\n return crmAnyOf('leadSourceKey', keys)\n }\n case 'ownerUid':\n return crmAnyOf('ownerUid', values)\n case 'campaignIds': {\n if (values.length !== 1) return { refused: 'pick one campaign' }\n if (!reader.scopeTokens) return { field: 'campaignIds', op: 'contains', value: values[0] }\n if (!reader.foldsScope) {\n return {\n refused:\n 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it',\n }\n }\n return {\n field: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n op: 'isAnyOf',\n value: reader.scopeTokens.map((token) => `${token}${SCOPED_SEARCH_JOIN}${values[0]}`).join(','),\n }\n }\n default:\n return { refused: 'this list does not filter by that' }\n }\n}\n\n/**\n * The query shape the Leads list can send beyond its own declaration, for\n * the index pin (AGL-3321): a collaborator's address prefix beside the\n * equalities they may set.\n */\nexport function leadIndexShapes(): ListQueryDeclaration[] {\n const equalities = LEAD_QUERY_FIELDS.filter((field) => !field.tokensPath)\n return [{ fields: [LEAD_PREFIX_SEARCH.field, ...equalities], sorts: [] }]\n}\n"],"names":["EMAIL_STATE_LABELS","EMAIL_STATE_STATUSES","CRM_LEAD_STATUS_LABELS","CRM_LEAD_SCOPED_CAMPAIGNS_FIELD","CRM_LEAD_STATUSES","crmLeadSourceKey","SCOPED_SEARCH_JOIN","listSelectCodec","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","crmSelectField","LEAD_FILTERS","LEAD_FILTER_LABELS","open","all","new","working","qualified","unqualified","LEAD_EMAIL_FILTERS","LEAD_EMAIL_FILTER_LABELS","any","problem","none","LEAD_SOURCE_FILTER_NONE","LEAD_LIST_FILTER_FIELDS","column","kind","path","operators","LEAD_LIST_FILTER_HEADERS","status","emailState","ownerUid","leadSource","campaignIds","LEAD_STATUS_FILTER_OPTIONS","filter","option","map","value","label","LEAD_EMAIL_FILTER_OPTIONS","STATUS_OPEN","field","op","STATUS_ALL","leadClausesForGrid","stored","find","clause","leadClausesToStore","shown","LEAD_CAMPAIGN_CODEC","toItem","operator","toClause","item","LEAD_SOURCE_CODEC","LEAD_FILTER_CODECS","LEAD_LIST_SORTS","direction","LEAD_QUERY_FIELDS","presence","tokensPath","LEAD_LIST_DECLARATION","fields","sorts","search","LEAD_PREFIX_SEARCH","lowerPath","notice","leadClauseImpliesScope","EMAIL_PROBLEMS","ORG_READER","scopeTokens","foldsScope","leadQueryClause","reader","values","statuses","push","includes","refused","keys","length","key","Boolean","token","join","leadIndexShapes","equalities"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { CrmListCollection } from '@aglyn/aglyn';
|
|
18
|
+
import { type MaybeTokenSource } from '@aglyn/shared-util-http/authorized-token';
|
|
19
|
+
import type { CrmTaskRouteScope } from './task-routes';
|
|
20
|
+
/**
|
|
21
|
+
* After a client-direct write the browser cannot follow up itself: ask the
|
|
22
|
+
* list-fields route to restamp the records (AGL-3321). Never throws — the
|
|
23
|
+
* write already happened, and a record left stale is what the backfill and
|
|
24
|
+
* the next write bring level.
|
|
25
|
+
*/
|
|
26
|
+
export declare function restampCrmListFields(user: MaybeTokenSource, scope: CrmTaskRouteScope | null, collection: CrmListCollection, ids: readonly string[]): Promise<void>;
|