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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/activity-list.d.ts +36 -5
  3. package/src/lib/components/activity-list.js +159 -137
  4. package/src/lib/components/activity-list.js.map +1 -1
  5. package/src/lib/components/activity-timeline.d.ts +84 -0
  6. package/src/lib/components/activity-timeline.js +224 -0
  7. package/src/lib/components/activity-timeline.js.map +1 -0
  8. package/src/lib/components/companies-section.d.ts +6 -6
  9. package/src/lib/components/companies-section.js +54 -84
  10. package/src/lib/components/companies-section.js.map +1 -1
  11. package/src/lib/components/company-edit-drawer.js +17 -4
  12. package/src/lib/components/company-edit-drawer.js.map +1 -1
  13. package/src/lib/components/company-picker.js +6 -2
  14. package/src/lib/components/company-picker.js.map +1 -1
  15. package/src/lib/components/company-properties-card.js +53 -36
  16. package/src/lib/components/company-properties-card.js.map +1 -1
  17. package/src/lib/components/contact-associations-card.d.ts +3 -2
  18. package/src/lib/components/contact-associations-card.js +12 -12
  19. package/src/lib/components/contact-associations-card.js.map +1 -1
  20. package/src/lib/components/contact-detail-page.d.ts +4 -3
  21. package/src/lib/components/contact-detail-page.js +18 -12
  22. package/src/lib/components/contact-detail-page.js.map +1 -1
  23. package/src/lib/components/contact-list-columns.d.ts +3 -5
  24. package/src/lib/components/contact-list-columns.js +6 -8
  25. package/src/lib/components/contact-list-columns.js.map +1 -1
  26. package/src/lib/components/contact-properties-card.d.ts +21 -4
  27. package/src/lib/components/contact-properties-card.js +129 -29
  28. package/src/lib/components/contact-properties-card.js.map +1 -1
  29. package/src/lib/components/contact-timeline-card.d.ts +12 -6
  30. package/src/lib/components/contact-timeline-card.js +132 -174
  31. package/src/lib/components/contact-timeline-card.js.map +1 -1
  32. package/src/lib/components/contacts-bulk-bar.js +10 -1
  33. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  34. package/src/lib/components/contacts-section.js +282 -331
  35. package/src/lib/components/contacts-section.js.map +1 -1
  36. package/src/lib/components/crm-email-state-chip.js +4 -1
  37. package/src/lib/components/crm-email-state-chip.js.map +1 -1
  38. package/src/lib/components/crm-next-activity-column.d.ts +4 -2
  39. package/src/lib/components/crm-next-activity-column.js +6 -3
  40. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  41. package/src/lib/components/deal-edit-drawer.js +8 -4
  42. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  43. package/src/lib/components/deal-products-card.js +15 -13
  44. package/src/lib/components/deal-products-card.js.map +1 -1
  45. package/src/lib/components/deal-properties-card.js +7 -0
  46. package/src/lib/components/deal-properties-card.js.map +1 -1
  47. package/src/lib/components/deal-stage-card.js +31 -29
  48. package/src/lib/components/deal-stage-card.js.map +1 -1
  49. package/src/lib/components/deals-section.d.ts +2 -2
  50. package/src/lib/components/deals-section.js +67 -97
  51. package/src/lib/components/deals-section.js.map +1 -1
  52. package/src/lib/components/fields-section.d.ts +4 -0
  53. package/src/lib/components/fields-section.js +368 -154
  54. package/src/lib/components/fields-section.js.map +1 -1
  55. package/src/lib/components/form-contact-fields-card.js +24 -20
  56. package/src/lib/components/form-contact-fields-card.js.map +1 -1
  57. package/src/lib/components/lead-campaigns-card.d.ts +1 -1
  58. package/src/lib/components/lead-campaigns-card.js +43 -38
  59. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  60. package/src/lib/components/lead-properties-card.d.ts +14 -6
  61. package/src/lib/components/lead-properties-card.js +323 -311
  62. package/src/lib/components/lead-properties-card.js.map +1 -1
  63. package/src/lib/components/lead-surfaces-note.js +13 -2
  64. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  65. package/src/lib/components/leads-bulk-bar.js +16 -3
  66. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  67. package/src/lib/components/leads-section.d.ts +2 -1
  68. package/src/lib/components/leads-section.js +110 -148
  69. package/src/lib/components/leads-section.js.map +1 -1
  70. package/src/lib/components/linked-deals-card.js +12 -10
  71. package/src/lib/components/linked-deals-card.js.map +1 -1
  72. package/src/lib/components/recent-activity-feed.d.ts +12 -8
  73. package/src/lib/components/recent-activity-feed.js +54 -29
  74. package/src/lib/components/recent-activity-feed.js.map +1 -1
  75. package/src/lib/components/record-activity-card.d.ts +4 -2
  76. package/src/lib/components/record-activity-card.js +29 -11
  77. package/src/lib/components/record-activity-card.js.map +1 -1
  78. package/src/lib/components/reports/activity-card.js +18 -12
  79. package/src/lib/components/reports/activity-card.js.map +1 -1
  80. package/src/lib/components/reports/lead-funnel-card.js +47 -37
  81. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  82. package/src/lib/components/reports/pipeline-card.js +22 -16
  83. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  84. package/src/lib/components/reports/report-export.d.ts +4 -11
  85. package/src/lib/components/reports/report-export.js +16 -33
  86. package/src/lib/components/reports/report-export.js.map +1 -1
  87. package/src/lib/components/reports/source-conversion-card.js +18 -12
  88. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  89. package/src/lib/components/reports/tasks-card.js +35 -25
  90. package/src/lib/components/reports/tasks-card.js.map +1 -1
  91. package/src/lib/components/tasks-section.js +151 -144
  92. package/src/lib/components/tasks-section.js.map +1 -1
  93. package/src/lib/constants/company-filters.d.ts +13 -0
  94. package/src/lib/constants/company-filters.js +47 -17
  95. package/src/lib/constants/company-filters.js.map +1 -1
  96. package/src/lib/constants/contact-filters.d.ts +61 -10
  97. package/src/lib/constants/contact-filters.js +373 -141
  98. package/src/lib/constants/contact-filters.js.map +1 -1
  99. package/src/lib/constants/deal-filters.d.ts +44 -0
  100. package/src/lib/constants/deal-filters.js +78 -0
  101. package/src/lib/constants/deal-filters.js.map +1 -0
  102. package/src/lib/constants/field-list-query.d.ts +23 -0
  103. package/src/lib/constants/field-list-query.js +80 -0
  104. package/src/lib/constants/field-list-query.js.map +1 -0
  105. package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
  106. package/src/lib/hooks/use-crm-list-query.js +182 -0
  107. package/src/lib/hooks/use-crm-list-query.js.map +1 -0
  108. package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
  109. package/src/lib/hooks/use-crm-tasks.js +44 -42
  110. package/src/lib/hooks/use-crm-tasks.js.map +1 -1
  111. package/src/lib/hooks/use-deals.d.ts +2 -34
  112. package/src/lib/hooks/use-deals.js +3 -62
  113. package/src/lib/hooks/use-deals.js.map +1 -1
  114. package/src/lib/model/crm-list-query.d.ts +91 -0
  115. package/src/lib/model/crm-list-query.js +182 -0
  116. package/src/lib/model/crm-list-query.js.map +1 -0
  117. package/src/lib/model/lead-filters.d.ts +50 -44
  118. package/src/lib/model/lead-filters.js +194 -88
  119. package/src/lib/model/lead-filters.js.map +1 -1
  120. package/src/lib/model/list-fields-api.d.ts +26 -0
  121. package/src/lib/model/list-fields-api.js +47 -0
  122. package/src/lib/model/list-fields-api.js.map +1 -0
  123. package/src/lib/model/list-fields.d.ts +47 -0
  124. package/src/lib/model/list-fields.js +39 -0
  125. package/src/lib/model/list-fields.js.map +1 -0
  126. package/src/lib/model/task-views.d.ts +15 -10
  127. package/src/lib/model/task-views.js +68 -9
  128. package/src/lib/model/task-views.js.map +1 -1
  129. package/src/lib/server/capture-contact.js +7 -5
  130. package/src/lib/server/capture-contact.js.map +1 -1
  131. package/src/lib/server/companies-import.js +3 -3
  132. package/src/lib/server/companies-import.js.map +1 -1
  133. package/src/lib/server/company-delete.js +3 -1
  134. package/src/lib/server/company-delete.js.map +1 -1
  135. package/src/lib/server/consent-group-participant.js +4 -0
  136. package/src/lib/server/consent-group-participant.js.map +1 -1
  137. package/src/lib/server/contact-update.js +14 -8
  138. package/src/lib/server/contact-update.js.map +1 -1
  139. package/src/lib/server/contacts-import.js +4 -2
  140. package/src/lib/server/contacts-import.js.map +1 -1
  141. package/src/lib/server/deals-import.js +4 -2
  142. package/src/lib/server/deals-import.js.map +1 -1
  143. package/src/lib/server/erase-person.js +2 -1
  144. package/src/lib/server/erase-person.js.map +1 -1
  145. package/src/lib/server/lead-create.js +4 -1
  146. package/src/lib/server/lead-create.js.map +1 -1
  147. package/src/lib/server/lead-source-values.js +9 -5
  148. package/src/lib/server/lead-source-values.js.map +1 -1
  149. package/src/lib/server/leads-import.js +4 -1
  150. package/src/lib/server/leads-import.js.map +1 -1
  151. package/src/lib/server/list-fields-routes.d.ts +29 -0
  152. package/src/lib/server/list-fields-routes.js +85 -0
  153. package/src/lib/server/list-fields-routes.js.map +1 -0
  154. package/src/lib/server/record-email-state.js +5 -2
  155. package/src/lib/server/record-email-state.js.map +1 -1
  156. package/src/lib/server/record-timeline.js +3 -2
  157. package/src/lib/server/record-timeline.js.map +1 -1
  158. package/src/lib/server/task-routes.js +6 -3
  159. package/src/lib/server/task-routes.js.map +1 -1
  160. package/src/lib/server/tasks-import.js +5 -2
  161. package/src/lib/server/tasks-import.js.map +1 -1
  162. package/src/lib/server.js +10 -1
  163. package/src/lib/server.js.map +1 -1
  164. package/src/lib/components/contact-custom-fields-card.d.ts +0 -39
  165. package/src/lib/components/contact-custom-fields-card.js +0 -204
  166. package/src/lib/components/contact-custom-fields-card.js.map +0 -1
  167. package/src/lib/hooks/use-org-leads.d.ts +0 -50
  168. package/src/lib/hooks/use-org-leads.js +0 -104
  169. package/src/lib/hooks/use-org-leads.js.map +0 -1
@@ -14,8 +14,10 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { EMAIL_STATE_LABELS, EMAIL_STATE_STATUSES, CRM_LEAD_STATUS_LABELS, emailStateForbidsEmail, crmLeadStatus, isCrmLeadOpen, normalizeCrmPicklistLabel, readCampaignIds, readEmailState } from "@aglyn/aglyn";
18
- import { listSelectCodec, listRowMatchesSearch } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
17
+ */ import { EMAIL_STATE_LABELS, EMAIL_STATE_STATUSES, CRM_LEAD_STATUS_LABELS, CRM_LEAD_SCOPED_CAMPAIGNS_FIELD, CRM_LEAD_STATUSES, crmLeadSourceKey } from "@aglyn/aglyn";
18
+ import { SCOPED_SEARCH_JOIN } from "@aglyn/aglyn/app-utils/name-search";
19
+ import { listSelectCodec } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
20
+ import { CRM_LIST_SEARCH, crmAnyOf, crmClauseValues, crmSelectField } from "./crm-list-query.js";
19
21
  export const LEAD_FILTERS = [
20
22
  'open',
21
23
  'new',
@@ -32,11 +34,6 @@ export const LEAD_FILTER_LABELS = {
32
34
  qualified: CRM_LEAD_STATUS_LABELS.qualified,
33
35
  unqualified: CRM_LEAD_STATUS_LABELS.unqualified
34
36
  };
35
- /** Whether a lead belongs in a filter's view. An absent status is `new`. */ export function leadMatchesFilter(lead, filter) {
36
- if (filter === 'all') return true;
37
- if (filter === 'open') return isCrmLeadOpen(lead);
38
- return crmLeadStatus(lead) === filter;
39
- }
40
37
  export const LEAD_EMAIL_FILTERS = [
41
38
  'any',
42
39
  'problem',
@@ -48,66 +45,11 @@ export const LEAD_EMAIL_FILTER_LABELS = _extends({
48
45
  problem: 'Cannot be emailed',
49
46
  none: 'Nothing known'
50
47
  }, EMAIL_STATE_LABELS);
51
- /** Whether a lead belongs in an email filter's view. */ export function leadMatchesEmailFilter(lead, filter) {
52
- if (filter === 'any') return true;
53
- const state = readEmailState(lead);
54
- if (filter === 'none') return state === null;
55
- if (filter === 'problem') return emailStateForbidsEmail(state);
56
- return (state == null ? void 0 : state.status) === filter;
57
- }
58
- /**
59
- * The Leads section's `Campaign` control (AGL-3254): every lead, or the
60
- * ones filed under one campaign — read off the lead's own `campaignIds`,
61
- * the field every campaign member carries. `''` is every lead, which is
62
- * what the control opens on; the ids are the site's containers', and the
63
- * section resolves their names for the menu.
64
- */ export function leadMatchesCampaignFilter(lead, campaignId) {
65
- if (!campaignId) return true;
66
- return readCampaignIds(lead).includes(campaignId);
67
- }
68
- /**
69
- * The fields the Leads section's search box reads (AGL-3246): who the lead
70
- * is and where they work — the words a person types to find one — plus the
71
- * tags, which are how an import names its batch (`sal-15`). `name` and
72
- * `email` are the capture door's fields rather than the CRM's, so the row is
73
- * read as a record rather than through `CrmLeadFields`.
74
- */ export const LEAD_SEARCH_FIELDS = [
75
- 'name',
76
- 'email',
77
- 'company',
78
- 'jobTitle',
79
- 'tags'
80
- ];
81
- /**
82
- * Whether a lead answers a search term.
83
- *
84
- * Case-insensitive, and every whitespace-separated word of the term must
85
- * appear in SOME searched field: `morgan lamphere` and `lamphere sal-15`
86
- * both find Morgan Lamphere, `morgan smith` does not. A blank term matches
87
- * every lead, so the box emptied is the list unfiltered.
88
- */ export function leadMatchesSearch(lead, term) {
89
- // Tags are a list; a stray string there is no tag of the lead's.
90
- const tags = Array.isArray(lead['tags']) ? lead['tags'] : undefined;
91
- return listRowMatchesSearch(_extends({}, lead, {
92
- tags
93
- }), LEAD_SEARCH_FIELDS, term.trim().split(/\s+/));
94
- }
95
48
  /**
96
49
  * The `Lead source` control's "no lead source" choice (AGL-3298). Held in
97
50
  * the saved view as an `isEmpty` clause, never as this string; it is only
98
51
  * the select's own value for the choice.
99
52
  */ export const LEAD_SOURCE_FILTER_NONE = '\u0000none';
100
- /**
101
- * The Leads section's `Lead source` control (AGL-3298): every lead (`''`),
102
- * the ones holding one value — compared the way the picklist compares
103
- * labels, so a value typed in another case before the list existed is
104
- * still found — or the ones holding none.
105
- */ export function leadMatchesLeadSourceFilter(lead, filter) {
106
- if (!filter) return true;
107
- const held = normalizeCrmPicklistLabel(lead['leadSource']).toLowerCase();
108
- if (filter === LEAD_SOURCE_FILTER_NONE) return held === '';
109
- return held === normalizeCrmPicklistLabel(filter).toLowerCase();
110
- }
111
53
  /*==========================================
112
54
  * THE LEADS GRID'S FILTERS (AGL-3313).
113
55
  *
@@ -120,8 +62,8 @@ export const LEAD_EMAIL_FILTER_LABELS = _extends({
120
62
  * campaignIds `contains <campaign id>`
121
63
  * leadSource `equals <label>`, or `isEmpty` for the leads holding none
122
64
  *
123
- * Every one narrows the loaded window, never the query — see the section's
124
- * `LEADS_WINDOW` for why a status cannot be asked of Firestore.
65
+ * Every one is asked of the Leads query (AGL-3321) through the field a
66
+ * writer stores for it — see {@link leadQueryClause}.
125
67
  *=========================================*/ /** The fields the grid's panel offers on Leads. */ export const LEAD_LIST_FILTER_FIELDS = [
126
68
  {
127
69
  column: 'status',
@@ -224,30 +166,6 @@ const STATUS_ALL = {
224
166
  ...shown
225
167
  ];
226
168
  }
227
- const listed = (clause)=>clause.op === 'isAnyOf' ? clause.value.split(',').map((entry)=>entry.trim()).filter(Boolean) : [
228
- clause.value
229
- ];
230
- /** Whether a lead answers every clause the grid shows. */ export function leadMatchesClauses(lead, clauses) {
231
- return clauses.every((clause)=>{
232
- switch(clause.field){
233
- case 'status':
234
- return listed(clause).some((value)=>LEAD_FILTERS.includes(value) ? leadMatchesFilter(lead, value) : true);
235
- case 'emailState':
236
- return listed(clause).some((value)=>LEAD_EMAIL_FILTERS.includes(value) ? leadMatchesEmailFilter(lead, value) : true);
237
- case 'campaignIds':
238
- return listed(clause).some((value)=>leadMatchesCampaignFilter(lead, value));
239
- case 'leadSource':
240
- return clause.op === 'isEmpty' ? leadMatchesLeadSourceFilter(lead, LEAD_SOURCE_FILTER_NONE) : listed(clause).some((value)=>leadMatchesLeadSourceFilter(lead, value));
241
- case 'ownerUid':
242
- var _lead_ownerUid;
243
- return listed(clause).includes(String((_lead_ownerUid = lead['ownerUid']) != null ? _lead_ownerUid : ''));
244
- default:
245
- // A clause on a field this list does not answer narrows nothing,
246
- // rather than emptying the list.
247
- return true;
248
- }
249
- });
250
- }
251
169
  /** Campaign clauses were stored as `contains`; the panel's select says `is`. */ export const LEAD_CAMPAIGN_CODEC = {
252
170
  toItem: (clause)=>clause.op === 'contains' || clause.op === 'equals' ? {
253
171
  operator: 'is',
@@ -281,5 +199,193 @@ const listed = (clause)=>clause.op === 'isAnyOf' ? clause.value.split(',').map((
281
199
  campaignIds: LEAD_CAMPAIGN_CODEC,
282
200
  leadSource: LEAD_SOURCE_CODEC
283
201
  };
202
+ /*==========================================
203
+ * THE LEADS QUERY (AGL-3321).
204
+ *
205
+ * `orgs/{orgId}/leads`, newest seen first, under a site narrowed by the
206
+ * `visibleTo` scope clause. Each grid clause is asked through a field every
207
+ * lead writer stores (`crmLeadListFields` in `@aglyn/aglyn`):
208
+ *
209
+ * status `status`, stored `new` on a lead nobody has touched, so
210
+ * Open is `status in [new, working]`
211
+ * emailState `emailStatus`, the verdict's status or `none`; "Cannot be
212
+ * emailed" is every verdict but `ok`
213
+ * leadSource `leadSourceKey`, the label as the picklist compares it, or
214
+ * `null` for none
215
+ * ownerUid as stored
216
+ * campaignIds `array-contains` on the lead's own campaigns; under a site,
217
+ * `scopedCampaignIds` — the campaign behind each of the
218
+ * site's scope tokens — which stands in the scope clause's
219
+ * place, so the one array clause answers both
220
+ *
221
+ * and the search box reads `searchTokens` — word prefixes of the name,
222
+ * address, company, title and tags. Every equality rides one
223
+ * `(field, lastSeenAtMs DESC)` composite; the campaign and the search are
224
+ * array clauses, so the two do not stand together (nor under a site, where
225
+ * the scope clause is the query's one array clause, save the search, which
226
+ * folds into it for a reader who may drop it).
227
+ *=========================================*/ /** The one order the Leads list takes: newest seen first. */ export const LEAD_LIST_SORTS = [
228
+ {
229
+ path: 'lastSeenAtMs',
230
+ direction: 'desc'
231
+ }
232
+ ];
233
+ /** The fields the Leads query asks — each a stored field, as named. */ export const LEAD_QUERY_FIELDS = [
234
+ crmSelectField('status'),
235
+ crmSelectField('emailStatus'),
236
+ crmSelectField('ownerUid'),
237
+ {
238
+ column: 'leadSourceKey',
239
+ kind: 'exact',
240
+ path: 'leadSourceKey',
241
+ presence: 'nullable',
242
+ operators: [
243
+ 'equals',
244
+ 'isAnyOf',
245
+ 'isEmpty'
246
+ ]
247
+ },
248
+ {
249
+ column: 'campaignIds',
250
+ kind: 'exact',
251
+ path: 'campaignIds',
252
+ tokensPath: 'campaignIds',
253
+ operators: [
254
+ 'contains'
255
+ ]
256
+ },
257
+ {
258
+ // A campaign under a site: the lead's campaigns behind its scope.
259
+ column: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,
260
+ kind: 'exact',
261
+ path: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,
262
+ tokensPath: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,
263
+ operators: [
264
+ 'isAnyOf'
265
+ ]
266
+ }
267
+ ];
268
+ export const LEAD_LIST_DECLARATION = {
269
+ fields: LEAD_QUERY_FIELDS,
270
+ sorts: LEAD_LIST_SORTS,
271
+ search: CRM_LIST_SEARCH
272
+ };
273
+ /**
274
+ * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the
275
+ * start of the address, beside the scope clause — the `(visibleTo, email)`
276
+ * composite the Inbox's leads list reads too.
277
+ */ export const LEAD_PREFIX_SEARCH = {
278
+ field: {
279
+ column: 'email',
280
+ kind: 'text',
281
+ path: 'email',
282
+ lowerPath: 'email',
283
+ presence: 'always',
284
+ operators: [
285
+ 'startsWith'
286
+ ]
287
+ },
288
+ notice: 'Search matches the start of a lead’s address for access limited to specific sites.'
289
+ };
290
+ /** Whether an asked clause stands in for the scope clause: a campaign behind the site's scope. */ export const leadClauseImpliesScope = (clause)=>clause.field === CRM_LEAD_SCOPED_CAMPAIGNS_FIELD;
291
+ /** The verdicts a member must not email to: every one but `ok`. */ const EMAIL_PROBLEMS = EMAIL_STATE_STATUSES.filter((status)=>status !== 'ok');
292
+ const ORG_READER = {
293
+ scopeTokens: null,
294
+ foldsScope: true
295
+ };
296
+ /**
297
+ * One grid clause as the clause the Leads query asks — see the block above.
298
+ * A value the list does not know is refused by name rather than dropped, so
299
+ * a stored view that names a retired choice says so.
300
+ */ export function leadQueryClause(clause, reader = ORG_READER) {
301
+ const values = crmClauseValues(clause);
302
+ switch(clause.field){
303
+ case 'status':
304
+ {
305
+ const statuses = [];
306
+ for (const value of values){
307
+ if (value === 'open') statuses.push('new', 'working');
308
+ else if (CRM_LEAD_STATUSES.includes(value)) statuses.push(value);
309
+ else return {
310
+ refused: `${value} is not a lead status`
311
+ };
312
+ }
313
+ return crmAnyOf('status', statuses);
314
+ }
315
+ case 'emailState':
316
+ {
317
+ const keys = [];
318
+ for (const value of values){
319
+ if (value === 'problem') keys.push(...EMAIL_PROBLEMS);
320
+ else if (value === 'none') keys.push('none');
321
+ else if (EMAIL_STATE_STATUSES.includes(value)) keys.push(value);
322
+ else return {
323
+ refused: `${value} is not an email verdict`
324
+ };
325
+ }
326
+ return crmAnyOf('emailStatus', keys);
327
+ }
328
+ case 'leadSource':
329
+ {
330
+ if (clause.op === 'isEmpty' || values.length === 1 && values[0] === LEAD_SOURCE_FILTER_NONE) {
331
+ return {
332
+ field: 'leadSourceKey',
333
+ op: 'isEmpty',
334
+ value: ''
335
+ };
336
+ }
337
+ if (values.includes(LEAD_SOURCE_FILTER_NONE)) {
338
+ return {
339
+ refused: '"No lead source" cannot be picked beside a value'
340
+ };
341
+ }
342
+ const keys = values.map((value)=>crmLeadSourceKey(value)).filter((key)=>Boolean(key));
343
+ return crmAnyOf('leadSourceKey', keys);
344
+ }
345
+ case 'ownerUid':
346
+ return crmAnyOf('ownerUid', values);
347
+ case 'campaignIds':
348
+ {
349
+ if (values.length !== 1) return {
350
+ refused: 'pick one campaign'
351
+ };
352
+ if (!reader.scopeTokens) return {
353
+ field: 'campaignIds',
354
+ op: 'contains',
355
+ value: values[0]
356
+ };
357
+ if (!reader.foldsScope) {
358
+ return {
359
+ refused: 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it'
360
+ };
361
+ }
362
+ return {
363
+ field: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,
364
+ op: 'isAnyOf',
365
+ value: reader.scopeTokens.map((token)=>`${token}${SCOPED_SEARCH_JOIN}${values[0]}`).join(',')
366
+ };
367
+ }
368
+ default:
369
+ return {
370
+ refused: 'this list does not filter by that'
371
+ };
372
+ }
373
+ }
374
+ /**
375
+ * The query shape the Leads list can send beyond its own declaration, for
376
+ * the index pin (AGL-3321): a collaborator's address prefix beside the
377
+ * equalities they may set.
378
+ */ export function leadIndexShapes() {
379
+ const equalities = LEAD_QUERY_FIELDS.filter((field)=>!field.tokensPath);
380
+ return [
381
+ {
382
+ fields: [
383
+ LEAD_PREFIX_SEARCH.field,
384
+ ...equalities
385
+ ],
386
+ sorts: []
387
+ }
388
+ ];
389
+ }
284
390
 
285
391
  //# sourceMappingURL=lead-filters.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/lead-filters.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n EMAIL_STATE_LABELS,\n EMAIL_STATE_STATUSES,\n CRM_LEAD_STATUS_LABELS,\n type EmailStateStatus,\n type CrmLeadFields,\n type CrmLeadStatus,\n emailStateForbidsEmail,\n crmLeadStatus,\n isCrmLeadOpen,\n normalizeCrmPicklistLabel,\n readCampaignIds,\n readEmailState,\n} from '@aglyn/aglyn'\nimport type { CrmViewFilterClause } from '@aglyn/aglyn'\nimport type { ListFilterField } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { type ListGridFilterCodec, listSelectCodec, listRowMatchesSearch } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\n\n/**\n * What the Leads section's `Show` control offers (AGL-2608): the two\n * aggregate views and each status on its own. `open` is the default — the\n * list opens on the work — and it is the one view a Firestore query cannot\n * express, because a lead nobody has touched carries no status field at all;\n * see the section for why the filter runs over a loaded window.\n */\nexport type LeadFilter = 'open' | 'all' | CrmLeadStatus\n\nexport const LEAD_FILTERS: readonly LeadFilter[] = [\n 'open',\n 'new',\n 'working',\n 'qualified',\n 'unqualified',\n 'all',\n]\n\nexport const LEAD_FILTER_LABELS: Record<LeadFilter, string> = {\n open: 'Open',\n all: 'All',\n new: CRM_LEAD_STATUS_LABELS.new,\n working: CRM_LEAD_STATUS_LABELS.working,\n qualified: CRM_LEAD_STATUS_LABELS.qualified,\n unqualified: CRM_LEAD_STATUS_LABELS.unqualified,\n}\n\n/** Whether a lead belongs in a filter's view. An absent status is `new`. */\nexport function leadMatchesFilter(\n lead: Pick<CrmLeadFields, 'status'>,\n filter: LeadFilter,\n): boolean {\n if (filter === 'all') return true\n if (filter === 'open') return isCrmLeadOpen(lead)\n return crmLeadStatus(lead) === filter\n}\n\n/**\n * The Leads section's `Email` control (AGL-3245): every lead, the ones a\n * member must not email — any verdict but `ok` — the ones nothing has been\n * said about, or one verdict on its own. Beside `Show`, not inside it: a\n * bounce does not move a lead out of Open, and a queue of bounced leads is\n * a queue of people to reach some other way.\n */\nexport type LeadEmailFilter = 'any' | 'problem' | 'none' | EmailStateStatus\n\nexport const LEAD_EMAIL_FILTERS: readonly LeadEmailFilter[] = [\n 'any',\n 'problem',\n ...EMAIL_STATE_STATUSES,\n 'none',\n]\n\nexport const LEAD_EMAIL_FILTER_LABELS: Record<LeadEmailFilter, string> = {\n any: 'Any',\n problem: 'Cannot be emailed',\n none: 'Nothing known',\n ...EMAIL_STATE_LABELS,\n}\n\n/** Whether a lead belongs in an email filter's view. */\nexport function leadMatchesEmailFilter(\n lead: Readonly<Record<string, unknown>>,\n filter: LeadEmailFilter,\n): boolean {\n if (filter === 'any') return true\n const state = readEmailState(lead)\n if (filter === 'none') return state === null\n if (filter === 'problem') return emailStateForbidsEmail(state)\n return state?.status === filter\n}\n\n/**\n * The Leads section's `Campaign` control (AGL-3254): every lead, or the\n * ones filed under one campaign — read off the lead's own `campaignIds`,\n * the field every campaign member carries. `''` is every lead, which is\n * what the control opens on; the ids are the site's containers', and the\n * section resolves their names for the menu.\n */\nexport function leadMatchesCampaignFilter(\n lead: Readonly<Record<string, unknown>>,\n campaignId: string,\n): boolean {\n if (!campaignId) return true\n return readCampaignIds(lead).includes(campaignId)\n}\n\n/**\n * The fields the Leads section's search box reads (AGL-3246): who the lead\n * is and where they work — the words a person types to find one — plus the\n * tags, which are how an import names its batch (`sal-15`). `name` and\n * `email` are the capture door's fields rather than the CRM's, so the row is\n * read as a record rather than through `CrmLeadFields`.\n */\nexport const LEAD_SEARCH_FIELDS = ['name', 'email', 'company', 'jobTitle', 'tags'] as const\n\n/**\n * Whether a lead answers a search term.\n *\n * Case-insensitive, and every whitespace-separated word of the term must\n * appear in SOME searched field: `morgan lamphere` and `lamphere sal-15`\n * both find Morgan Lamphere, `morgan smith` does not. A blank term matches\n * every lead, so the box emptied is the list unfiltered.\n */\nexport function leadMatchesSearch(\n lead: Readonly<Record<string, unknown>>,\n term: string,\n): boolean {\n // Tags are a list; a stray string there is no tag of the lead's.\n const tags = Array.isArray(lead['tags']) ? lead['tags'] : undefined\n return listRowMatchesSearch({ ...lead, tags }, LEAD_SEARCH_FIELDS, term.trim().split(/\\s+/))\n}\n\n/**\n * The `Lead source` control's \"no lead source\" choice (AGL-3298). Held in\n * the saved view as an `isEmpty` clause, never as this string; it is only\n * the select's own value for the choice.\n */\nexport const LEAD_SOURCE_FILTER_NONE = '\\u0000none'\n\n/**\n * The Leads section's `Lead source` control (AGL-3298): every lead (`''`),\n * the ones holding one value — compared the way the picklist compares\n * labels, so a value typed in another case before the list existed is\n * still found — or the ones holding none.\n */\nexport function leadMatchesLeadSourceFilter(\n lead: Readonly<Record<string, unknown>>,\n filter: string,\n): boolean {\n if (!filter) return true\n const held = normalizeCrmPicklistLabel(lead['leadSource']).toLowerCase()\n if (filter === LEAD_SOURCE_FILTER_NONE) return held === ''\n return held === normalizeCrmPicklistLabel(filter).toLowerCase()\n}\n\n/*==========================================\n * THE LEADS GRID'S FILTERS (AGL-3313).\n *\n * The Show, Email, Campaign and Lead source dropdowns became columns of the\n * grid's own Filters panel. Their clauses keep the shape the dropdowns\n * stored, so every saved view of leads reads as it did:\n *\n * status `equals <filter>`; none at all is Open, `all` is every lead\n * emailState `equals <filter>`\n * campaignIds `contains <campaign id>`\n * leadSource `equals <label>`, or `isEmpty` for the leads holding none\n *\n * Every one narrows the loaded window, never the query — see the section's\n * `LEADS_WINDOW` for why a status cannot be asked of Firestore.\n *=========================================*/\n\n/** The fields the grid's panel offers on Leads. */\nexport const LEAD_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals', 'isAnyOf'] },\n { column: 'emailState', kind: 'exact', path: 'emailState', operators: ['equals', 'isAnyOf'] },\n { column: 'ownerUid', kind: 'exact', path: 'ownerUid', operators: ['equals', 'isAnyOf'] },\n { column: 'leadSource', kind: 'exact', path: 'leadSource', operators: ['equals', 'isAnyOf'] },\n { column: 'campaignIds', kind: 'exact', path: 'campaignIds', operators: ['equals'] },\n]\n\n/** What each filter field reads as on a chip. */\nexport const LEAD_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n emailState: 'Email',\n ownerUid: 'Owner',\n leadSource: 'Lead source',\n campaignIds: 'Campaign',\n}\n\n/** The choices for Status: Open (new or working) and each status on its own. */\nexport const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter(\n (option) => option !== 'all',\n).map((option) => ({\n value: option,\n label: option === 'open' ? 'Open (new or working)' : LEAD_FILTER_LABELS[option],\n}))\n\n/** The choices for Email: every verdict, plus the two aggregates. */\nexport const LEAD_EMAIL_FILTER_OPTIONS = LEAD_EMAIL_FILTERS.filter(\n (option) => option !== 'any',\n).map((option) => ({ value: option, label: LEAD_EMAIL_FILTER_LABELS[option] }))\n\nconst STATUS_OPEN: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'open' }\nconst STATUS_ALL: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'all' }\n\n/**\n * The clauses the grid shows for a view's stored ones. A stored list with\n * no status clause is Open, which the list opened on before views existed,\n * so it is shown as the clause it means; `status equals all` is every lead\n * and so no clause at all.\n */\nexport function leadClausesForGrid(\n stored: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = stored.find((clause) => clause.field === 'status')\n if (!status) return [STATUS_OPEN, ...stored]\n if (status.op === 'equals' && status.value === 'all') {\n return stored.filter((clause) => clause.field !== 'status')\n }\n return [...stored]\n}\n\n/** The inverse of {@link leadClausesForGrid}: what the view stores. */\nexport function leadClausesToStore(\n shown: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = shown.find((clause) => clause.field === 'status')\n if (!status) return [...shown, STATUS_ALL]\n if (status.op === 'equals' && status.value === 'open') {\n return shown.filter((clause) => clause.field !== 'status')\n }\n return [...shown]\n}\n\nconst listed = (clause: CrmViewFilterClause): string[] =>\n clause.op === 'isAnyOf'\n ? clause.value.split(',').map((entry) => entry.trim()).filter(Boolean)\n : [clause.value]\n\n/** Whether a lead answers every clause the grid shows. */\nexport function leadMatchesClauses(\n lead: Readonly<Record<string, unknown>> & Pick<CrmLeadFields, 'status'>,\n clauses: readonly CrmViewFilterClause[],\n): boolean {\n return clauses.every((clause) => {\n switch (clause.field) {\n case 'status':\n return listed(clause).some((value) =>\n (LEAD_FILTERS as readonly string[]).includes(value)\n ? leadMatchesFilter(lead, value as LeadFilter)\n : true,\n )\n case 'emailState':\n return listed(clause).some((value) =>\n (LEAD_EMAIL_FILTERS as readonly string[]).includes(value)\n ? leadMatchesEmailFilter(lead, value as LeadEmailFilter)\n : true,\n )\n case 'campaignIds':\n return listed(clause).some((value) => leadMatchesCampaignFilter(lead, value))\n case 'leadSource':\n return clause.op === 'isEmpty'\n ? leadMatchesLeadSourceFilter(lead, LEAD_SOURCE_FILTER_NONE)\n : listed(clause).some((value) => leadMatchesLeadSourceFilter(lead, value))\n case 'ownerUid':\n return listed(clause).includes(String(lead['ownerUid'] ?? ''))\n default:\n // A clause on a field this list does not answer narrows nothing,\n // rather than emptying the list.\n return true\n }\n })\n}\n\n/** Campaign clauses were stored as `contains`; the panel's select says `is`. */\nexport const LEAD_CAMPAIGN_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'contains' || clause.op === 'equals'\n ? { operator: 'is', value: clause.value }\n : null,\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n return clause && clause.op === 'equals' ? { ...clause, op: 'contains' } : null\n },\n}\n\n/** \"No lead source\" is an `isEmpty` clause, shown as a choice of the select. */\nexport const LEAD_SOURCE_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'isEmpty'\n ? { operator: 'is', value: LEAD_SOURCE_FILTER_NONE }\n : listSelectCodec.toItem(clause),\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n if (clause?.op === 'equals' && clause.value === LEAD_SOURCE_FILTER_NONE) {\n return { field: clause.field, op: 'isEmpty', value: '' }\n }\n return clause\n },\n}\n\n/** The Leads fields whose stored clauses translate their own way. */\nexport const LEAD_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>> = {\n campaignIds: LEAD_CAMPAIGN_CODEC,\n leadSource: LEAD_SOURCE_CODEC,\n}\n"],"names":["EMAIL_STATE_LABELS","EMAIL_STATE_STATUSES","CRM_LEAD_STATUS_LABELS","emailStateForbidsEmail","crmLeadStatus","isCrmLeadOpen","normalizeCrmPicklistLabel","readCampaignIds","readEmailState","listSelectCodec","listRowMatchesSearch","LEAD_FILTERS","LEAD_FILTER_LABELS","open","all","new","working","qualified","unqualified","leadMatchesFilter","lead","filter","LEAD_EMAIL_FILTERS","LEAD_EMAIL_FILTER_LABELS","any","problem","none","leadMatchesEmailFilter","state","status","leadMatchesCampaignFilter","campaignId","includes","LEAD_SEARCH_FIELDS","leadMatchesSearch","term","tags","Array","isArray","undefined","trim","split","LEAD_SOURCE_FILTER_NONE","leadMatchesLeadSourceFilter","held","toLowerCase","LEAD_LIST_FILTER_FIELDS","column","kind","path","operators","LEAD_LIST_FILTER_HEADERS","emailState","ownerUid","leadSource","campaignIds","LEAD_STATUS_FILTER_OPTIONS","option","map","value","label","LEAD_EMAIL_FILTER_OPTIONS","STATUS_OPEN","field","op","STATUS_ALL","leadClausesForGrid","stored","find","clause","leadClausesToStore","shown","listed","entry","Boolean","leadMatchesClauses","clauses","every","some","String","LEAD_CAMPAIGN_CODEC","toItem","operator","toClause","item","LEAD_SOURCE_CODEC","LEAD_FILTER_CODECS"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,kBAAkB,EAClBC,oBAAoB,EACpBC,sBAAsB,EAItBC,sBAAsB,EACtBC,aAAa,EACbC,aAAa,EACbC,yBAAyB,EACzBC,eAAe,EACfC,cAAc,QACT,eAAc;AAGrB,SAAmCC,eAAe,EAAEC,oBAAoB,QAAQ,8CAA6C;AAW7H,OAAO,MAAMC,eAAsC;IACjD;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAED,OAAO,MAAMC,qBAAiD;IAC5DC,MAAM;IACNC,KAAK;IACLC,KAAKb,uBAAuBa,GAAG;IAC/BC,SAASd,uBAAuBc,OAAO;IACvCC,WAAWf,uBAAuBe,SAAS;IAC3CC,aAAahB,uBAAuBgB,WAAW;AACjD,EAAC;AAED,0EAA0E,GAC1E,OAAO,SAASC,kBACdC,IAAmC,EACnCC,MAAkB;IAElB,IAAIA,WAAW,OAAO,OAAO;IAC7B,IAAIA,WAAW,QAAQ,OAAOhB,cAAce;IAC5C,OAAOhB,cAAcgB,UAAUC;AACjC;AAWA,OAAO,MAAMC,qBAAiD;IAC5D;IACA;OACGrB;IACH;CACD,CAAA;AAED,OAAO,MAAMsB,2BAA4D;IACvEC,KAAK;IACLC,SAAS;IACTC,MAAM;GACH1B,oBACJ;AAED,sDAAsD,GACtD,OAAO,SAAS2B,uBACdP,IAAuC,EACvCC,MAAuB;IAEvB,IAAIA,WAAW,OAAO,OAAO;IAC7B,MAAMO,QAAQpB,eAAeY;IAC7B,IAAIC,WAAW,QAAQ,OAAOO,UAAU;IACxC,IAAIP,WAAW,WAAW,OAAOlB,uBAAuByB;IACxD,OAAOA,CAAAA,yBAAAA,MAAOC,MAAM,MAAKR;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,SAASS,0BACdV,IAAuC,EACvCW,UAAkB;IAElB,IAAI,CAACA,YAAY,OAAO;IACxB,OAAOxB,gBAAgBa,MAAMY,QAAQ,CAACD;AACxC;AAEA;;;;;;CAMC,GACD,OAAO,MAAME,qBAAqB;IAAC;IAAQ;IAAS;IAAW;IAAY;CAAO,CAAS;AAE3F;;;;;;;CAOC,GACD,OAAO,SAASC,kBACdd,IAAuC,EACvCe,IAAY;IAEZ,iEAAiE;IACjE,MAAMC,OAAOC,MAAMC,OAAO,CAAClB,IAAI,CAAC,OAAO,IAAIA,IAAI,CAAC,OAAO,GAAGmB;IAC1D,OAAO7B,qBAAqB,aAAKU;QAAMgB;QAAQH,oBAAoBE,KAAKK,IAAI,GAAGC,KAAK,CAAC;AACvF;AAEA;;;;CAIC,GACD,OAAO,MAAMC,0BAA0B,aAAY;AAEnD;;;;;CAKC,GACD,OAAO,SAASC,4BACdvB,IAAuC,EACvCC,MAAc;IAEd,IAAI,CAACA,QAAQ,OAAO;IACpB,MAAMuB,OAAOtC,0BAA0Bc,IAAI,CAAC,aAAa,EAAEyB,WAAW;IACtE,IAAIxB,WAAWqB,yBAAyB,OAAOE,SAAS;IACxD,OAAOA,SAAStC,0BAA0Be,QAAQwB,WAAW;AAC/D;AAEA;;;;;;;;;;;;;;2CAc2C,GAE3C,iDAAiD,GACjD,OAAO,MAAMC,0BAAsD;IACjE;QAAEC,QAAQ;QAAUC,MAAM;QAASC,MAAM;QAAUC,WAAW;YAAC;YAAU;SAAU;IAAC;IACpF;QAAEH,QAAQ;QAAcC,MAAM;QAASC,MAAM;QAAcC,WAAW;YAAC;YAAU;SAAU;IAAC;IAC5F;QAAEH,QAAQ;QAAYC,MAAM;QAASC,MAAM;QAAYC,WAAW;YAAC;YAAU;SAAU;IAAC;IACxF;QAAEH,QAAQ;QAAcC,MAAM;QAASC,MAAM;QAAcC,WAAW;YAAC;YAAU;SAAU;IAAC;IAC5F;QAAEH,QAAQ;QAAeC,MAAM;QAASC,MAAM;QAAeC,WAAW;YAAC;SAAS;IAAC;CACpF,CAAA;AAED,+CAA+C,GAC/C,OAAO,MAAMC,2BAA6D;IACxEtB,QAAQ;IACRuB,YAAY;IACZC,UAAU;IACVC,YAAY;IACZC,aAAa;AACf,EAAC;AAED,8EAA8E,GAC9E,OAAO,MAAMC,6BAA6B7C,aAAaU,MAAM,CAC3D,CAACoC,SAAWA,WAAW,OACvBC,GAAG,CAAC,CAACD,SAAY,CAAA;QACjBE,OAAOF;QACPG,OAAOH,WAAW,SAAS,0BAA0B7C,kBAAkB,CAAC6C,OAAO;IACjF,CAAA,GAAG;AAEH,mEAAmE,GACnE,OAAO,MAAMI,4BAA4BvC,mBAAmBD,MAAM,CAChE,CAACoC,SAAWA,WAAW,OACvBC,GAAG,CAAC,CAACD,SAAY,CAAA;QAAEE,OAAOF;QAAQG,OAAOrC,wBAAwB,CAACkC,OAAO;IAAC,CAAA,GAAG;AAE/E,MAAMK,cAAmC;IAAEC,OAAO;IAAUC,IAAI;IAAUL,OAAO;AAAO;AACxF,MAAMM,aAAkC;IAAEF,OAAO;IAAUC,IAAI;IAAUL,OAAO;AAAM;AAEtF;;;;;CAKC,GACD,OAAO,SAASO,mBACdC,MAAsC;IAEtC,MAAMtC,SAASsC,OAAOC,IAAI,CAAC,CAACC,SAAWA,OAAON,KAAK,KAAK;IACxD,IAAI,CAAClC,QAAQ,OAAO;QAACiC;WAAgBK;KAAO;IAC5C,IAAItC,OAAOmC,EAAE,KAAK,YAAYnC,OAAO8B,KAAK,KAAK,OAAO;QACpD,OAAOQ,OAAO9C,MAAM,CAAC,CAACgD,SAAWA,OAAON,KAAK,KAAK;IACpD;IACA,OAAO;WAAII;KAAO;AACpB;AAEA,qEAAqE,GACrE,OAAO,SAASG,mBACdC,KAAqC;IAErC,MAAM1C,SAAS0C,MAAMH,IAAI,CAAC,CAACC,SAAWA,OAAON,KAAK,KAAK;IACvD,IAAI,CAAClC,QAAQ,OAAO;WAAI0C;QAAON;KAAW;IAC1C,IAAIpC,OAAOmC,EAAE,KAAK,YAAYnC,OAAO8B,KAAK,KAAK,QAAQ;QACrD,OAAOY,MAAMlD,MAAM,CAAC,CAACgD,SAAWA,OAAON,KAAK,KAAK;IACnD;IACA,OAAO;WAAIQ;KAAM;AACnB;AAEA,MAAMC,SAAS,CAACH,SACdA,OAAOL,EAAE,KAAK,YACVK,OAAOV,KAAK,CAAClB,KAAK,CAAC,KAAKiB,GAAG,CAAC,CAACe,QAAUA,MAAMjC,IAAI,IAAInB,MAAM,CAACqD,WAC5D;QAACL,OAAOV,KAAK;KAAC;AAEpB,wDAAwD,GACxD,OAAO,SAASgB,mBACdvD,IAAuE,EACvEwD,OAAuC;IAEvC,OAAOA,QAAQC,KAAK,CAAC,CAACR;QACpB,OAAQA,OAAON,KAAK;YAClB,KAAK;gBACH,OAAOS,OAAOH,QAAQS,IAAI,CAAC,CAACnB,QAC1B,AAAChD,aAAmCqB,QAAQ,CAAC2B,SACzCxC,kBAAkBC,MAAMuC,SACxB;YAER,KAAK;gBACH,OAAOa,OAAOH,QAAQS,IAAI,CAAC,CAACnB,QAC1B,AAACrC,mBAAyCU,QAAQ,CAAC2B,SAC/ChC,uBAAuBP,MAAMuC,SAC7B;YAER,KAAK;gBACH,OAAOa,OAAOH,QAAQS,IAAI,CAAC,CAACnB,QAAU7B,0BAA0BV,MAAMuC;YACxE,KAAK;gBACH,OAAOU,OAAOL,EAAE,KAAK,YACjBrB,4BAA4BvB,MAAMsB,2BAClC8B,OAAOH,QAAQS,IAAI,CAAC,CAACnB,QAAUhB,4BAA4BvB,MAAMuC;YACvE,KAAK;oBACmCvC;gBAAtC,OAAOoD,OAAOH,QAAQrC,QAAQ,CAAC+C,QAAO3D,iBAAAA,IAAI,CAAC,WAAW,YAAhBA,iBAAoB;YAC5D;gBACE,iEAAiE;gBACjE,iCAAiC;gBACjC,OAAO;QACX;IACF;AACF;AAEA,8EAA8E,GAC9E,OAAO,MAAM4D,sBAA2C;IACtDC,QAAQ,CAACZ,SACPA,OAAOL,EAAE,KAAK,cAAcK,OAAOL,EAAE,KAAK,WACtC;YAAEkB,UAAU;YAAMvB,OAAOU,OAAOV,KAAK;QAAC,IACtC;IACNwB,UAAU,CAACC;QACT,MAAMf,SAAS5D,gBAAgB0E,QAAQ,CAACC;QACxC,OAAOf,UAAUA,OAAOL,EAAE,KAAK,WAAW,aAAKK;YAAQL,IAAI;aAAe;IAC5E;AACF,EAAC;AAED,8EAA8E,GAC9E,OAAO,MAAMqB,oBAAyC;IACpDJ,QAAQ,CAACZ,SACPA,OAAOL,EAAE,KAAK,YACV;YAAEkB,UAAU;YAAMvB,OAAOjB;QAAwB,IACjDjC,gBAAgBwE,MAAM,CAACZ;IAC7Bc,UAAU,CAACC;QACT,MAAMf,SAAS5D,gBAAgB0E,QAAQ,CAACC;QACxC,IAAIf,CAAAA,0BAAAA,OAAQL,EAAE,MAAK,YAAYK,OAAOV,KAAK,KAAKjB,yBAAyB;YACvE,OAAO;gBAAEqB,OAAOM,OAAON,KAAK;gBAAEC,IAAI;gBAAWL,OAAO;YAAG;QACzD;QACA,OAAOU;IACT;AACF,EAAC;AAED,mEAAmE,GACnE,OAAO,MAAMiB,qBAAoE;IAC/E/B,aAAayB;IACb1B,YAAY+B;AACd,EAAC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/lead-filters.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n EMAIL_STATE_LABELS,\n EMAIL_STATE_STATUSES,\n CRM_LEAD_STATUS_LABELS,\n CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n CRM_LEAD_STATUSES,\n type EmailStateStatus,\n type CrmLeadStatus,\n crmLeadSourceKey,\n} from '@aglyn/aglyn'\nimport type { CrmViewFilterClause } from '@aglyn/aglyn'\nimport { SCOPED_SEARCH_JOIN } from '@aglyn/aglyn/app-utils/name-search'\nimport type { ListFilterField, ListFilterRequest } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { type ListGridFilterCodec, listSelectCodec } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n CRM_LIST_SEARCH,\n type CrmClauseAsked,\n crmAnyOf,\n crmClauseValues,\n crmSelectField,\n} from './crm-list-query'\n\n/**\n * What the Leads section's `Show` control offers (AGL-2608): the two\n * aggregate views and each status on its own. `open` is the default — the\n * list opens on the work — asked of the query as `status in [new, working]`,\n * which every lead answers because every lead writer stores a status, `new`\n * for one nobody has touched (`crmLeadListFields`, AGL-3321).\n */\nexport type LeadFilter = 'open' | 'all' | CrmLeadStatus\n\nexport const LEAD_FILTERS: readonly LeadFilter[] = [\n 'open',\n 'new',\n 'working',\n 'qualified',\n 'unqualified',\n 'all',\n]\n\nexport const LEAD_FILTER_LABELS: Record<LeadFilter, string> = {\n open: 'Open',\n all: 'All',\n new: CRM_LEAD_STATUS_LABELS.new,\n working: CRM_LEAD_STATUS_LABELS.working,\n qualified: CRM_LEAD_STATUS_LABELS.qualified,\n unqualified: CRM_LEAD_STATUS_LABELS.unqualified,\n}\n\n/**\n * The Leads section's `Email` control (AGL-3245): every lead, the ones a\n * member must not email — any verdict but `ok` — the ones nothing has been\n * said about, or one verdict on its own. Beside `Show`, not inside it: a\n * bounce does not move a lead out of Open, and a queue of bounced leads is\n * a queue of people to reach some other way.\n */\nexport type LeadEmailFilter = 'any' | 'problem' | 'none' | EmailStateStatus\n\nexport const LEAD_EMAIL_FILTERS: readonly LeadEmailFilter[] = [\n 'any',\n 'problem',\n ...EMAIL_STATE_STATUSES,\n 'none',\n]\n\nexport const LEAD_EMAIL_FILTER_LABELS: Record<LeadEmailFilter, string> = {\n any: 'Any',\n problem: 'Cannot be emailed',\n none: 'Nothing known',\n ...EMAIL_STATE_LABELS,\n}\n\n/**\n * The `Lead source` control's \"no lead source\" choice (AGL-3298). Held in\n * the saved view as an `isEmpty` clause, never as this string; it is only\n * the select's own value for the choice.\n */\nexport const LEAD_SOURCE_FILTER_NONE = '\\u0000none'\n\n/*==========================================\n * THE LEADS GRID'S FILTERS (AGL-3313).\n *\n * The Show, Email, Campaign and Lead source dropdowns became columns of the\n * grid's own Filters panel. Their clauses keep the shape the dropdowns\n * stored, so every saved view of leads reads as it did:\n *\n * status `equals <filter>`; none at all is Open, `all` is every lead\n * emailState `equals <filter>`\n * campaignIds `contains <campaign id>`\n * leadSource `equals <label>`, or `isEmpty` for the leads holding none\n *\n * Every one is asked of the Leads query (AGL-3321) through the field a\n * writer stores for it — see {@link leadQueryClause}.\n *=========================================*/\n\n/** The fields the grid's panel offers on Leads. */\nexport const LEAD_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n { column: 'status', kind: 'exact', path: 'status', operators: ['equals', 'isAnyOf'] },\n { column: 'emailState', kind: 'exact', path: 'emailState', operators: ['equals', 'isAnyOf'] },\n { column: 'ownerUid', kind: 'exact', path: 'ownerUid', operators: ['equals', 'isAnyOf'] },\n { column: 'leadSource', kind: 'exact', path: 'leadSource', operators: ['equals', 'isAnyOf'] },\n { column: 'campaignIds', kind: 'exact', path: 'campaignIds', operators: ['equals'] },\n]\n\n/** What each filter field reads as on a chip. */\nexport const LEAD_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n status: 'Status',\n emailState: 'Email',\n ownerUid: 'Owner',\n leadSource: 'Lead source',\n campaignIds: 'Campaign',\n}\n\n/** The choices for Status: Open (new or working) and each status on its own. */\nexport const LEAD_STATUS_FILTER_OPTIONS = LEAD_FILTERS.filter(\n (option) => option !== 'all',\n).map((option) => ({\n value: option,\n label: option === 'open' ? 'Open (new or working)' : LEAD_FILTER_LABELS[option],\n}))\n\n/** The choices for Email: every verdict, plus the two aggregates. */\nexport const LEAD_EMAIL_FILTER_OPTIONS = LEAD_EMAIL_FILTERS.filter(\n (option) => option !== 'any',\n).map((option) => ({ value: option, label: LEAD_EMAIL_FILTER_LABELS[option] }))\n\nconst STATUS_OPEN: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'open' }\nconst STATUS_ALL: CrmViewFilterClause = { field: 'status', op: 'equals', value: 'all' }\n\n/**\n * The clauses the grid shows for a view's stored ones. A stored list with\n * no status clause is Open, which the list opened on before views existed,\n * so it is shown as the clause it means; `status equals all` is every lead\n * and so no clause at all.\n */\nexport function leadClausesForGrid(\n stored: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = stored.find((clause) => clause.field === 'status')\n if (!status) return [STATUS_OPEN, ...stored]\n if (status.op === 'equals' && status.value === 'all') {\n return stored.filter((clause) => clause.field !== 'status')\n }\n return [...stored]\n}\n\n/** The inverse of {@link leadClausesForGrid}: what the view stores. */\nexport function leadClausesToStore(\n shown: readonly CrmViewFilterClause[],\n): CrmViewFilterClause[] {\n const status = shown.find((clause) => clause.field === 'status')\n if (!status) return [...shown, STATUS_ALL]\n if (status.op === 'equals' && status.value === 'open') {\n return shown.filter((clause) => clause.field !== 'status')\n }\n return [...shown]\n}\n\n/** Campaign clauses were stored as `contains`; the panel's select says `is`. */\nexport const LEAD_CAMPAIGN_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'contains' || clause.op === 'equals'\n ? { operator: 'is', value: clause.value }\n : null,\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n return clause && clause.op === 'equals' ? { ...clause, op: 'contains' } : null\n },\n}\n\n/** \"No lead source\" is an `isEmpty` clause, shown as a choice of the select. */\nexport const LEAD_SOURCE_CODEC: ListGridFilterCodec = {\n toItem: (clause) =>\n clause.op === 'isEmpty'\n ? { operator: 'is', value: LEAD_SOURCE_FILTER_NONE }\n : listSelectCodec.toItem(clause),\n toClause: (item) => {\n const clause = listSelectCodec.toClause(item)\n if (clause?.op === 'equals' && clause.value === LEAD_SOURCE_FILTER_NONE) {\n return { field: clause.field, op: 'isEmpty', value: '' }\n }\n return clause\n },\n}\n\n/** The Leads fields whose stored clauses translate their own way. */\nexport const LEAD_FILTER_CODECS: Readonly<Record<string, ListGridFilterCodec>> = {\n campaignIds: LEAD_CAMPAIGN_CODEC,\n leadSource: LEAD_SOURCE_CODEC,\n}\n\n/*==========================================\n * THE LEADS QUERY (AGL-3321).\n *\n * `orgs/{orgId}/leads`, newest seen first, under a site narrowed by the\n * `visibleTo` scope clause. Each grid clause is asked through a field every\n * lead writer stores (`crmLeadListFields` in `@aglyn/aglyn`):\n *\n * status `status`, stored `new` on a lead nobody has touched, so\n * Open is `status in [new, working]`\n * emailState `emailStatus`, the verdict's status or `none`; \"Cannot be\n * emailed\" is every verdict but `ok`\n * leadSource `leadSourceKey`, the label as the picklist compares it, or\n * `null` for none\n * ownerUid as stored\n * campaignIds `array-contains` on the lead's own campaigns; under a site,\n * `scopedCampaignIds` — the campaign behind each of the\n * site's scope tokens — which stands in the scope clause's\n * place, so the one array clause answers both\n *\n * and the search box reads `searchTokens` — word prefixes of the name,\n * address, company, title and tags. Every equality rides one\n * `(field, lastSeenAtMs DESC)` composite; the campaign and the search are\n * array clauses, so the two do not stand together (nor under a site, where\n * the scope clause is the query's one array clause, save the search, which\n * folds into it for a reader who may drop it).\n *=========================================*/\n\n/** The one order the Leads list takes: newest seen first. */\nexport const LEAD_LIST_SORTS: readonly ListQuerySort[] = [{ path: 'lastSeenAtMs', direction: 'desc' }]\n\n/** The fields the Leads query asks — each a stored field, as named. */\nexport const LEAD_QUERY_FIELDS: readonly ListFilterField[] = [\n crmSelectField('status'),\n crmSelectField('emailStatus'),\n crmSelectField('ownerUid'),\n {\n column: 'leadSourceKey',\n kind: 'exact',\n path: 'leadSourceKey',\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty'],\n },\n {\n column: 'campaignIds',\n kind: 'exact',\n path: 'campaignIds',\n tokensPath: 'campaignIds',\n operators: ['contains'],\n },\n {\n // A campaign under a site: the lead's campaigns behind its scope.\n column: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n kind: 'exact',\n path: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n tokensPath: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n operators: ['isAnyOf'],\n },\n]\n\nexport const LEAD_LIST_DECLARATION: ListQueryDeclaration = {\n fields: LEAD_QUERY_FIELDS,\n sorts: LEAD_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the address, beside the scope clause — the `(visibleTo, email)`\n * composite the Inbox's leads list reads too.\n */\nexport const LEAD_PREFIX_SEARCH = {\n field: {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['startsWith'],\n } satisfies ListFilterField,\n notice: 'Search matches the start of a lead’s address for access limited to specific sites.',\n}\n\n/** Whether an asked clause stands in for the scope clause: a campaign behind the site's scope. */\nexport const leadClauseImpliesScope = (clause: ListFilterRequest): boolean =>\n clause.field === CRM_LEAD_SCOPED_CAMPAIGNS_FIELD\n\n/** The verdicts a member must not email to: every one but `ok`. */\nconst EMAIL_PROBLEMS = EMAIL_STATE_STATUSES.filter((status) => status !== 'ok')\n\n/** Who is asking, for the one clause whose shape depends on it: the Campaign. */\nexport interface LeadQueryReader {\n /** The site's scope tokens, or `null` at the organization level. */\n scopeTokens: readonly string[] | null\n /** Whether the reader may run a query without the scope clause (`useCrmFoldsScope`). */\n foldsScope: boolean\n}\n\nconst ORG_READER: LeadQueryReader = { scopeTokens: null, foldsScope: true }\n\n/**\n * One grid clause as the clause the Leads query asks — see the block above.\n * A value the list does not know is refused by name rather than dropped, so\n * a stored view that names a retired choice says so.\n */\nexport function leadQueryClause(\n clause: CrmViewFilterClause | ListFilterRequest,\n reader: LeadQueryReader = ORG_READER,\n): CrmClauseAsked {\n const values = crmClauseValues(clause)\n switch (clause.field) {\n case 'status': {\n const statuses: string[] = []\n for (const value of values) {\n if (value === 'open') statuses.push('new', 'working')\n else if ((CRM_LEAD_STATUSES as readonly string[]).includes(value)) statuses.push(value)\n else return { refused: `${value} is not a lead status` }\n }\n return crmAnyOf('status', statuses)\n }\n case 'emailState': {\n const keys: string[] = []\n for (const value of values) {\n if (value === 'problem') keys.push(...EMAIL_PROBLEMS)\n else if (value === 'none') keys.push('none')\n else if ((EMAIL_STATE_STATUSES as readonly string[]).includes(value)) keys.push(value)\n else return { refused: `${value} is not an email verdict` }\n }\n return crmAnyOf('emailStatus', keys)\n }\n case 'leadSource': {\n if (clause.op === 'isEmpty' || (values.length === 1 && values[0] === LEAD_SOURCE_FILTER_NONE)) {\n return { field: 'leadSourceKey', op: 'isEmpty', value: '' }\n }\n if (values.includes(LEAD_SOURCE_FILTER_NONE)) {\n return { refused: '\"No lead source\" cannot be picked beside a value' }\n }\n const keys = values.map((value) => crmLeadSourceKey(value)).filter((key): key is string => Boolean(key))\n return crmAnyOf('leadSourceKey', keys)\n }\n case 'ownerUid':\n return crmAnyOf('ownerUid', values)\n case 'campaignIds': {\n if (values.length !== 1) return { refused: 'pick one campaign' }\n if (!reader.scopeTokens) return { field: 'campaignIds', op: 'contains', value: values[0] }\n if (!reader.foldsScope) {\n return {\n refused:\n 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it',\n }\n }\n return {\n field: CRM_LEAD_SCOPED_CAMPAIGNS_FIELD,\n op: 'isAnyOf',\n value: reader.scopeTokens.map((token) => `${token}${SCOPED_SEARCH_JOIN}${values[0]}`).join(','),\n }\n }\n default:\n return { refused: 'this list does not filter by that' }\n }\n}\n\n/**\n * The query shape the Leads list can send beyond its own declaration, for\n * the index pin (AGL-3321): a collaborator's address prefix beside the\n * equalities they may set.\n */\nexport function leadIndexShapes(): ListQueryDeclaration[] {\n const equalities = LEAD_QUERY_FIELDS.filter((field) => !field.tokensPath)\n return [{ fields: [LEAD_PREFIX_SEARCH.field, ...equalities], sorts: [] }]\n}\n"],"names":["EMAIL_STATE_LABELS","EMAIL_STATE_STATUSES","CRM_LEAD_STATUS_LABELS","CRM_LEAD_SCOPED_CAMPAIGNS_FIELD","CRM_LEAD_STATUSES","crmLeadSourceKey","SCOPED_SEARCH_JOIN","listSelectCodec","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","crmSelectField","LEAD_FILTERS","LEAD_FILTER_LABELS","open","all","new","working","qualified","unqualified","LEAD_EMAIL_FILTERS","LEAD_EMAIL_FILTER_LABELS","any","problem","none","LEAD_SOURCE_FILTER_NONE","LEAD_LIST_FILTER_FIELDS","column","kind","path","operators","LEAD_LIST_FILTER_HEADERS","status","emailState","ownerUid","leadSource","campaignIds","LEAD_STATUS_FILTER_OPTIONS","filter","option","map","value","label","LEAD_EMAIL_FILTER_OPTIONS","STATUS_OPEN","field","op","STATUS_ALL","leadClausesForGrid","stored","find","clause","leadClausesToStore","shown","LEAD_CAMPAIGN_CODEC","toItem","operator","toClause","item","LEAD_SOURCE_CODEC","LEAD_FILTER_CODECS","LEAD_LIST_SORTS","direction","LEAD_QUERY_FIELDS","presence","tokensPath","LEAD_LIST_DECLARATION","fields","sorts","search","LEAD_PREFIX_SEARCH","lowerPath","notice","leadClauseImpliesScope","EMAIL_PROBLEMS","ORG_READER","scopeTokens","foldsScope","leadQueryClause","reader","values","statuses","push","includes","refused","keys","length","key","Boolean","token","join","leadIndexShapes","equalities"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { CrmListCollection } from '@aglyn/aglyn';
18
+ import { type MaybeTokenSource } from '@aglyn/shared-util-http/authorized-token';
19
+ import type { CrmTaskRouteScope } from './task-routes';
20
+ /**
21
+ * After a client-direct write the browser cannot follow up itself: ask the
22
+ * list-fields route to restamp the records (AGL-3321). Never throws — the
23
+ * write already happened, and a record left stale is what the backfill and
24
+ * the next write bring level.
25
+ */
26
+ export declare function restampCrmListFields(user: MaybeTokenSource, scope: CrmTaskRouteScope | null, collection: CrmListCollection, ids: readonly string[]): Promise<void>;
@@ -0,0 +1,47 @@
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
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
18
+ import { CRM_LIST_FIELDS_IDS_MAX, crmListFieldsRouteUrl } from "./list-fields.js";
19
+ /**
20
+ * After a client-direct write the browser cannot follow up itself: ask the
21
+ * list-fields route to restamp the records (AGL-3321). Never throws — the
22
+ * write already happened, and a record left stale is what the backfill and
23
+ * the next write bring level.
24
+ */ export async function restampCrmListFields(user, scope, collection, ids) {
25
+ const unique = [
26
+ ...new Set(ids.filter(Boolean))
27
+ ].slice(0, CRM_LIST_FIELDS_IDS_MAX);
28
+ if (!scope || !unique.length) return;
29
+ const body = _extends({}, scope, {
30
+ collection,
31
+ ids: unique
32
+ });
33
+ try {
34
+ const response = await authorizedFetch(user, crmListFieldsRouteUrl(), {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'application/json'
38
+ },
39
+ body: JSON.stringify(body)
40
+ });
41
+ if (!response.ok) console.warn('[crm] list fields were not restamped', response.status);
42
+ } catch (error) {
43
+ console.warn('[crm] list fields were not restamped', error);
44
+ }
45
+ }
46
+
47
+ //# sourceMappingURL=list-fields-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/list-fields-api.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 type { CrmListCollection } from '@aglyn/aglyn'\nimport {\n authorizedFetch,\n type MaybeTokenSource,\n} from '@aglyn/shared-util-http/authorized-token'\nimport {\n CRM_LIST_FIELDS_IDS_MAX,\n type CrmListFieldsRequest,\n crmListFieldsRouteUrl,\n} from './list-fields'\nimport type { CrmTaskRouteScope } from './task-routes'\n\n/**\n * After a client-direct write the browser cannot follow up itself: ask the\n * list-fields route to restamp the records (AGL-3321). Never throws — the\n * write already happened, and a record left stale is what the backfill and\n * the next write bring level.\n */\nexport async function restampCrmListFields(\n user: MaybeTokenSource,\n scope: CrmTaskRouteScope | null,\n collection: CrmListCollection,\n ids: readonly string[],\n): Promise<void> {\n const unique = [...new Set(ids.filter(Boolean))].slice(0, CRM_LIST_FIELDS_IDS_MAX)\n if (!scope || !unique.length) return\n const body: CrmListFieldsRequest = { ...scope, collection, ids: unique }\n try {\n const response = await authorizedFetch(user, crmListFieldsRouteUrl(), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!response.ok) console.warn('[crm] list fields were not restamped', response.status)\n } catch (error) {\n console.warn('[crm] list fields were not restamped', error)\n }\n}\n"],"names":["authorizedFetch","CRM_LIST_FIELDS_IDS_MAX","crmListFieldsRouteUrl","restampCrmListFields","user","scope","collection","ids","unique","Set","filter","Boolean","slice","length","body","response","method","headers","JSON","stringify","ok","console","warn","status","error"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,eAAe,QAEV,2CAA0C;AACjD,SACEC,uBAAuB,EAEvBC,qBAAqB,QAChB,mBAAe;AAGtB;;;;;CAKC,GACD,OAAO,eAAeC,qBACpBC,IAAsB,EACtBC,KAA+B,EAC/BC,UAA6B,EAC7BC,GAAsB;IAEtB,MAAMC,SAAS;WAAI,IAAIC,IAAIF,IAAIG,MAAM,CAACC;KAAU,CAACC,KAAK,CAAC,GAAGX;IAC1D,IAAI,CAACI,SAAS,CAACG,OAAOK,MAAM,EAAE;IAC9B,MAAMC,OAA6B,aAAKT;QAAOC;QAAYC,KAAKC;;IAChE,IAAI;QACF,MAAMO,WAAW,MAAMf,gBAAgBI,MAAMF,yBAAyB;YACpEc,QAAQ;YACRC,SAAS;gBAAE,gBAAgB;YAAmB;YAC9CH,MAAMI,KAAKC,SAAS,CAACL;QACvB;QACA,IAAI,CAACC,SAASK,EAAE,EAAEC,QAAQC,IAAI,CAAC,wCAAwCP,SAASQ,MAAM;IACxF,EAAE,OAAOC,OAAO;QACdH,QAAQC,IAAI,CAAC,wCAAwCE;IACvD;AACF"}
@@ -0,0 +1,47 @@
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 { CrmTaskRouteScope } from './task-routes';
19
+ /**
20
+ * The contract between the console and the plugin's list-fields route
21
+ * (AGL-3321), the door a CLIENT-DIRECT write uses to keep the fields a CRM
22
+ * list queries honest.
23
+ *
24
+ * Those fields — the search tokens, a contact's facet keys — are derived
25
+ * from the record (`crmListFields` in `@aglyn/aglyn`). A client that
26
+ * creates a record, or edits one from the listener's copy, stamps them
27
+ * itself. A write the browser may not follow up — a holder letting go of a
28
+ * contact, which the rules narrow to removing its own half — asks this
29
+ * route to read the record back and restamp it. Best effort, like the
30
+ * next-activity route beside it: the write already happened.
31
+ */
32
+ export declare const CRM_LIST_FIELDS_ROUTE: "crm/list-fields";
33
+ export declare const crmListFieldsRouteUrl: () => string;
34
+ /** The most records one request restamps — the bulk bar's own window. */
35
+ export declare const CRM_LIST_FIELDS_IDS_MAX = 200;
36
+ /** The collections the route restamps. */
37
+ export declare const CRM_LIST_FIELDS_COLLECTIONS: readonly CrmListCollection[];
38
+ export type CrmListFieldsRequest = CrmTaskRouteScope & {
39
+ collection: CrmListCollection;
40
+ ids: string[];
41
+ };
42
+ export interface CrmListFieldsResponse {
43
+ ok: true;
44
+ restamped: number;
45
+ current: number;
46
+ missing: number;
47
+ }
@@ -0,0 +1,39 @@
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
+ * The contract between the console and the plugin's list-fields route
18
+ * (AGL-3321), the door a CLIENT-DIRECT write uses to keep the fields a CRM
19
+ * list queries honest.
20
+ *
21
+ * Those fields — the search tokens, a contact's facet keys — are derived
22
+ * from the record (`crmListFields` in `@aglyn/aglyn`). A client that
23
+ * creates a record, or edits one from the listener's copy, stamps them
24
+ * itself. A write the browser may not follow up — a holder letting go of a
25
+ * contact, which the rules narrow to removing its own half — asks this
26
+ * route to read the record back and restamp it. Best effort, like the
27
+ * next-activity route beside it: the write already happened.
28
+ */ export const CRM_LIST_FIELDS_ROUTE = 'crm/list-fields';
29
+ export const crmListFieldsRouteUrl = ()=>`/api/${CRM_LIST_FIELDS_ROUTE}`;
30
+ /** The most records one request restamps — the bulk bar's own window. */ export const CRM_LIST_FIELDS_IDS_MAX = 200;
31
+ /** The collections the route restamps. */ export const CRM_LIST_FIELDS_COLLECTIONS = [
32
+ 'leads',
33
+ 'contacts',
34
+ 'companies',
35
+ 'deals',
36
+ 'crmTasks'
37
+ ];
38
+
39
+ //# sourceMappingURL=list-fields.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/list-fields.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 type { CrmListCollection } from '@aglyn/aglyn'\nimport type { CrmTaskRouteScope } from './task-routes'\n\n/**\n * The contract between the console and the plugin's list-fields route\n * (AGL-3321), the door a CLIENT-DIRECT write uses to keep the fields a CRM\n * list queries honest.\n *\n * Those fields — the search tokens, a contact's facet keys — are derived\n * from the record (`crmListFields` in `@aglyn/aglyn`). A client that\n * creates a record, or edits one from the listener's copy, stamps them\n * itself. A write the browser may not follow up — a holder letting go of a\n * contact, which the rules narrow to removing its own half — asks this\n * route to read the record back and restamp it. Best effort, like the\n * next-activity route beside it: the write already happened.\n */\nexport const CRM_LIST_FIELDS_ROUTE = 'crm/list-fields' as const\n\nexport const crmListFieldsRouteUrl = (): string => `/api/${CRM_LIST_FIELDS_ROUTE}`\n\n/** The most records one request restamps — the bulk bar's own window. */\nexport const CRM_LIST_FIELDS_IDS_MAX = 200\n\n/** The collections the route restamps. */\nexport const CRM_LIST_FIELDS_COLLECTIONS: readonly CrmListCollection[] = [\n 'leads',\n 'contacts',\n 'companies',\n 'deals',\n 'crmTasks',\n]\n\nexport type CrmListFieldsRequest = CrmTaskRouteScope & {\n collection: CrmListCollection\n ids: string[]\n}\n\nexport interface CrmListFieldsResponse {\n ok: true\n restamped: number\n current: number\n missing: number\n}\n"],"names":["CRM_LIST_FIELDS_ROUTE","crmListFieldsRouteUrl","CRM_LIST_FIELDS_IDS_MAX","CRM_LIST_FIELDS_COLLECTIONS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAKD;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMA,wBAAwB,kBAA0B;AAE/D,OAAO,MAAMC,wBAAwB,IAAc,CAAC,KAAK,EAAED,uBAAuB,CAAA;AAElF,uEAAuE,GACvE,OAAO,MAAME,0BAA0B,IAAG;AAE1C,wCAAwC,GACxC,OAAO,MAAMC,8BAA4D;IACvE;IACA;IACA;IACA;IACA;CACD,CAAA"}