@aglyn/plugins-crm 1.0.0-beta.186 → 1.0.0-beta.188
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.js +6 -6
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/contacts-section.js +7 -7
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/deals-section.js +75 -23
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/leads-section.js +6 -6
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/tasks-section.js +7 -7
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/hooks/use-deals.d.ts +21 -0
- package/src/lib/hooks/use-deals.js +34 -0
- package/src/lib/hooks/use-deals.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +4 -4
- package/src/lib/model/lead-filters.js +5 -5
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/components/crm-filter-bar.d.ts +0 -55
- package/src/lib/components/crm-filter-bar.js +0 -93
- package/src/lib/components/crm-filter-bar.js.map +0 -1
- package/src/lib/hooks/use-crm-grid-filter.d.ts +0 -77
- package/src/lib/hooks/use-crm-grid-filter.js +0 -143
- package/src/lib/hooks/use-crm-grid-filter.js.map +0 -1
- package/src/lib/model/crm-grid-filter.d.ts +0 -64
- package/src/lib/model/crm-grid-filter.js +0 -144
- package/src/lib/model/crm-grid-filter.js.map +0 -1
|
@@ -34,6 +34,27 @@ export declare function useDealsByStatus(orgId: string | null, readTokens: reado
|
|
|
34
34
|
* the first shape orders on one field and needs none.
|
|
35
35
|
*/
|
|
36
36
|
export declare function usePagedDeals(orgId: string | null, readTokens: readonly string[] | null, status: CrmDealStatus | 'all', pipelineId?: string | null): import("@aglyn/tenant-feature-instance").UsePagedCollectionResult<DealDoc>;
|
|
37
|
+
/**
|
|
38
|
+
* How many deals a table search reads (AGL-3315): the newest by last
|
|
39
|
+
* change, in the table's own scope, status and pipeline, plus one probe row
|
|
40
|
+
* so "there are more" is a fact rather than a guess at the boundary.
|
|
41
|
+
*/
|
|
42
|
+
export declare const DEAL_SEARCH_WINDOW = 1000;
|
|
43
|
+
/**
|
|
44
|
+
* The deals a table SEARCH runs over (AGL-3315) — a window, read only while
|
|
45
|
+
* the search box holds something.
|
|
46
|
+
*
|
|
47
|
+
* A search has to find a deal on any page, and it has to find it by a word
|
|
48
|
+
* from the middle of its title: "Acme renewal" is looked for as "renewal".
|
|
49
|
+
* An indexed prefix, the way the companies list searches names, answers
|
|
50
|
+
* only the first word, and word tokens would need every writer and a
|
|
51
|
+
* backfill to agree on them. An organization's deals are counted in the
|
|
52
|
+
* tens or hundreds — the board already reads every open one — so the
|
|
53
|
+
* window is the whole collection for all but the largest, and the table
|
|
54
|
+
* says so when it is not. The query is the table's own, one of the four
|
|
55
|
+
* shapes `usePagedDeals` states, so it rides indexes that already exist.
|
|
56
|
+
*/
|
|
57
|
+
export declare function useDealSearchWindow(orgId: string | null, readTokens: readonly string[] | null, status: CrmDealStatus | 'all', pipelineId?: string | null): import("@aglyn/tenant-feature-instance").UseFirestoreCollectionResult<DealDoc>;
|
|
37
58
|
/**
|
|
38
59
|
* The deals that name one contact or one company, for the card on that
|
|
39
60
|
* record's page. `(visibleTo, contactId|companyId, updatedAt DESC)`.
|
|
@@ -74,6 +74,40 @@ import { crmScopeListable, crmVisibleToClause } from "./use-crm-scope.js";
|
|
|
74
74
|
idField: '$id'
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* How many deals a table search reads (AGL-3315): the newest by last
|
|
79
|
+
* change, in the table's own scope, status and pipeline, plus one probe row
|
|
80
|
+
* so "there are more" is a fact rather than a guess at the boundary.
|
|
81
|
+
*/ export const DEAL_SEARCH_WINDOW = 1000;
|
|
82
|
+
/**
|
|
83
|
+
* The deals a table SEARCH runs over (AGL-3315) — a window, read only while
|
|
84
|
+
* the search box holds something.
|
|
85
|
+
*
|
|
86
|
+
* A search has to find a deal on any page, and it has to find it by a word
|
|
87
|
+
* from the middle of its title: "Acme renewal" is looked for as "renewal".
|
|
88
|
+
* An indexed prefix, the way the companies list searches names, answers
|
|
89
|
+
* only the first word, and word tokens would need every writer and a
|
|
90
|
+
* backfill to agree on them. An organization's deals are counted in the
|
|
91
|
+
* tens or hundreds — the board already reads every open one — so the
|
|
92
|
+
* window is the whole collection for all but the largest, and the table
|
|
93
|
+
* says so when it is not. The query is the table's own, one of the four
|
|
94
|
+
* shapes `usePagedDeals` states, so it rides indexes that already exist.
|
|
95
|
+
*/ export function useDealSearchWindow(orgId, readTokens, status, pipelineId = null) {
|
|
96
|
+
const firestore = useFirestore();
|
|
97
|
+
return useFirestoreCollection(()=>orgId && crmScopeListable(readTokens) ? query(collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals), ...crmVisibleToClause(readTokens), ...pipelineId ? [
|
|
98
|
+
where('pipelineId', '==', pipelineId)
|
|
99
|
+
] : [], ...status === 'all' ? [] : [
|
|
100
|
+
where('status', '==', status)
|
|
101
|
+
], orderBy('updatedAt', 'desc'), limit(DEAL_SEARCH_WINDOW + 1)) : null, [
|
|
102
|
+
firestore,
|
|
103
|
+
orgId,
|
|
104
|
+
readTokens,
|
|
105
|
+
status,
|
|
106
|
+
pipelineId
|
|
107
|
+
], {
|
|
108
|
+
idField: '$id'
|
|
109
|
+
});
|
|
110
|
+
}
|
|
77
111
|
/**
|
|
78
112
|
* The deals that name one contact or one company, for the card on that
|
|
79
113
|
* record's page. `(visibleTo, contactId|companyId, updatedAt DESC)`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-deals.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'use client'\n\nimport { CRM_COLLECTIONS, type CrmDealStatus } from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n usePagedCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport type { DealDoc } from '../model/deal-board-model'\nimport { crmScopeListable, crmVisibleToClause } from './use-crm-scope'\n\n/**\n * The most open cards one pipeline draws. A board past this is a board\n * nobody reads column by column; the table is paged and reaches the rest.\n */\nexport const BOARD_OPEN_LIMIT = 500\n\n/** Closed deals shown per closed column when it is expanded. */\nexport const BOARD_CLOSED_LIMIT = 50\n\n/** Deals listed on a contact's or a company's page. */\nexport const LINKED_DEALS_LIMIT = 25\n\n/**\n * The deals of one status in one pipeline, newest activity first (AGL-2598).\n *\n * Every query here carries the viewer's scope tokens and an `orderBy`, and\n * each shape sits on an index the foundation declared:\n * `(visibleTo, pipelineId, status, updatedAt DESC)`. Passing `null` for the\n * pipeline or an EMPTY token list issues nothing, which is how a column\n * stays quiet until the pipeline has loaded and a collapsed Won column\n * costs no read. A `null` token list is the ORGANIZATION level (AGL-2630):\n * the clause is dropped and the query rides the `(pipelineId, status,\n * updatedAt DESC)` twin of that index.\n */\nexport function useDealsByStatus(\n orgId: string | null,\n readTokens: readonly string[] | null,\n pipelineId: string | null,\n status: CrmDealStatus,\n max: number,\n) {\n const firestore = useFirestore()\n return useFirestoreCollection<DealDoc>(\n () =>\n orgId && pipelineId && crmScopeListable(readTokens)\n ? query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n ...crmVisibleToClause(readTokens),\n where('pipelineId', '==', pipelineId),\n where('status', '==', status),\n orderBy('updatedAt', 'desc'),\n limit(max),\n )\n : null,\n [firestore, orgId, readTokens, pipelineId, status, max],\n { idField: '$id' },\n )\n}\n\n/**\n * Every deal the viewer may see, paged, optionally narrowed to one status\n * and to one pipeline — the table's read. Four shapes, four indexes:\n * `(visibleTo, updatedAt DESC)`, `(visibleTo, status, updatedAt DESC)`,\n * `(visibleTo, pipelineId, updatedAt DESC)` and\n * `(visibleTo, pipelineId, status, updatedAt DESC)`. At the organization\n * level (AGL-2630) the scope clause is dropped and the same shapes ride\n * their twins without `visibleTo` — `(status, updatedAt DESC)`,\n * `(pipelineId, updatedAt DESC)`, `(pipelineId, status, updatedAt DESC)`;\n * the first shape orders on one field and needs none.\n */\nexport function usePagedDeals(\n orgId: string | null,\n readTokens: readonly string[] | null,\n status: CrmDealStatus | 'all',\n pipelineId: string | null = null,\n) {\n const firestore = useFirestore()\n return usePagedCollection<DealDoc>(\n (pageLimit) =>\n orgId && crmScopeListable(readTokens)\n ? query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n ...crmVisibleToClause(readTokens),\n ...(pipelineId ? [where('pipelineId', '==', pipelineId)] : []),\n ...(status === 'all' ? [] : [where('status', '==', status)]),\n orderBy('updatedAt', 'desc'),\n limit(pageLimit),\n )\n : null,\n [firestore, orgId, readTokens, status, pipelineId],\n { idField: '$id' },\n )\n}\n\n/**\n * The deals that name one contact or one company, for the card on that\n * record's page. `(visibleTo, contactId|companyId, updatedAt DESC)`.\n */\nexport function useLinkedDeals(\n orgId: string | null,\n readTokens: readonly string[] | null,\n link: { contactId: string } | { companyId: string },\n) {\n const firestore = useFirestore()\n const field = 'contactId' in link ? 'contactId' : 'companyId'\n const id = 'contactId' in link ? link.contactId : link.companyId\n return useFirestoreCollection<DealDoc>(\n () =>\n orgId && id && crmScopeListable(readTokens)\n ? query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n ...crmVisibleToClause(readTokens),\n where(field, '==', id),\n orderBy('updatedAt', 'desc'),\n limit(LINKED_DEALS_LIMIT),\n )\n : null,\n [firestore, orgId, readTokens, field, id],\n { idField: '$id' },\n )\n}\n\n/** One deal, live. `undefined` while loading, and after a delete. */\nexport function useDeal(orgId: string | null, dealId: string) {\n const firestore = useFirestore()\n return useFirestoreDoc<DealDoc>(\n () =>\n orgId && dealId\n ? doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, dealId)\n : null,\n [firestore, orgId, dealId],\n { idField: '$id' },\n )\n}\n"],"names":["CRM_COLLECTIONS","useFirestore","useFirestoreCollection","useFirestoreDoc","usePagedCollection","collection","doc","limit","orderBy","query","where","crmScopeListable","crmVisibleToClause","BOARD_OPEN_LIMIT","BOARD_CLOSED_LIMIT","LINKED_DEALS_LIMIT","useDealsByStatus","orgId","readTokens","pipelineId","status","max","firestore","deals","idField","usePagedDeals","pageLimit","useLinkedDeals","link","field","id","contactId","companyId","useDeal","dealId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,eAAe,QAA4B,eAAc;AAClE,SACEC,YAAY,EACZC,sBAAsB,EACtBC,eAAe,EACfC,kBAAkB,QACb,iCAAgC;AACvC,SACEC,UAAU,EACVC,GAAG,EACHC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,KAAK,QACA,qBAAoB;AAE3B,SAASC,gBAAgB,EAAEC,kBAAkB,QAAQ,qBAAiB;AAEtE;;;CAGC,GACD,OAAO,MAAMC,mBAAmB,IAAG;AAEnC,8DAA8D,GAC9D,OAAO,MAAMC,qBAAqB,GAAE;AAEpC,qDAAqD,GACrD,OAAO,MAAMC,qBAAqB,GAAE;AAEpC;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,iBACdC,KAAoB,EACpBC,UAAoC,EACpCC,UAAyB,EACzBC,MAAqB,EACrBC,GAAW;IAEX,MAAMC,YAAYrB;IAClB,OAAOC,uBACL,IACEe,SAASE,cAAcR,iBAAiBO,cACpCT,MACEJ,WAAWiB,WAAW,QAAQL,OAAOjB,gBAAgBuB,KAAK,MACvDX,mBAAmBM,aACtBR,MAAM,cAAc,MAAMS,aAC1BT,MAAM,UAAU,MAAMU,SACtBZ,QAAQ,aAAa,SACrBD,MAAMc,QAER,MACN;QAACC;QAAWL;QAAOC;QAAYC;QAAYC;QAAQC;KAAI,EACvD;QAAEG,SAAS;IAAM;AAErB;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,cACdR,KAAoB,EACpBC,UAAoC,EACpCE,MAA6B,EAC7BD,aAA4B,IAAI;IAEhC,MAAMG,YAAYrB;IAClB,OAAOG,mBACL,CAACsB,YACCT,SAASN,iBAAiBO,cACtBT,MACEJ,WAAWiB,WAAW,QAAQL,OAAOjB,gBAAgBuB,KAAK,MACvDX,mBAAmBM,gBAClBC,aAAa;YAACT,MAAM,cAAc,MAAMS;SAAY,GAAG,EAAE,KACzDC,WAAW,QAAQ,EAAE,GAAG;YAACV,MAAM,UAAU,MAAMU;SAAQ,EAC3DZ,QAAQ,aAAa,SACrBD,MAAMmB,cAER,MACN;QAACJ;QAAWL;QAAOC;QAAYE;QAAQD;KAAW,EAClD;QAAEK,SAAS;IAAM;AAErB;AAEA;;;CAGC,GACD,OAAO,SAASG,eACdV,KAAoB,EACpBC,UAAoC,EACpCU,IAAmD;IAEnD,MAAMN,YAAYrB;IAClB,MAAM4B,QAAQ,eAAeD,OAAO,cAAc;IAClD,MAAME,KAAK,eAAeF,OAAOA,KAAKG,SAAS,GAAGH,KAAKI,SAAS;IAChE,OAAO9B,uBACL,IACEe,SAASa,MAAMnB,iBAAiBO,cAC5BT,MACEJ,WAAWiB,WAAW,QAAQL,OAAOjB,gBAAgBuB,KAAK,MACvDX,mBAAmBM,aACtBR,MAAMmB,OAAO,MAAMC,KACnBtB,QAAQ,aAAa,SACrBD,MAAMQ,uBAER,MACN;QAACO;QAAWL;QAAOC;QAAYW;QAAOC;KAAG,EACzC;QAAEN,SAAS;IAAM;AAErB;AAEA,mEAAmE,GACnE,OAAO,SAASS,QAAQhB,KAAoB,EAAEiB,MAAc;IAC1D,MAAMZ,YAAYrB;IAClB,OAAOE,gBACL,IACEc,SAASiB,SACL5B,IAAIgB,WAAW,QAAQL,OAAOjB,gBAAgBuB,KAAK,EAAEW,UACrD,MACN;QAACZ;QAAWL;QAAOiB;KAAO,EAC1B;QAAEV,SAAS;IAAM;AAErB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-deals.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'use client'\n\nimport { CRM_COLLECTIONS, type CrmDealStatus } from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreCollection,\n useFirestoreDoc,\n usePagedCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport type { DealDoc } from '../model/deal-board-model'\nimport { crmScopeListable, crmVisibleToClause } from './use-crm-scope'\n\n/**\n * The most open cards one pipeline draws. A board past this is a board\n * nobody reads column by column; the table is paged and reaches the rest.\n */\nexport const BOARD_OPEN_LIMIT = 500\n\n/** Closed deals shown per closed column when it is expanded. */\nexport const BOARD_CLOSED_LIMIT = 50\n\n/** Deals listed on a contact's or a company's page. */\nexport const LINKED_DEALS_LIMIT = 25\n\n/**\n * The deals of one status in one pipeline, newest activity first (AGL-2598).\n *\n * Every query here carries the viewer's scope tokens and an `orderBy`, and\n * each shape sits on an index the foundation declared:\n * `(visibleTo, pipelineId, status, updatedAt DESC)`. Passing `null` for the\n * pipeline or an EMPTY token list issues nothing, which is how a column\n * stays quiet until the pipeline has loaded and a collapsed Won column\n * costs no read. A `null` token list is the ORGANIZATION level (AGL-2630):\n * the clause is dropped and the query rides the `(pipelineId, status,\n * updatedAt DESC)` twin of that index.\n */\nexport function useDealsByStatus(\n orgId: string | null,\n readTokens: readonly string[] | null,\n pipelineId: string | null,\n status: CrmDealStatus,\n max: number,\n) {\n const firestore = useFirestore()\n return useFirestoreCollection<DealDoc>(\n () =>\n orgId && pipelineId && crmScopeListable(readTokens)\n ? query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n ...crmVisibleToClause(readTokens),\n where('pipelineId', '==', pipelineId),\n where('status', '==', status),\n orderBy('updatedAt', 'desc'),\n limit(max),\n )\n : null,\n [firestore, orgId, readTokens, pipelineId, status, max],\n { idField: '$id' },\n )\n}\n\n/**\n * Every deal the viewer may see, paged, optionally narrowed to one status\n * and to one pipeline — the table's read. Four shapes, four indexes:\n * `(visibleTo, updatedAt DESC)`, `(visibleTo, status, updatedAt DESC)`,\n * `(visibleTo, pipelineId, updatedAt DESC)` and\n * `(visibleTo, pipelineId, status, updatedAt DESC)`. At the organization\n * level (AGL-2630) the scope clause is dropped and the same shapes ride\n * their twins without `visibleTo` — `(status, updatedAt DESC)`,\n * `(pipelineId, updatedAt DESC)`, `(pipelineId, status, updatedAt DESC)`;\n * the first shape orders on one field and needs none.\n */\nexport function usePagedDeals(\n orgId: string | null,\n readTokens: readonly string[] | null,\n status: CrmDealStatus | 'all',\n pipelineId: string | null = null,\n) {\n const firestore = useFirestore()\n return usePagedCollection<DealDoc>(\n (pageLimit) =>\n orgId && crmScopeListable(readTokens)\n ? query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n ...crmVisibleToClause(readTokens),\n ...(pipelineId ? [where('pipelineId', '==', pipelineId)] : []),\n ...(status === 'all' ? [] : [where('status', '==', status)]),\n orderBy('updatedAt', 'desc'),\n limit(pageLimit),\n )\n : null,\n [firestore, orgId, readTokens, status, pipelineId],\n { idField: '$id' },\n )\n}\n\n/**\n * How many deals a table search reads (AGL-3315): the newest by last\n * change, in the table's own scope, status and pipeline, plus one probe row\n * so \"there are more\" is a fact rather than a guess at the boundary.\n */\nexport const DEAL_SEARCH_WINDOW = 1000\n\n/**\n * The deals a table SEARCH runs over (AGL-3315) — a window, read only while\n * the search box holds something.\n *\n * A search has to find a deal on any page, and it has to find it by a word\n * from the middle of its title: \"Acme renewal\" is looked for as \"renewal\".\n * An indexed prefix, the way the companies list searches names, answers\n * only the first word, and word tokens would need every writer and a\n * backfill to agree on them. An organization's deals are counted in the\n * tens or hundreds — the board already reads every open one — so the\n * window is the whole collection for all but the largest, and the table\n * says so when it is not. The query is the table's own, one of the four\n * shapes `usePagedDeals` states, so it rides indexes that already exist.\n */\nexport function useDealSearchWindow(\n orgId: string | null,\n readTokens: readonly string[] | null,\n status: CrmDealStatus | 'all',\n pipelineId: string | null = null,\n) {\n const firestore = useFirestore()\n return useFirestoreCollection<DealDoc>(\n () =>\n orgId && crmScopeListable(readTokens)\n ? query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n ...crmVisibleToClause(readTokens),\n ...(pipelineId ? [where('pipelineId', '==', pipelineId)] : []),\n ...(status === 'all' ? [] : [where('status', '==', status)]),\n orderBy('updatedAt', 'desc'),\n limit(DEAL_SEARCH_WINDOW + 1),\n )\n : null,\n [firestore, orgId, readTokens, status, pipelineId],\n { idField: '$id' },\n )\n}\n\n/**\n * The deals that name one contact or one company, for the card on that\n * record's page. `(visibleTo, contactId|companyId, updatedAt DESC)`.\n */\nexport function useLinkedDeals(\n orgId: string | null,\n readTokens: readonly string[] | null,\n link: { contactId: string } | { companyId: string },\n) {\n const firestore = useFirestore()\n const field = 'contactId' in link ? 'contactId' : 'companyId'\n const id = 'contactId' in link ? link.contactId : link.companyId\n return useFirestoreCollection<DealDoc>(\n () =>\n orgId && id && crmScopeListable(readTokens)\n ? query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n ...crmVisibleToClause(readTokens),\n where(field, '==', id),\n orderBy('updatedAt', 'desc'),\n limit(LINKED_DEALS_LIMIT),\n )\n : null,\n [firestore, orgId, readTokens, field, id],\n { idField: '$id' },\n )\n}\n\n/** One deal, live. `undefined` while loading, and after a delete. */\nexport function useDeal(orgId: string | null, dealId: string) {\n const firestore = useFirestore()\n return useFirestoreDoc<DealDoc>(\n () =>\n orgId && dealId\n ? doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, dealId)\n : null,\n [firestore, orgId, dealId],\n { idField: '$id' },\n )\n}\n"],"names":["CRM_COLLECTIONS","useFirestore","useFirestoreCollection","useFirestoreDoc","usePagedCollection","collection","doc","limit","orderBy","query","where","crmScopeListable","crmVisibleToClause","BOARD_OPEN_LIMIT","BOARD_CLOSED_LIMIT","LINKED_DEALS_LIMIT","useDealsByStatus","orgId","readTokens","pipelineId","status","max","firestore","deals","idField","usePagedDeals","pageLimit","DEAL_SEARCH_WINDOW","useDealSearchWindow","useLinkedDeals","link","field","id","contactId","companyId","useDeal","dealId"],"mappings":"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"}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { type EmailStateStatus, type CrmLeadFields, type CrmLeadStatus } from '@aglyn/aglyn';
|
|
18
18
|
import type { CrmViewFilterClause } from '@aglyn/aglyn';
|
|
19
19
|
import type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter';
|
|
20
|
-
import { type
|
|
20
|
+
import { type ListGridFilterCodec } from '@aglyn/shared-ui-jsx/const/list-grid-filter';
|
|
21
21
|
/**
|
|
22
22
|
* What the Leads section's `Show` control offers (AGL-2608): the two
|
|
23
23
|
* aggregate views and each status on its own. `open` is the default — the
|
|
@@ -106,8 +106,8 @@ export declare function leadClausesToStore(shown: readonly CrmViewFilterClause[]
|
|
|
106
106
|
/** Whether a lead answers every clause the grid shows. */
|
|
107
107
|
export declare function leadMatchesClauses(lead: Readonly<Record<string, unknown>> & Pick<CrmLeadFields, 'status'>, clauses: readonly CrmViewFilterClause[]): boolean;
|
|
108
108
|
/** Campaign clauses were stored as `contains`; the panel's select says `is`. */
|
|
109
|
-
export declare const LEAD_CAMPAIGN_CODEC:
|
|
109
|
+
export declare const LEAD_CAMPAIGN_CODEC: ListGridFilterCodec;
|
|
110
110
|
/** "No lead source" is an `isEmpty` clause, shown as a choice of the select. */
|
|
111
|
-
export declare const LEAD_SOURCE_CODEC:
|
|
111
|
+
export declare const LEAD_SOURCE_CODEC: ListGridFilterCodec;
|
|
112
112
|
/** The Leads fields whose stored clauses translate their own way. */
|
|
113
|
-
export declare const LEAD_FILTER_CODECS: Readonly<Record<string,
|
|
113
|
+
export declare const LEAD_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>>;
|
|
@@ -15,7 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { EMAIL_STATE_LABELS, EMAIL_STATE_STATUSES, CRM_LEAD_STATUS_LABELS, emailStateForbidsEmail, crmLeadStatus, isCrmLeadOpen, normalizeCrmPicklistLabel, readCampaignIds, readEmailState } from "@aglyn/aglyn";
|
|
18
|
-
import {
|
|
18
|
+
import { listSelectCodec, listRowMatchesSearch } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
19
19
|
export const LEAD_FILTERS = [
|
|
20
20
|
'open',
|
|
21
21
|
'new',
|
|
@@ -88,7 +88,7 @@ export const LEAD_EMAIL_FILTER_LABELS = _extends({
|
|
|
88
88
|
*/ export function leadMatchesSearch(lead, term) {
|
|
89
89
|
// Tags are a list; a stray string there is no tag of the lead's.
|
|
90
90
|
const tags = Array.isArray(lead['tags']) ? lead['tags'] : undefined;
|
|
91
|
-
return
|
|
91
|
+
return listRowMatchesSearch(_extends({}, lead, {
|
|
92
92
|
tags
|
|
93
93
|
}), LEAD_SEARCH_FIELDS, term.trim().split(/\s+/));
|
|
94
94
|
}
|
|
@@ -254,7 +254,7 @@ const listed = (clause)=>clause.op === 'isAnyOf' ? clause.value.split(',').map((
|
|
|
254
254
|
value: clause.value
|
|
255
255
|
} : null,
|
|
256
256
|
toClause: (item)=>{
|
|
257
|
-
const clause =
|
|
257
|
+
const clause = listSelectCodec.toClause(item);
|
|
258
258
|
return clause && clause.op === 'equals' ? _extends({}, clause, {
|
|
259
259
|
op: 'contains'
|
|
260
260
|
}) : null;
|
|
@@ -264,9 +264,9 @@ const listed = (clause)=>clause.op === 'isAnyOf' ? clause.value.split(',').map((
|
|
|
264
264
|
toItem: (clause)=>clause.op === 'isEmpty' ? {
|
|
265
265
|
operator: 'is',
|
|
266
266
|
value: LEAD_SOURCE_FILTER_NONE
|
|
267
|
-
} :
|
|
267
|
+
} : listSelectCodec.toItem(clause),
|
|
268
268
|
toClause: (item)=>{
|
|
269
|
-
const clause =
|
|
269
|
+
const clause = listSelectCodec.toClause(item);
|
|
270
270
|
if ((clause == null ? void 0 : clause.op) === 'equals' && clause.value === LEAD_SOURCE_FILTER_NONE) {
|
|
271
271
|
return {
|
|
272
272
|
field: clause.field,
|
|
@@ -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 CrmGridFilterCodec, crmSelectCodec, crmRowMatchesSearch } from './crm-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 crmRowMatchesSearch({ ...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: CrmGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'contains' || clause.op === 'equals'\n ? { operator: 'is', value: clause.value }\n : null,\n toClause: (item) => {\n const clause = crmSelectCodec.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: CrmGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'isEmpty'\n ? { operator: 'is', value: LEAD_SOURCE_FILTER_NONE }\n : crmSelectCodec.toItem(clause),\n toClause: (item) => {\n const clause = crmSelectCodec.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, CrmGridFilterCodec>> = {\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","crmSelectCodec","crmRowMatchesSearch","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 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,55 +0,0 @@
|
|
|
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 { CrmViewFilterClause } from '@aglyn/aglyn';
|
|
18
|
-
import { type ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter';
|
|
19
|
-
import type { CrmFilterOption } from '../model/crm-grid-filter';
|
|
20
|
-
export type { CrmFilterOption } from '../model/crm-grid-filter';
|
|
21
|
-
export interface CrmFilterBarProps {
|
|
22
|
-
/** The list's grammar — every field a clause may name. */
|
|
23
|
-
fields: readonly ListFilterField[];
|
|
24
|
-
/** How a field reads on a chip; a field without one reads as its column. */
|
|
25
|
-
headers: Readonly<Record<string, string>>;
|
|
26
|
-
clauses: readonly CrmViewFilterClause[];
|
|
27
|
-
onChange: (clauses: CrmViewFilterClause[]) => void;
|
|
28
|
-
/** Choices per field, so a chip names a picked value by its label. */
|
|
29
|
-
options?: Readonly<Record<string, readonly CrmFilterOption[]>>;
|
|
30
|
-
/**
|
|
31
|
-
* The field the query is serving, marked so the reader knows which reached
|
|
32
|
-
* everything. Omitted on a list whose every clause narrows the same rows.
|
|
33
|
-
*/
|
|
34
|
-
servedField?: string | null;
|
|
35
|
-
/** Whether chips carry the served / loaded-window distinction at all. */
|
|
36
|
-
marksServed?: boolean;
|
|
37
|
-
disabled?: boolean;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Every clause a CRM list is narrowed by, as chips over the grid (AGL-2617,
|
|
41
|
-
* AGL-3313).
|
|
42
|
-
*
|
|
43
|
-
* The clauses are ADDED through the grid's own Filters panel; this bar is
|
|
44
|
-
* what shows the SET. The free DataGrid's panel holds one item at a time
|
|
45
|
-
* and a saved view holds several, so without it a view narrowed by lead
|
|
46
|
-
* source and status would show one of the two and hide the other. Each
|
|
47
|
-
* chip reads as a sentence — "Owner is Dana", "Created on or after 1 Jan"
|
|
48
|
-
* — and removes its clause; where some clauses reach the query and others
|
|
49
|
-
* narrow the loaded rows, the served one is marked.
|
|
50
|
-
*/
|
|
51
|
-
export declare function CrmFilterBar(props: CrmFilterBarProps): import("react").JSX.Element;
|
|
52
|
-
export declare namespace CrmFilterBar {
|
|
53
|
-
var displayName: string;
|
|
54
|
-
}
|
|
55
|
-
export default CrmFilterBar;
|
|
@@ -1,93 +0,0 @@
|
|
|
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
|
-
*/ 'use client';
|
|
17
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
-
import { listFilterOperatorLabel } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
19
|
-
import { Chip, Stack, Tooltip } from "@mui/material";
|
|
20
|
-
/** Operators that carry no value. */ const VALUELESS = new Set([
|
|
21
|
-
'isEmpty',
|
|
22
|
-
'isNotEmpty'
|
|
23
|
-
]);
|
|
24
|
-
/** Operators that take several values, comma-joined the way the grammar splits them. */ const MULTI = new Set([
|
|
25
|
-
'isAnyOf'
|
|
26
|
-
]);
|
|
27
|
-
const dayLabel = (raw)=>{
|
|
28
|
-
const at = new Date(raw);
|
|
29
|
-
return Number.isNaN(at.getTime()) ? raw : at.toLocaleDateString();
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Every clause a CRM list is narrowed by, as chips over the grid (AGL-2617,
|
|
33
|
-
* AGL-3313).
|
|
34
|
-
*
|
|
35
|
-
* The clauses are ADDED through the grid's own Filters panel; this bar is
|
|
36
|
-
* what shows the SET. The free DataGrid's panel holds one item at a time
|
|
37
|
-
* and a saved view holds several, so without it a view narrowed by lead
|
|
38
|
-
* source and status would show one of the two and hide the other. Each
|
|
39
|
-
* chip reads as a sentence — "Owner is Dana", "Created on or after 1 Jan"
|
|
40
|
-
* — and removes its clause; where some clauses reach the query and others
|
|
41
|
-
* narrow the loaded rows, the served one is marked.
|
|
42
|
-
*/ export function CrmFilterBar(props) {
|
|
43
|
-
const { fields, headers, clauses, onChange, options = {}, servedField = null, marksServed = true, disabled = false } = props;
|
|
44
|
-
const header = (column)=>{
|
|
45
|
-
var _headers_column;
|
|
46
|
-
return (_headers_column = headers[column]) != null ? _headers_column : column;
|
|
47
|
-
};
|
|
48
|
-
/** How a clause reads: its header, its operator, and its value by label. */ const sentence = (clause)=>{
|
|
49
|
-
var _clause_label;
|
|
50
|
-
const field = fields.find((entry)=>entry.column === clause.field);
|
|
51
|
-
const choices = options[clause.field];
|
|
52
|
-
const named = (value)=>{
|
|
53
|
-
var _ref;
|
|
54
|
-
var _choices_find;
|
|
55
|
-
return (_ref = choices == null ? void 0 : (_choices_find = choices.find((option)=>option.value === value)) == null ? void 0 : _choices_find.label) != null ? _ref : value;
|
|
56
|
-
};
|
|
57
|
-
const value = VALUELESS.has(clause.op) ? '' : MULTI.has(clause.op) ? clause.value.split(',').map((entry)=>entry.trim()).filter(Boolean).map(named).join(', ') : (_clause_label = clause.label) != null ? _clause_label : (field == null ? void 0 : field.kind) === 'date' ? dayLabel(clause.value) : named(clause.value);
|
|
58
|
-
return `${header(clause.field)} ${listFilterOperatorLabel(clause.op)}${value ? ` ${value}` : ''}`;
|
|
59
|
-
};
|
|
60
|
-
if (!clauses.length) return null;
|
|
61
|
-
return /*#__PURE__*/ _jsx(Stack, {
|
|
62
|
-
direction: "row",
|
|
63
|
-
spacing: 1,
|
|
64
|
-
useFlexGap: true,
|
|
65
|
-
sx: {
|
|
66
|
-
alignItems: 'center',
|
|
67
|
-
flexWrap: 'wrap'
|
|
68
|
-
},
|
|
69
|
-
role: "list",
|
|
70
|
-
"aria-label": "Filters",
|
|
71
|
-
children: clauses.map((clause, index)=>{
|
|
72
|
-
const served = marksServed && servedField !== null && clause.field === servedField;
|
|
73
|
-
const key = `${clause.field}-${clause.op}-${index}`;
|
|
74
|
-
const chip = /*#__PURE__*/ _jsx(Chip, {
|
|
75
|
-
role: "listitem",
|
|
76
|
-
size: "small",
|
|
77
|
-
variant: served ? 'filled' : 'outlined',
|
|
78
|
-
color: served ? 'primary' : 'default',
|
|
79
|
-
label: sentence(clause),
|
|
80
|
-
disabled: disabled,
|
|
81
|
-
onDelete: ()=>onChange(clauses.filter((_entry, at)=>at !== index))
|
|
82
|
-
}, key);
|
|
83
|
-
return marksServed ? /*#__PURE__*/ _jsx(Tooltip, {
|
|
84
|
-
title: served ? 'Searched across every record' : 'Narrows the records already loaded',
|
|
85
|
-
children: chip
|
|
86
|
-
}, key) : chip;
|
|
87
|
-
})
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
CrmFilterBar.displayName = 'CrmFilterBar';
|
|
91
|
-
export default CrmFilterBar;
|
|
92
|
-
|
|
93
|
-
//# sourceMappingURL=crm-filter-bar.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/crm-filter-bar.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n'use client'\n\nimport type { CrmViewFilterClause } from '@aglyn/aglyn'\nimport {\n type ListFilterField,\n listFilterOperatorLabel,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { Chip, Stack, Tooltip } from '@mui/material'\nimport type { CrmFilterOption } from '../model/crm-grid-filter'\n\nexport type { CrmFilterOption } from '../model/crm-grid-filter'\n\nexport interface CrmFilterBarProps {\n /** The list's grammar — every field a clause may name. */\n fields: readonly ListFilterField[]\n /** How a field reads on a chip; a field without one reads as its column. */\n headers: Readonly<Record<string, string>>\n clauses: readonly CrmViewFilterClause[]\n onChange: (clauses: CrmViewFilterClause[]) => void\n /** Choices per field, so a chip names a picked value by its label. */\n options?: Readonly<Record<string, readonly CrmFilterOption[]>>\n /**\n * The field the query is serving, marked so the reader knows which reached\n * everything. Omitted on a list whose every clause narrows the same rows.\n */\n servedField?: string | null\n /** Whether chips carry the served / loaded-window distinction at all. */\n marksServed?: boolean\n disabled?: boolean\n}\n\n/** Operators that carry no value. */\nconst VALUELESS = new Set(['isEmpty', 'isNotEmpty'])\n/** Operators that take several values, comma-joined the way the grammar splits them. */\nconst MULTI = new Set(['isAnyOf'])\n\nconst dayLabel = (raw: string): string => {\n const at = new Date(raw)\n return Number.isNaN(at.getTime()) ? raw : at.toLocaleDateString()\n}\n\n/**\n * Every clause a CRM list is narrowed by, as chips over the grid (AGL-2617,\n * AGL-3313).\n *\n * The clauses are ADDED through the grid's own Filters panel; this bar is\n * what shows the SET. The free DataGrid's panel holds one item at a time\n * and a saved view holds several, so without it a view narrowed by lead\n * source and status would show one of the two and hide the other. Each\n * chip reads as a sentence — \"Owner is Dana\", \"Created on or after 1 Jan\"\n * — and removes its clause; where some clauses reach the query and others\n * narrow the loaded rows, the served one is marked.\n */\nexport function CrmFilterBar(props: CrmFilterBarProps) {\n const {\n fields,\n headers,\n clauses,\n onChange,\n options = {},\n servedField = null,\n marksServed = true,\n disabled = false,\n } = props\n const header = (column: string) => headers[column] ?? column\n\n /** How a clause reads: its header, its operator, and its value by label. */\n const sentence = (clause: CrmViewFilterClause): string => {\n const field = fields.find((entry) => entry.column === clause.field)\n const choices = options[clause.field]\n const named = (value: string) =>\n choices?.find((option) => option.value === value)?.label ?? value\n const value = VALUELESS.has(clause.op)\n ? ''\n : MULTI.has(clause.op)\n ? clause.value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n .map(named)\n .join(', ')\n : clause.label ?? (field?.kind === 'date' ? dayLabel(clause.value) : named(clause.value))\n return `${header(clause.field)} ${listFilterOperatorLabel(clause.op)}${value ? ` ${value}` : ''}`\n }\n\n if (!clauses.length) return null\n return (\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n role=\"list\"\n aria-label=\"Filters\"\n >\n {clauses.map((clause, index) => {\n const served = marksServed && servedField !== null && clause.field === servedField\n const key = `${clause.field}-${clause.op}-${index}`\n const chip = (\n <Chip\n key={key}\n role=\"listitem\"\n size=\"small\"\n variant={served ? 'filled' : 'outlined'}\n color={served ? 'primary' : 'default'}\n label={sentence(clause)}\n disabled={disabled}\n onDelete={() => onChange(clauses.filter((_entry, at) => at !== index))}\n />\n )\n return marksServed ? (\n <Tooltip\n key={key}\n title={served ? 'Searched across every record' : 'Narrows the records already loaded'}\n >\n {chip}\n </Tooltip>\n ) : (\n chip\n )\n })}\n </Stack>\n )\n}\nCrmFilterBar.displayName = 'CrmFilterBar'\n\nexport default CrmFilterBar\n"],"names":["listFilterOperatorLabel","Chip","Stack","Tooltip","VALUELESS","Set","MULTI","dayLabel","raw","at","Date","Number","isNaN","getTime","toLocaleDateString","CrmFilterBar","props","fields","headers","clauses","onChange","options","servedField","marksServed","disabled","header","column","sentence","clause","field","find","entry","choices","named","value","option","label","has","op","split","map","trim","filter","Boolean","join","kind","length","direction","spacing","useFlexGap","sx","alignItems","flexWrap","role","aria-label","index","served","key","chip","size","variant","color","onDelete","_entry","title","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;AAGA,SAEEA,uBAAuB,QAClB,yCAAwC;AAC/C,SAASC,IAAI,EAAEC,KAAK,EAAEC,OAAO,QAAQ,gBAAe;AAwBpD,mCAAmC,GACnC,MAAMC,YAAY,IAAIC,IAAI;IAAC;IAAW;CAAa;AACnD,sFAAsF,GACtF,MAAMC,QAAQ,IAAID,IAAI;IAAC;CAAU;AAEjC,MAAME,WAAW,CAACC;IAChB,MAAMC,KAAK,IAAIC,KAAKF;IACpB,OAAOG,OAAOC,KAAK,CAACH,GAAGI,OAAO,MAAML,MAAMC,GAAGK,kBAAkB;AACjE;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,aAAaC,KAAwB;IACnD,MAAM,EACJC,MAAM,EACNC,OAAO,EACPC,OAAO,EACPC,QAAQ,EACRC,UAAU,CAAC,CAAC,EACZC,cAAc,IAAI,EAClBC,cAAc,IAAI,EAClBC,WAAW,KAAK,EACjB,GAAGR;IACJ,MAAMS,SAAS,CAACC;YAAmBR;gBAAAA,kBAAAA,OAAO,CAACQ,OAAO,YAAfR,kBAAmBQ;;IAEtD,0EAA0E,GAC1E,MAAMC,WAAW,CAACC;YAcVA;QAbN,MAAMC,QAAQZ,OAAOa,IAAI,CAAC,CAACC,QAAUA,MAAML,MAAM,KAAKE,OAAOC,KAAK;QAClE,MAAMG,UAAUX,OAAO,CAACO,OAAOC,KAAK,CAAC;QACrC,MAAMI,QAAQ,CAACC;;gBACbF;2BAAAA,4BAAAA,gBAAAA,QAASF,IAAI,CAAC,CAACK,SAAWA,OAAOD,KAAK,KAAKA,2BAA3CF,cAAmDI,KAAK,mBAAIF;;QAC9D,MAAMA,QAAQ9B,UAAUiC,GAAG,CAACT,OAAOU,EAAE,IACjC,KACAhC,MAAM+B,GAAG,CAACT,OAAOU,EAAE,IACjBV,OAAOM,KAAK,CACTK,KAAK,CAAC,KACNC,GAAG,CAAC,CAACT,QAAUA,MAAMU,IAAI,IACzBC,MAAM,CAACC,SACPH,GAAG,CAACP,OACJW,IAAI,CAAC,SACRhB,gBAAAA,OAAOQ,KAAK,YAAZR,gBAAiBC,CAAAA,yBAAAA,MAAOgB,IAAI,MAAK,SAAStC,SAASqB,OAAOM,KAAK,IAAID,MAAML,OAAOM,KAAK;QAC3F,OAAO,GAAGT,OAAOG,OAAOC,KAAK,EAAE,CAAC,EAAE7B,wBAAwB4B,OAAOU,EAAE,IAAIJ,QAAQ,CAAC,CAAC,EAAEA,OAAO,GAAG,IAAI;IACnG;IAEA,IAAI,CAACf,QAAQ2B,MAAM,EAAE,OAAO;IAC5B,qBACE,KAAC5C;QACC6C,WAAU;QACVC,SAAS;QACTC,UAAU;QACVC,IAAI;YAAEC,YAAY;YAAUC,UAAU;QAAO;QAC7CC,MAAK;QACLC,cAAW;kBAEVnC,QAAQqB,GAAG,CAAC,CAACZ,QAAQ2B;YACpB,MAAMC,SAASjC,eAAeD,gBAAgB,QAAQM,OAAOC,KAAK,KAAKP;YACvE,MAAMmC,MAAM,GAAG7B,OAAOC,KAAK,CAAC,CAAC,EAAED,OAAOU,EAAE,CAAC,CAAC,EAAEiB,OAAO;YACnD,MAAMG,qBACJ,KAACzD;gBAECoD,MAAK;gBACLM,MAAK;gBACLC,SAASJ,SAAS,WAAW;gBAC7BK,OAAOL,SAAS,YAAY;gBAC5BpB,OAAOT,SAASC;gBAChBJ,UAAUA;gBACVsC,UAAU,IAAM1C,SAASD,QAAQuB,MAAM,CAAC,CAACqB,QAAQtD,KAAOA,OAAO8C;eAP1DE;YAUT,OAAOlC,4BACL,KAACpB;gBAEC6D,OAAOR,SAAS,iCAAiC;0BAEhDE;eAHID,OAMPC;QAEJ;;AAGN;AACA3C,aAAakD,WAAW,GAAG;AAE3B,eAAelD,aAAY"}
|
|
@@ -1,77 +0,0 @@
|
|
|
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 { CrmViewFilterClause } from '@aglyn/aglyn';
|
|
18
|
-
import type { GridFilterModel } from '@mui/x-data-grid';
|
|
19
|
-
import { type CrmGridFilterCodec } from '../model/crm-grid-filter';
|
|
20
|
-
export interface CrmGridFilterOptions {
|
|
21
|
-
/** The clauses the list is narrowed by — the saved view's working filters. */
|
|
22
|
-
clauses: readonly CrmViewFilterClause[];
|
|
23
|
-
onChange: (clauses: CrmViewFilterClause[]) => void;
|
|
24
|
-
/** Fields whose values are picked, which the panel shows as a select. */
|
|
25
|
-
selectFields?: readonly string[];
|
|
26
|
-
/** A field whose stored clauses predate the panel, translated its own way. */
|
|
27
|
-
codecs?: Readonly<Record<string, CrmGridFilterCodec>>;
|
|
28
|
-
/**
|
|
29
|
-
* The query serves ONE clause, so the panel's replaces every other — a
|
|
30
|
-
* server-paged list, which has no loaded window to narrow further.
|
|
31
|
-
* `keepAlongside` names the clauses that do narrow the loaded page and so
|
|
32
|
-
* may stand beside it ("No next activity").
|
|
33
|
-
*/
|
|
34
|
-
single?: boolean;
|
|
35
|
-
keepAlongside?: (clause: CrmViewFilterClause) => boolean;
|
|
36
|
-
/**
|
|
37
|
-
* The quick search's words, held by the list when it needs them before
|
|
38
|
-
* the grid's columns exist; the hook holds them itself otherwise.
|
|
39
|
-
*/
|
|
40
|
-
search?: {
|
|
41
|
-
words: string[];
|
|
42
|
-
onChange: (words: string[]) => void;
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
/** What the grid is handed, and what the list filters its rows by. */
|
|
46
|
-
export interface CrmGridFilter {
|
|
47
|
-
/** Controlled: the panel's one item, and the quick search's words. */
|
|
48
|
-
filterModel: GridFilterModel;
|
|
49
|
-
onFilterModelChange: (model: GridFilterModel) => void;
|
|
50
|
-
/** The quick search's words, for the list to match its rows against. */
|
|
51
|
-
searchWords: string[];
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* The grid's Filters panel and quick search, bound to a list's view clauses
|
|
55
|
-
* (AGL-3313). See `model/crm-grid-filter.ts` for the shape both sides keep.
|
|
56
|
-
*
|
|
57
|
-
* ## One field, one clause; the panel edits one at a time
|
|
58
|
-
*
|
|
59
|
-
* The free DataGrid's panel holds a single item, while a saved view
|
|
60
|
-
* holds several clauses. So each FIELD holds one clause, and the panel is a
|
|
61
|
-
* window onto whichever field was last filtered: setting a value writes
|
|
62
|
-
* that field's clause, clearing it or deleting the row removes it, and
|
|
63
|
-
* choosing another column in the panel moves the window without touching
|
|
64
|
-
* the clause the reader just set. Filtering Lead source and then Status is
|
|
65
|
-
* therefore both, which is what a reader who does it means; the chips over
|
|
66
|
-
* the grid (`CrmFilterBar`) show every clause and remove any of them.
|
|
67
|
-
*
|
|
68
|
-
* ## The list answers, never the grid
|
|
69
|
-
*
|
|
70
|
-
* Every list passes `filterMode="server"`: the grid holds one page, or a
|
|
71
|
-
* window the query already narrowed, so a filter the grid ran itself would
|
|
72
|
-
* answer "no match" for a row on the next page. The list reads the clauses
|
|
73
|
-
* — onto its query where it can, over its loaded rows where it cannot —
|
|
74
|
-
* and the quick search's words, and hands the grid the rows that answer.
|
|
75
|
-
*/
|
|
76
|
-
export declare function useCrmGridFilter(options: CrmGridFilterOptions): CrmGridFilter;
|
|
77
|
-
export default useCrmGridFilter;
|