@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.
Files changed (105) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/companies-section.d.ts +6 -6
  3. package/src/lib/components/companies-section.js +54 -84
  4. package/src/lib/components/companies-section.js.map +1 -1
  5. package/src/lib/components/company-edit-drawer.js +17 -4
  6. package/src/lib/components/company-edit-drawer.js.map +1 -1
  7. package/src/lib/components/company-picker.js +6 -2
  8. package/src/lib/components/company-picker.js.map +1 -1
  9. package/src/lib/components/contact-detail-page.js +14 -2
  10. package/src/lib/components/contact-detail-page.js.map +1 -1
  11. package/src/lib/components/contact-list-columns.d.ts +3 -5
  12. package/src/lib/components/contact-list-columns.js +6 -8
  13. package/src/lib/components/contact-list-columns.js.map +1 -1
  14. package/src/lib/components/contacts-bulk-bar.js +10 -1
  15. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  16. package/src/lib/components/contacts-section.js +115 -163
  17. package/src/lib/components/contacts-section.js.map +1 -1
  18. package/src/lib/components/crm-next-activity-column.d.ts +4 -2
  19. package/src/lib/components/crm-next-activity-column.js +6 -3
  20. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  21. package/src/lib/components/deal-edit-drawer.js +8 -4
  22. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  23. package/src/lib/components/deals-section.d.ts +2 -2
  24. package/src/lib/components/deals-section.js +59 -84
  25. package/src/lib/components/deals-section.js.map +1 -1
  26. package/src/lib/components/lead-campaigns-card.js +11 -3
  27. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  28. package/src/lib/components/lead-properties-card.js +9 -1
  29. package/src/lib/components/lead-properties-card.js.map +1 -1
  30. package/src/lib/components/leads-bulk-bar.js +16 -3
  31. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  32. package/src/lib/components/leads-section.d.ts +2 -1
  33. package/src/lib/components/leads-section.js +110 -148
  34. package/src/lib/components/leads-section.js.map +1 -1
  35. package/src/lib/components/tasks-section.js +128 -121
  36. package/src/lib/components/tasks-section.js.map +1 -1
  37. package/src/lib/constants/company-filters.d.ts +13 -0
  38. package/src/lib/constants/company-filters.js +47 -17
  39. package/src/lib/constants/company-filters.js.map +1 -1
  40. package/src/lib/constants/contact-filters.d.ts +61 -10
  41. package/src/lib/constants/contact-filters.js +373 -141
  42. package/src/lib/constants/contact-filters.js.map +1 -1
  43. package/src/lib/constants/deal-filters.d.ts +44 -0
  44. package/src/lib/constants/deal-filters.js +78 -0
  45. package/src/lib/constants/deal-filters.js.map +1 -0
  46. package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
  47. package/src/lib/hooks/use-crm-list-query.js +182 -0
  48. package/src/lib/hooks/use-crm-list-query.js.map +1 -0
  49. package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
  50. package/src/lib/hooks/use-crm-tasks.js +44 -42
  51. package/src/lib/hooks/use-crm-tasks.js.map +1 -1
  52. package/src/lib/hooks/use-deals.d.ts +2 -34
  53. package/src/lib/hooks/use-deals.js +3 -62
  54. package/src/lib/hooks/use-deals.js.map +1 -1
  55. package/src/lib/model/crm-list-query.d.ts +91 -0
  56. package/src/lib/model/crm-list-query.js +182 -0
  57. package/src/lib/model/crm-list-query.js.map +1 -0
  58. package/src/lib/model/lead-filters.d.ts +49 -43
  59. package/src/lib/model/lead-filters.js +194 -88
  60. package/src/lib/model/lead-filters.js.map +1 -1
  61. package/src/lib/model/list-fields-api.d.ts +26 -0
  62. package/src/lib/model/list-fields-api.js +47 -0
  63. package/src/lib/model/list-fields-api.js.map +1 -0
  64. package/src/lib/model/list-fields.d.ts +47 -0
  65. package/src/lib/model/list-fields.js +39 -0
  66. package/src/lib/model/list-fields.js.map +1 -0
  67. package/src/lib/model/task-views.d.ts +15 -10
  68. package/src/lib/model/task-views.js +68 -9
  69. package/src/lib/model/task-views.js.map +1 -1
  70. package/src/lib/server/companies-import.js +3 -3
  71. package/src/lib/server/companies-import.js.map +1 -1
  72. package/src/lib/server/company-delete.js +3 -1
  73. package/src/lib/server/company-delete.js.map +1 -1
  74. package/src/lib/server/consent-group-participant.js +4 -0
  75. package/src/lib/server/consent-group-participant.js.map +1 -1
  76. package/src/lib/server/contact-update.js +14 -8
  77. package/src/lib/server/contact-update.js.map +1 -1
  78. package/src/lib/server/contacts-import.js +4 -2
  79. package/src/lib/server/contacts-import.js.map +1 -1
  80. package/src/lib/server/deals-import.js +4 -2
  81. package/src/lib/server/deals-import.js.map +1 -1
  82. package/src/lib/server/erase-person.js +2 -1
  83. package/src/lib/server/erase-person.js.map +1 -1
  84. package/src/lib/server/lead-create.js +4 -1
  85. package/src/lib/server/lead-create.js.map +1 -1
  86. package/src/lib/server/lead-source-values.js +9 -5
  87. package/src/lib/server/lead-source-values.js.map +1 -1
  88. package/src/lib/server/leads-import.js +4 -1
  89. package/src/lib/server/leads-import.js.map +1 -1
  90. package/src/lib/server/list-fields-routes.d.ts +29 -0
  91. package/src/lib/server/list-fields-routes.js +85 -0
  92. package/src/lib/server/list-fields-routes.js.map +1 -0
  93. package/src/lib/server/record-email-state.js +5 -2
  94. package/src/lib/server/record-email-state.js.map +1 -1
  95. package/src/lib/server/record-timeline.js +3 -2
  96. package/src/lib/server/record-timeline.js.map +1 -1
  97. package/src/lib/server/task-routes.js +6 -3
  98. package/src/lib/server/task-routes.js.map +1 -1
  99. package/src/lib/server/tasks-import.js +5 -2
  100. package/src/lib/server/tasks-import.js.map +1 -1
  101. package/src/lib/server.js +10 -1
  102. package/src/lib/server.js.map +1 -1
  103. package/src/lib/hooks/use-org-leads.d.ts +0 -50
  104. package/src/lib/hooks/use-org-leads.js +0 -104
  105. package/src/lib/hooks/use-org-leads.js.map +0 -1
@@ -17,7 +17,8 @@
17
17
  import { useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
18
18
  import { collection, limit, orderBy, query, where } from "firebase/firestore";
19
19
  import { useEffect, useMemo, useState } from "react";
20
- import { CRM_TASK_VIEW_LIMIT, crmTaskViewPlan, orderTaskRows } from "../model/task-views.js";
20
+ import { crmTaskViewBase, crmTaskViewPlan, orderTaskRows, TASK_LIST_DECLARATION } from "../model/task-views.js";
21
+ import { useCrmFoldsScope, useCrmListQuery } from "./use-crm-list-query.js";
21
22
  import { crmVisibleToClause, useCrmScope } from "./use-crm-scope.js";
22
23
  /**
23
24
  * The reader's clock, refreshed once a minute (AGL-2599).
@@ -35,17 +36,17 @@ import { crmVisibleToClause, useCrmScope } from "./use-crm-scope.js";
35
36
  return nowMs;
36
37
  }
37
38
  /**
38
- * One view of the tasks section as a live, bounded listener.
39
+ * One view of the tasks section, PAGED BY ITS QUERY (AGL-3321).
39
40
  *
40
41
  * The plan comes from `crmTaskViewPlan`, so the hook holds no opinion about
41
- * what "today" means; it translates a plan into constraints in the order the
42
- * `crmTasks` indexes are declared and caps the window. "My tasks" opens no
43
- * listener at all until the reader's uid is known — a query for
44
- * `assigneeUid == ''` would be a real read that could only answer nothing.
42
+ * what "today" means; the view's predicates are the query's base beside the
43
+ * scope clause, and Kind, Priority, Assignee and the search word are planned
44
+ * onto the same query — every page a page of matches, none narrowed in
45
+ * memory. "My tasks" opens no listener until the reader's uid is known — a
46
+ * query for `assigneeUid == ''` would be a real read that could only answer
47
+ * nothing.
45
48
  */ export function useCrmTaskList(options) {
46
- var _ref;
47
- const { hostId, org, view, uid, nowMs } = options;
48
- const firestore = useFirestore();
49
+ const { hostId, org, view, uid, nowMs, clauses = NO_CLAUSES, search = NO_WORDS } = options;
49
50
  // The org root and the reader's tokens from the one scope hook (AGL-2614)
50
51
  // — the same expression the contacts list runs, so a task listed beside a
51
52
  // contact is scoped by exactly the predicate that admitted the contact.
@@ -61,48 +62,49 @@ import { crmVisibleToClause, useCrmScope } from "./use-crm-scope.js";
61
62
  nowMs,
62
63
  uid
63
64
  ]);
64
- const { data, status, fromCache } = useFirestoreCollection(()=>{
65
- if (!scope) return null;
66
- if (plan.assigneeUid !== undefined && !plan.assigneeUid) return null;
67
- const constraints = [
68
- ...crmVisibleToClause(readTokens),
69
- where('status', '==', plan.status)
70
- ];
71
- if (plan.assigneeUid) {
72
- constraints.push(where('assigneeUid', '==', plan.assigneeUid));
73
- }
74
- if (plan.dueFrom !== undefined) {
75
- constraints.push(where('dueAtMs', '>=', plan.dueFrom));
76
- }
77
- if (plan.dueBefore !== undefined) {
78
- constraints.push(where('dueAtMs', '<', plan.dueBefore));
79
- }
80
- return query(collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.tasks), ...constraints, orderBy('dueAtMs', plan.direction), limit(CRM_TASK_VIEW_LIMIT));
81
- }, [
82
- firestore,
83
- scope,
84
- readTokens,
85
- plan.status,
86
- plan.assigneeUid,
87
- plan.dueFrom,
88
- plan.dueBefore,
65
+ const base = useMemo(()=>crmTaskViewBase(plan), [
66
+ plan
67
+ ]);
68
+ const sort = useMemo(()=>({
69
+ path: 'dueAtMs',
70
+ direction: plan.direction
71
+ }), [
89
72
  plan.direction
90
- ], {
91
- idField: '$id'
73
+ ]);
74
+ const foldsScope = useCrmFoldsScope(orgId, readTokens);
75
+ const paged = useCrmListQuery({
76
+ scope,
77
+ collection: CRM_COLLECTIONS.tasks,
78
+ visibleTo: readTokens,
79
+ foldsScope,
80
+ declaration: TASK_LIST_DECLARATION,
81
+ clauses,
82
+ search,
83
+ sort,
84
+ base,
85
+ enabled: plan.assigneeUid === undefined || Boolean(plan.assigneeUid)
92
86
  });
93
- const tasks = useMemo(()=>orderTaskRows(data != null ? data : []), [
94
- data
87
+ const tasks = useMemo(()=>orderTaskRows(paged.rows), [
88
+ paged.rows
95
89
  ]);
96
90
  return {
97
91
  tasks,
98
- status,
99
- fromCache,
100
- truncated: ((_ref = data == null ? void 0 : data.length) != null ? _ref : 0) >= CRM_TASK_VIEW_LIMIT,
92
+ status: paged.status,
93
+ fromCache: paged.fromCache,
94
+ truncated: paged.hasMore,
101
95
  scope,
102
96
  orgId,
103
- readTokens
97
+ readTokens,
98
+ plan: paged.plan,
99
+ hasMore: paged.hasMore,
100
+ page: paged.page,
101
+ setPage: paged.setPage,
102
+ pageSize: paged.pageSize,
103
+ setPageSize: paged.setPageSize
104
104
  };
105
105
  }
106
+ const NO_CLAUSES = [];
107
+ const NO_WORDS = [];
106
108
  /**
107
109
  * How many of one record's tasks a card reads. A person with more than fifty
108
110
  * open tasks about one contact has a different problem than a short list.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-crm-tasks.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 { CRM_COLLECTIONS, type CrmTask } from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n collection,\n limit,\n orderBy,\n query,\n type QueryConstraint,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n CRM_TASK_VIEW_LIMIT,\n type CrmTaskView,\n crmTaskViewPlan,\n orderTaskRows,\n} from '../model/task-views'\nimport { crmVisibleToClause, useCrmScope } from './use-crm-scope'\n\n/** A task as the console lists it: the document plus its id. */\nexport type CrmTaskRow = CrmTask & { $id: string }\n\n/**\n * The reader's clock, refreshed once a minute (AGL-2599).\n *\n * \"Overdue\" and \"today\" are computed at read time against this value, so a\n * tab left open across midnight repaints yesterday's work red without a\n * reload. A minute is coarse enough that the re-render is invisible and fine\n * enough that a task due at 9:00 does not read as upcoming at 9:05.\n */\nexport function useNowMs(): number {\n const [nowMs, setNowMs] = useState(() => Date.now())\n useEffect(() => {\n const timer = setInterval(() => setNowMs(Date.now()), 60_000)\n return () => clearInterval(timer)\n }, [])\n return nowMs\n}\n\nexport interface CrmTaskListResult {\n tasks: CrmTaskRow[]\n status: 'loading' | 'success' | 'error'\n /** The rows are unconfirmed by the server — what a guarded write reads. */\n fromCache: boolean\n /** The window filled up; there are tasks this view is not showing. */\n truncated: boolean\n scope: readonly [string, string] | null\n orgId: string | null | undefined\n /** The reader's tokens, or `null` at the organization level — no clause (AGL-2630). */\n readTokens: readonly string[] | null\n}\n\n/**\n * One view of the tasks section as a live, bounded listener.\n *\n * The plan comes from `crmTaskViewPlan`, so the hook holds no opinion about\n * what \"today\" means; it translates a plan into constraints in the order the\n * `crmTasks` indexes are declared and caps the window. \"My tasks\" opens no\n * listener at all until the reader's uid is known — a query for\n * `assigneeUid == ''` would be a real read that could only answer nothing.\n */\nexport function useCrmTaskList(options: {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Record<string, unknown> | null\n view: CrmTaskView\n uid: string | null | undefined\n nowMs: number\n}): CrmTaskListResult {\n const { hostId, org, view, uid, nowMs } = options\n const firestore = useFirestore()\n // The org root and the reader's tokens from the one scope hook (AGL-2614)\n // — the same expression the contacts list runs, so a task listed beside a\n // contact is scoped by exactly the predicate that admitted the contact.\n const { scope, orgId, visibleTo: readTokens } = useCrmScope({ hostId, org })\n const plan = useMemo(\n () => crmTaskViewPlan(view, { nowMs, uid }),\n [view, nowMs, uid],\n )\n const { data, status, fromCache } = useFirestoreCollection<CrmTaskRow>(\n () => {\n if (!scope) return null\n if (plan.assigneeUid !== undefined && !plan.assigneeUid) return null\n const constraints: QueryConstraint[] = [\n ...crmVisibleToClause(readTokens),\n where('status', '==', plan.status),\n ]\n if (plan.assigneeUid) {\n constraints.push(where('assigneeUid', '==', plan.assigneeUid))\n }\n if (plan.dueFrom !== undefined) {\n constraints.push(where('dueAtMs', '>=', plan.dueFrom))\n }\n if (plan.dueBefore !== undefined) {\n constraints.push(where('dueAtMs', '<', plan.dueBefore))\n }\n return query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.tasks),\n ...constraints,\n orderBy('dueAtMs', plan.direction),\n limit(CRM_TASK_VIEW_LIMIT),\n )\n },\n [\n firestore,\n scope,\n readTokens,\n plan.status,\n plan.assigneeUid,\n plan.dueFrom,\n plan.dueBefore,\n plan.direction,\n ],\n { idField: '$id' },\n )\n const tasks = useMemo(() => orderTaskRows(data ?? []), [data])\n return {\n tasks,\n status,\n fromCache,\n truncated: (data?.length ?? 0) >= CRM_TASK_VIEW_LIMIT,\n scope,\n orgId,\n readTokens,\n }\n}\n\n/**\n * How many of one record's tasks a card reads. A person with more than fifty\n * open tasks about one contact has a different problem than a short list.\n */\nexport const CRM_RECORD_TASK_LIMIT = 50\n\n/** Which record a task list is FOR — exactly one of the three. */\nexport interface CrmRecordRef {\n contactId?: string\n companyId?: string\n dealId?: string\n}\n\n/**\n * The tasks that hang off one contact, company or deal, open and done\n * together, soonest due first.\n *\n * Rides the per-record indexes — `(visibleTo, contactId, dueAtMs)` and its\n * two siblings — with no status clause, because the card shows the open ones\n * and counts the rest, and two listeners for one short list is one too many.\n */\nexport function useCrmRecordTasks(options: {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Record<string, unknown> | null\n record: CrmRecordRef\n}): CrmTaskListResult {\n const { hostId, org, record } = options\n const firestore = useFirestore()\n const { scope, orgId, visibleTo: readTokens } = useCrmScope({ hostId, org })\n const field = record.contactId\n ? 'contactId'\n : record.companyId\n ? 'companyId'\n : record.dealId\n ? 'dealId'\n : null\n const id = field ? record[field] : undefined\n const { data, status, fromCache } = useFirestoreCollection<CrmTaskRow>(\n () => {\n if (!scope || !field || !id) return null\n return query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.tasks),\n ...crmVisibleToClause(readTokens),\n where(field, '==', id),\n orderBy('dueAtMs', 'asc'),\n limit(CRM_RECORD_TASK_LIMIT),\n )\n },\n [firestore, scope, readTokens, field, id],\n { idField: '$id' },\n )\n const tasks = useMemo(() => orderTaskRows(data ?? []), [data])\n return {\n tasks,\n status,\n fromCache,\n truncated: (data?.length ?? 0) >= CRM_RECORD_TASK_LIMIT,\n scope,\n orgId,\n readTokens,\n }\n}\n"],"names":["CRM_COLLECTIONS","useFirestore","useFirestoreCollection","collection","limit","orderBy","query","where","useEffect","useMemo","useState","CRM_TASK_VIEW_LIMIT","crmTaskViewPlan","orderTaskRows","crmVisibleToClause","useCrmScope","useNowMs","nowMs","setNowMs","Date","now","timer","setInterval","clearInterval","useCrmTaskList","options","hostId","org","view","uid","firestore","scope","orgId","visibleTo","readTokens","plan","data","status","fromCache","assigneeUid","undefined","constraints","push","dueFrom","dueBefore","tasks","direction","idField","truncated","length","CRM_RECORD_TASK_LIMIT","useCrmRecordTasks","record","field","contactId","companyId","dealId","id"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/hooks/use-crm-tasks.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 { CRM_COLLECTIONS, type CrmTask } from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n collection,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useEffect, useMemo, useState } from 'react'\nimport type { ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type { ListQueryPlan } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n type CrmTaskView,\n crmTaskViewBase,\n crmTaskViewPlan,\n orderTaskRows,\n TASK_LIST_DECLARATION,\n} from '../model/task-views'\nimport { useCrmFoldsScope, useCrmListQuery } from './use-crm-list-query'\nimport { crmVisibleToClause, useCrmScope } from './use-crm-scope'\n\n/** A task as the console lists it: the document plus its id. */\nexport type CrmTaskRow = CrmTask & { $id: string }\n\n/**\n * The reader's clock, refreshed once a minute (AGL-2599).\n *\n * \"Overdue\" and \"today\" are computed at read time against this value, so a\n * tab left open across midnight repaints yesterday's work red without a\n * reload. A minute is coarse enough that the re-render is invisible and fine\n * enough that a task due at 9:00 does not read as upcoming at 9:05.\n */\nexport function useNowMs(): number {\n const [nowMs, setNowMs] = useState(() => Date.now())\n useEffect(() => {\n const timer = setInterval(() => setNowMs(Date.now()), 60_000)\n return () => clearInterval(timer)\n }, [])\n return nowMs\n}\n\nexport interface CrmTaskListResult {\n tasks: CrmTaskRow[]\n status: 'loading' | 'success' | 'error'\n /** The rows are unconfirmed by the server — what a guarded write reads. */\n fromCache: boolean\n /** The window filled up; there are tasks this view is not showing. */\n truncated: boolean\n scope: readonly [string, string] | null\n orgId: string | null | undefined\n /** The reader's tokens, or `null` at the organization level — no clause (AGL-2630). */\n readTokens: readonly string[] | null\n}\n\n/** One page of the tasks list, and what its query held. */\nexport interface CrmTaskPageResult extends CrmTaskListResult {\n /** What the query holds, what it refused and why (AGL-3321). */\n plan: ListQueryPlan\n hasMore: boolean\n page: number\n setPage: (page: number) => void\n pageSize: number\n setPageSize: (pageSize: number) => void\n}\n\n/**\n * One view of the tasks section, PAGED BY ITS QUERY (AGL-3321).\n *\n * The plan comes from `crmTaskViewPlan`, so the hook holds no opinion about\n * what \"today\" means; the view's predicates are the query's base beside the\n * scope clause, and Kind, Priority, Assignee and the search word are planned\n * onto the same query — every page a page of matches, none narrowed in\n * memory. \"My tasks\" opens no listener until the reader's uid is known — a\n * query for `assigneeUid == ''` would be a real read that could only answer\n * nothing.\n */\nexport function useCrmTaskList(options: {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Record<string, unknown> | null\n view: CrmTaskView\n uid: string | null | undefined\n nowMs: number\n /** The Filters panel's clauses, the view's own excluded. */\n clauses?: readonly ListFilterRequest[]\n search?: readonly string[]\n}): CrmTaskPageResult {\n const { hostId, org, view, uid, nowMs, clauses = NO_CLAUSES, search = NO_WORDS } = options\n // The org root and the reader's tokens from the one scope hook (AGL-2614)\n // — the same expression the contacts list runs, so a task listed beside a\n // contact is scoped by exactly the predicate that admitted the contact.\n const { scope, orgId, visibleTo: readTokens } = useCrmScope({ hostId, org })\n const plan = useMemo(\n () => crmTaskViewPlan(view, { nowMs, uid }),\n [view, nowMs, uid],\n )\n const base = useMemo(() => crmTaskViewBase(plan), [plan])\n const sort = useMemo(\n () => ({ path: 'dueAtMs', direction: plan.direction }),\n [plan.direction],\n )\n const foldsScope = useCrmFoldsScope(orgId, readTokens)\n const paged = useCrmListQuery<CrmTaskRow>({\n scope,\n collection: CRM_COLLECTIONS.tasks,\n visibleTo: readTokens,\n foldsScope,\n declaration: TASK_LIST_DECLARATION,\n clauses,\n search,\n sort,\n base,\n enabled: plan.assigneeUid === undefined || Boolean(plan.assigneeUid),\n })\n const tasks = useMemo(() => orderTaskRows(paged.rows), [paged.rows])\n return {\n tasks,\n status: paged.status,\n fromCache: paged.fromCache,\n truncated: paged.hasMore,\n scope,\n orgId,\n readTokens,\n plan: paged.plan,\n hasMore: paged.hasMore,\n page: paged.page,\n setPage: paged.setPage,\n pageSize: paged.pageSize,\n setPageSize: paged.setPageSize,\n }\n}\n\nconst NO_CLAUSES: readonly ListFilterRequest[] = []\nconst NO_WORDS: readonly string[] = []\n\n/**\n * How many of one record's tasks a card reads. A person with more than fifty\n * open tasks about one contact has a different problem than a short list.\n */\nexport const CRM_RECORD_TASK_LIMIT = 50\n\n/** Which record a task list is FOR — exactly one of the three. */\nexport interface CrmRecordRef {\n contactId?: string\n companyId?: string\n dealId?: string\n}\n\n/**\n * The tasks that hang off one contact, company or deal, open and done\n * together, soonest due first.\n *\n * Rides the per-record indexes — `(visibleTo, contactId, dueAtMs)` and its\n * two siblings — with no status clause, because the card shows the open ones\n * and counts the rest, and two listeners for one short list is one too many.\n */\nexport function useCrmRecordTasks(options: {\n /** The site the record is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Record<string, unknown> | null\n record: CrmRecordRef\n}): CrmTaskListResult {\n const { hostId, org, record } = options\n const firestore = useFirestore()\n const { scope, orgId, visibleTo: readTokens } = useCrmScope({ hostId, org })\n const field = record.contactId\n ? 'contactId'\n : record.companyId\n ? 'companyId'\n : record.dealId\n ? 'dealId'\n : null\n const id = field ? record[field] : undefined\n const { data, status, fromCache } = useFirestoreCollection<CrmTaskRow>(\n () => {\n if (!scope || !field || !id) return null\n return query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.tasks),\n ...crmVisibleToClause(readTokens),\n where(field, '==', id),\n orderBy('dueAtMs', 'asc'),\n limit(CRM_RECORD_TASK_LIMIT),\n )\n },\n [firestore, scope, readTokens, field, id],\n { idField: '$id' },\n )\n const tasks = useMemo(() => orderTaskRows(data ?? []), [data])\n return {\n tasks,\n status,\n fromCache,\n truncated: (data?.length ?? 0) >= CRM_RECORD_TASK_LIMIT,\n scope,\n orgId,\n readTokens,\n }\n}\n"],"names":["CRM_COLLECTIONS","useFirestore","useFirestoreCollection","collection","limit","orderBy","query","where","useEffect","useMemo","useState","crmTaskViewBase","crmTaskViewPlan","orderTaskRows","TASK_LIST_DECLARATION","useCrmFoldsScope","useCrmListQuery","crmVisibleToClause","useCrmScope","useNowMs","nowMs","setNowMs","Date","now","timer","setInterval","clearInterval","useCrmTaskList","options","hostId","org","view","uid","clauses","NO_CLAUSES","search","NO_WORDS","scope","orgId","visibleTo","readTokens","plan","base","sort","path","direction","foldsScope","paged","tasks","declaration","enabled","assigneeUid","undefined","Boolean","rows","status","fromCache","truncated","hasMore","page","setPage","pageSize","setPageSize","CRM_RECORD_TASK_LIMIT","useCrmRecordTasks","record","firestore","field","contactId","companyId","dealId","id","data","idField","length"],"mappings":"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"}
@@ -2,7 +2,8 @@ import { type CrmDealStatus } from '@aglyn/aglyn';
2
2
  import type { DealDoc } from '../model/deal-board-model';
3
3
  /**
4
4
  * The most open cards one pipeline draws. A board past this is a board
5
- * nobody reads column by column; the table is paged and reaches the rest.
5
+ * nobody reads column by column; the table is paged by its query
6
+ * (`DEAL_LIST_DECLARATION`) and reaches the rest.
6
7
  */
7
8
  export declare const BOARD_OPEN_LIMIT = 500;
8
9
  /** Closed deals shown per closed column when it is expanded. */
@@ -22,39 +23,6 @@ export declare const LINKED_DEALS_LIMIT = 25;
22
23
  * updatedAt DESC)` twin of that index.
23
24
  */
24
25
  export declare function useDealsByStatus(orgId: string | null, readTokens: readonly string[] | null, pipelineId: string | null, status: CrmDealStatus, max: number): import("@aglyn/tenant-feature-instance").UseFirestoreCollectionResult<DealDoc>;
25
- /**
26
- * Every deal the viewer may see, paged, optionally narrowed to one status
27
- * and to one pipeline — the table's read. Four shapes, four indexes:
28
- * `(visibleTo, updatedAt DESC)`, `(visibleTo, status, updatedAt DESC)`,
29
- * `(visibleTo, pipelineId, updatedAt DESC)` and
30
- * `(visibleTo, pipelineId, status, updatedAt DESC)`. At the organization
31
- * level (AGL-2630) the scope clause is dropped and the same shapes ride
32
- * their twins without `visibleTo` — `(status, updatedAt DESC)`,
33
- * `(pipelineId, updatedAt DESC)`, `(pipelineId, status, updatedAt DESC)`;
34
- * the first shape orders on one field and needs none.
35
- */
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>;
58
26
  /**
59
27
  * The deals that name one contact or one company, for the card on that
60
28
  * record's page. `(visibleTo, contactId|companyId, updatedAt DESC)`.
@@ -15,12 +15,13 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { CRM_COLLECTIONS } from "@aglyn/aglyn";
18
- import { useFirestore, useFirestoreCollection, useFirestoreDoc, usePagedCollection } from "@aglyn/tenant-feature-instance";
18
+ import { useFirestore, useFirestoreCollection, useFirestoreDoc } from "@aglyn/tenant-feature-instance";
19
19
  import { collection, doc, limit, orderBy, query, where } from "firebase/firestore";
20
20
  import { crmScopeListable, crmVisibleToClause } from "./use-crm-scope.js";
21
21
  /**
22
22
  * The most open cards one pipeline draws. A board past this is a board
23
- * nobody reads column by column; the table is paged and reaches the rest.
23
+ * nobody reads column by column; the table is paged by its query
24
+ * (`DEAL_LIST_DECLARATION`) and reaches the rest.
24
25
  */ export const BOARD_OPEN_LIMIT = 500;
25
26
  /** Closed deals shown per closed column when it is expanded. */ export const BOARD_CLOSED_LIMIT = 50;
26
27
  /** Deals listed on a contact's or a company's page. */ export const LINKED_DEALS_LIMIT = 25;
@@ -48,66 +49,6 @@ import { crmScopeListable, crmVisibleToClause } from "./use-crm-scope.js";
48
49
  idField: '$id'
49
50
  });
50
51
  }
51
- /**
52
- * Every deal the viewer may see, paged, optionally narrowed to one status
53
- * and to one pipeline — the table's read. Four shapes, four indexes:
54
- * `(visibleTo, updatedAt DESC)`, `(visibleTo, status, updatedAt DESC)`,
55
- * `(visibleTo, pipelineId, updatedAt DESC)` and
56
- * `(visibleTo, pipelineId, status, updatedAt DESC)`. At the organization
57
- * level (AGL-2630) the scope clause is dropped and the same shapes ride
58
- * their twins without `visibleTo` — `(status, updatedAt DESC)`,
59
- * `(pipelineId, updatedAt DESC)`, `(pipelineId, status, updatedAt DESC)`;
60
- * the first shape orders on one field and needs none.
61
- */ export function usePagedDeals(orgId, readTokens, status, pipelineId = null) {
62
- const firestore = useFirestore();
63
- return usePagedCollection((pageLimit)=>orgId && crmScopeListable(readTokens) ? query(collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals), ...crmVisibleToClause(readTokens), ...pipelineId ? [
64
- where('pipelineId', '==', pipelineId)
65
- ] : [], ...status === 'all' ? [] : [
66
- where('status', '==', status)
67
- ], orderBy('updatedAt', 'desc'), limit(pageLimit)) : null, [
68
- firestore,
69
- orgId,
70
- readTokens,
71
- status,
72
- pipelineId
73
- ], {
74
- idField: '$id'
75
- });
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
- }
111
52
  /**
112
53
  * The deals that name one contact or one company, for the card on that
113
54
  * 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 * 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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"}
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} 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 by its query\n * (`DEAL_LIST_DECLARATION`) 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 * 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","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","useLinkedDeals","link","field","id","contactId","companyId","useDeal","dealId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;AAEA,SAASA,eAAe,QAA4B,eAAc;AAClE,SACEC,YAAY,EACZC,sBAAsB,EACtBC,eAAe,QACV,iCAAgC;AACvC,SACEC,UAAU,EACVC,GAAG,EACHC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,KAAK,QACA,qBAAoB;AAE3B,SAASC,gBAAgB,EAAEC,kBAAkB,QAAQ,qBAAiB;AAEtE;;;;CAIC,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,YAAYpB;IAClB,OAAOC,uBACL,IACEc,SAASE,cAAcR,iBAAiBO,cACpCT,MACEJ,WAAWiB,WAAW,QAAQL,OAAOhB,gBAAgBsB,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;;;CAGC,GACD,OAAO,SAASC,eACdR,KAAoB,EACpBC,UAAoC,EACpCQ,IAAmD;IAEnD,MAAMJ,YAAYpB;IAClB,MAAMyB,QAAQ,eAAeD,OAAO,cAAc;IAClD,MAAME,KAAK,eAAeF,OAAOA,KAAKG,SAAS,GAAGH,KAAKI,SAAS;IAChE,OAAO3B,uBACL,IACEc,SAASW,MAAMjB,iBAAiBO,cAC5BT,MACEJ,WAAWiB,WAAW,QAAQL,OAAOhB,gBAAgBsB,KAAK,MACvDX,mBAAmBM,aACtBR,MAAMiB,OAAO,MAAMC,KACnBpB,QAAQ,aAAa,SACrBD,MAAMQ,uBAER,MACN;QAACO;QAAWL;QAAOC;QAAYS;QAAOC;KAAG,EACzC;QAAEJ,SAAS;IAAM;AAErB;AAEA,mEAAmE,GACnE,OAAO,SAASO,QAAQd,KAAoB,EAAEe,MAAc;IAC1D,MAAMV,YAAYpB;IAClB,OAAOE,gBACL,IACEa,SAASe,SACL1B,IAAIgB,WAAW,QAAQL,OAAOhB,gBAAgBsB,KAAK,EAAES,UACrD,MACN;QAACV;QAAWL;QAAOe;KAAO,EAC1B;QAAER,SAAS;IAAM;AAErB"}
@@ -0,0 +1,91 @@
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 { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter';
19
+ import type { ListFilterOption } from '@aglyn/shared-ui-jsx/const/list-grid-filter';
20
+ import type { ListQueryDeclaration, ListQueryFilter, ListQueryPlan } from '@aglyn/shared-ui-jsx/const/list-query-plan';
21
+ /** Every CRM list's quick search: the tokens every writer stamps. */
22
+ export declare const CRM_LIST_SEARCH: NonNullable<ListQueryDeclaration['search']>;
23
+ /**
24
+ * The scope clause as the plan's base: `visibleTo array-contains-any` over
25
+ * the reader's tokens under a site, nothing at the organization level
26
+ * (`null` tokens), where an org-wide member's listeners carry none.
27
+ */
28
+ export declare function crmListBase(visibleTo: readonly string[] | null): ListQueryFilter[];
29
+ /** The base's shape, for `listQueryIndexes`: the scope clause is an array clause. */
30
+ export declare const CRM_LIST_BASE_INDEX: readonly [{
31
+ readonly path: "visibleTo";
32
+ readonly array: true;
33
+ }];
34
+ /**
35
+ * The declaration a reader may run.
36
+ *
37
+ * Under a site the search folds into the scope clause (`search.scoped`),
38
+ * and a query without `visibleTo` is one the rules can prove only for an
39
+ * ORG-WIDE member — `canReadScoped()` short-circuits on it, and for anybody
40
+ * else inspects `visibleTo`, which the folded query no longer constrains.
41
+ * A member whose reach is some sites keeps the scope clause, and the plan
42
+ * refuses their search by name rather than sending a query the rules deny.
43
+ */
44
+ export declare function crmListDeclarationFor(declaration: ListQueryDeclaration, foldsScope: boolean): ListQueryDeclaration;
45
+ /** How one stored clause is asked: a query clause, or why it cannot be. */
46
+ export type CrmClauseAsked = ListFilterRequest | {
47
+ refused: string;
48
+ };
49
+ export interface CrmAskedClauses {
50
+ /** What the plan is asked, in the order the reader set them. */
51
+ clauses: ListFilterRequest[];
52
+ /** The stored clause each asked one stands for, index for index. */
53
+ origins: ListFilterRequest[];
54
+ /** Stored clauses refused before the plan saw them, with why. */
55
+ refused: Array<{
56
+ clause: ListFilterRequest;
57
+ reason: string;
58
+ }>;
59
+ }
60
+ /** Every stored clause through `ask`, keeping where each came from. */
61
+ export declare function crmAskClauses(stored: readonly ListFilterRequest[], ask: (clause: ListFilterRequest) => CrmClauseAsked): CrmAskedClauses;
62
+ /**
63
+ * The plan's refusals and the translation's, each named by the clause the
64
+ * reader SET, as `ListQueryNotices` takes them — through the platform's one
65
+ * wording (`listQueryRefusals`), so a notice and its chip read alike.
66
+ */
67
+ export declare function crmQueryRefusals(plan: Pick<ListQueryPlan, 'refused'>, asked: CrmAskedClauses, context: {
68
+ fields: readonly ListFilterField[];
69
+ headers?: Readonly<Record<string, string>>;
70
+ options?: Readonly<Record<string, readonly ListFilterOption[]>>;
71
+ }): Array<{
72
+ label: string;
73
+ reason: string;
74
+ }>;
75
+ /** The comma-separated values of an `isAnyOf`, or the one value of anything else. */
76
+ export declare function crmClauseValues(clause: Pick<ListFilterRequest, 'op' | 'value'>): string[];
77
+ /** One value as `equals`, several as `isAnyOf` — the shape the plan takes a list in. */
78
+ export declare function crmAnyOf(field: string, values: readonly string[]): CrmClauseAsked;
79
+ /** A `select` field: equality and "any of" over stored values, nothing else. */
80
+ export declare function crmSelectField(column: string, path?: string): ListFilterField;
81
+ /**
82
+ * The list fields a CLIENT edit writes beside its patch (AGL-3321): the
83
+ * record as the listener holds it with the patch laid over — a `null` or a
84
+ * sentinel in the patch reads as the field removed — through
85
+ * `crmListFields`, narrowed to `fields`. A client never writes a field the
86
+ * rules keep for the server (a lead's `emailStatus`), which is why the
87
+ * caller names the ones it may.
88
+ */
89
+ export declare function crmClientListFields(collection: CrmListCollection, record: Readonly<Record<string, unknown>>, patch: Readonly<Record<string, unknown>>, fields: readonly string[]): Record<string, unknown>;
90
+ /** The search fields every CRM record carries — what a client edit of searched text rewrites. */
91
+ export declare const CRM_CLIENT_SEARCH_FIELDS: readonly ["searchTokens", "scopedSearchTokens"];
@@ -0,0 +1,182 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { CRM_SCOPED_SEARCH_TOKENS_FIELD, CRM_SEARCH_TOKENS_FIELD, crmListFields } from "@aglyn/aglyn";
18
+ import { scopedSearch } from "@aglyn/aglyn/app-utils/name-search";
19
+ import { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
20
+ /*
21
+ * THE CRM LISTS ON THEIR QUERIES (AGL-3321).
22
+ *
23
+ * Every CRM list — Leads, Contacts, Companies, Deals, Tasks — declares its
24
+ * filterable fields, its one order and its search as a
25
+ * `ListQueryDeclaration`, and `planListQuery` puts every clause and the
26
+ * search word on ONE Firestore query paged by `useListQuery`. What a query
27
+ * cannot hold is refused by name (`ListQueryNotices`), never matched over
28
+ * the rows a page happened to load.
29
+ *
30
+ * This module is what the five share: the search every CRM record carries
31
+ * (`searchTokens`, and `scopedSearchTokens` under a site — see
32
+ * `crmListFields` in `@aglyn/aglyn`), the scope clause as the plan's base,
33
+ * a stored clause translated to the one its query asks, and the refusals
34
+ * read back through the clause the reader set.
35
+ */ /** Every CRM list's quick search: the tokens every writer stamps. */ export const CRM_LIST_SEARCH = {
36
+ tokensPath: CRM_SEARCH_TOKENS_FIELD,
37
+ scoped: scopedSearch(CRM_SCOPED_SEARCH_TOKENS_FIELD)
38
+ };
39
+ /**
40
+ * The scope clause as the plan's base: `visibleTo array-contains-any` over
41
+ * the reader's tokens under a site, nothing at the organization level
42
+ * (`null` tokens), where an org-wide member's listeners carry none.
43
+ */ export function crmListBase(visibleTo) {
44
+ return visibleTo ? [
45
+ {
46
+ path: 'visibleTo',
47
+ op: 'array-contains-any',
48
+ value: [
49
+ ...visibleTo
50
+ ]
51
+ }
52
+ ] : [];
53
+ }
54
+ /** The base's shape, for `listQueryIndexes`: the scope clause is an array clause. */ export const CRM_LIST_BASE_INDEX = [
55
+ {
56
+ path: 'visibleTo',
57
+ array: true
58
+ }
59
+ ];
60
+ /**
61
+ * The declaration a reader may run.
62
+ *
63
+ * Under a site the search folds into the scope clause (`search.scoped`),
64
+ * and a query without `visibleTo` is one the rules can prove only for an
65
+ * ORG-WIDE member — `canReadScoped()` short-circuits on it, and for anybody
66
+ * else inspects `visibleTo`, which the folded query no longer constrains.
67
+ * A member whose reach is some sites keeps the scope clause, and the plan
68
+ * refuses their search by name rather than sending a query the rules deny.
69
+ */ export function crmListDeclarationFor(declaration, foldsScope) {
70
+ var _declaration_search;
71
+ if (foldsScope || !((_declaration_search = declaration.search) == null ? void 0 : _declaration_search.scoped)) return declaration;
72
+ return _extends({}, declaration, {
73
+ search: {
74
+ tokensPath: declaration.search.tokensPath
75
+ }
76
+ });
77
+ }
78
+ /** Every stored clause through `ask`, keeping where each came from. */ export function crmAskClauses(stored, ask) {
79
+ const clauses = [];
80
+ const origins = [];
81
+ const refused = [];
82
+ for (const clause of stored){
83
+ const asked = ask(clause);
84
+ if ('refused' in asked) {
85
+ refused.push({
86
+ clause,
87
+ reason: asked.refused
88
+ });
89
+ } else {
90
+ clauses.push(asked);
91
+ origins.push(clause);
92
+ }
93
+ }
94
+ return {
95
+ clauses,
96
+ origins,
97
+ refused
98
+ };
99
+ }
100
+ const sameClause = (a, b)=>a.field === b.field && a.op === b.op && a.value === b.value;
101
+ /**
102
+ * The plan's refusals and the translation's, each named by the clause the
103
+ * reader SET, as `ListQueryNotices` takes them — through the platform's one
104
+ * wording (`listQueryRefusals`), so a notice and its chip read alike.
105
+ */ export function crmQueryRefusals(plan, asked, context) {
106
+ const planned = plan.refused.map((entry)=>{
107
+ if (entry.clause === 'search') return entry;
108
+ const clause = entry.clause;
109
+ const at = asked.clauses.findIndex((candidate)=>sameClause(candidate, clause));
110
+ return {
111
+ clause: at >= 0 ? asked.origins[at] : clause,
112
+ reason: entry.reason
113
+ };
114
+ });
115
+ return listQueryRefusals([
116
+ ...asked.refused,
117
+ ...planned
118
+ ], context);
119
+ }
120
+ /** The comma-separated values of an `isAnyOf`, or the one value of anything else. */ export function crmClauseValues(clause) {
121
+ const raw = clause.op === 'isAnyOf' ? clause.value.split(',') : [
122
+ clause.value
123
+ ];
124
+ return [
125
+ ...new Set(raw.map((value)=>value.trim()).filter(Boolean))
126
+ ];
127
+ }
128
+ /** One value as `equals`, several as `isAnyOf` — the shape the plan takes a list in. */ export function crmAnyOf(field, values) {
129
+ const unique = [
130
+ ...new Set(values)
131
+ ];
132
+ if (!unique.length) return {
133
+ refused: 'no value yet'
134
+ };
135
+ return unique.length === 1 ? {
136
+ field,
137
+ op: 'equals',
138
+ value: unique[0]
139
+ } : {
140
+ field,
141
+ op: 'isAnyOf',
142
+ value: unique.join(',')
143
+ };
144
+ }
145
+ /** A `select` field: equality and "any of" over stored values, nothing else. */ export function crmSelectField(column, path = column) {
146
+ return {
147
+ column,
148
+ kind: 'exact',
149
+ path,
150
+ operators: [
151
+ 'equals',
152
+ 'isAnyOf'
153
+ ]
154
+ };
155
+ }
156
+ /**
157
+ * The list fields a CLIENT edit writes beside its patch (AGL-3321): the
158
+ * record as the listener holds it with the patch laid over — a `null` or a
159
+ * sentinel in the patch reads as the field removed — through
160
+ * `crmListFields`, narrowed to `fields`. A client never writes a field the
161
+ * rules keep for the server (a lead's `emailStatus`), which is why the
162
+ * caller names the ones it may.
163
+ */ export function crmClientListFields(collection, record, patch, fields) {
164
+ const merged = _extends({}, record);
165
+ for (const [key, value] of Object.entries(patch)){
166
+ // A dotted path is a map entry (`custom.tier`), never a searched field.
167
+ if (key.includes('.')) continue;
168
+ merged[key] = value === null || isSentinel(value) ? undefined : value;
169
+ }
170
+ const computed = crmListFields(collection, merged);
171
+ return Object.fromEntries(fields.filter((field)=>field in computed).map((field)=>[
172
+ field,
173
+ computed[field]
174
+ ]));
175
+ }
176
+ /** A web-SDK `FieldValue` — `deleteField()`, the one a client edit of these fields sends. */ const isSentinel = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value.isEqual === 'function';
177
+ /** The search fields every CRM record carries — what a client edit of searched text rewrites. */ export const CRM_CLIENT_SEARCH_FIELDS = [
178
+ 'searchTokens',
179
+ 'scopedSearchTokens'
180
+ ];
181
+
182
+ //# sourceMappingURL=crm-list-query.js.map