@open-mercato/core 0.6.3-develop.3651.1.d3a548a5f1 → 0.6.3-develop.3653.1.b8558737ae

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 (160) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_settings/index.js +2 -0
  3. package/dist/generated/entities/customer_settings/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/modules/customers/api/deals/aggregate/route.js +332 -0
  7. package/dist/modules/customers/api/deals/aggregate/route.js.map +7 -0
  8. package/dist/modules/customers/api/deals/bulk-update-owner/route.js +188 -0
  9. package/dist/modules/customers/api/deals/bulk-update-owner/route.js.map +7 -0
  10. package/dist/modules/customers/api/deals/bulk-update-stage/route.js +188 -0
  11. package/dist/modules/customers/api/deals/bulk-update-stage/route.js.map +7 -0
  12. package/dist/modules/customers/api/deals/route.js +255 -122
  13. package/dist/modules/customers/api/deals/route.js.map +2 -2
  14. package/dist/modules/customers/api/settings/stuck-threshold/route.js +169 -0
  15. package/dist/modules/customers/api/settings/stuck-threshold/route.js.map +7 -0
  16. package/dist/modules/customers/backend/config/customers/deals/page.js +181 -0
  17. package/dist/modules/customers/backend/config/customers/deals/page.js.map +7 -0
  18. package/dist/modules/customers/backend/config/customers/deals/page.meta.js +38 -0
  19. package/dist/modules/customers/backend/config/customers/deals/page.meta.js.map +7 -0
  20. package/dist/modules/customers/backend/customers/deals/create/page.js +17 -6
  21. package/dist/modules/customers/backend/customers/deals/create/page.js.map +2 -2
  22. package/dist/modules/customers/backend/customers/deals/page.js +2 -0
  23. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  24. package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js +217 -0
  25. package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js.map +7 -0
  26. package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js +335 -0
  27. package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js.map +7 -0
  28. package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js +29 -0
  29. package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js.map +7 -0
  30. package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js +160 -0
  31. package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js.map +7 -0
  32. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js +157 -0
  33. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js.map +7 -0
  34. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js +114 -0
  35. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js.map +7 -0
  36. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js +36 -0
  37. package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js.map +7 -0
  38. package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js +199 -0
  39. package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js.map +7 -0
  40. package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js +71 -0
  41. package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js.map +7 -0
  42. package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js +233 -0
  43. package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js.map +7 -0
  44. package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js +126 -0
  45. package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js.map +7 -0
  46. package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js +31 -0
  47. package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js.map +7 -0
  48. package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js +389 -0
  49. package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js.map +7 -0
  50. package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js +181 -0
  51. package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js.map +7 -0
  52. package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js +199 -0
  53. package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js.map +7 -0
  54. package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js +44 -0
  55. package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js.map +7 -0
  56. package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js +70 -0
  57. package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js.map +7 -0
  58. package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +278 -0
  59. package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +7 -0
  60. package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js +135 -0
  61. package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js.map +7 -0
  62. package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js +83 -0
  63. package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js.map +7 -0
  64. package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js +155 -0
  65. package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js.map +7 -0
  66. package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +318 -0
  67. package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +7 -0
  68. package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js +138 -0
  69. package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js.map +7 -0
  70. package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js +126 -0
  71. package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js.map +7 -0
  72. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +71 -0
  73. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +7 -0
  74. package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js +7 -0
  75. package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js.map +7 -0
  76. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +2330 -415
  77. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +3 -3
  78. package/dist/modules/customers/commands/deals.js +6 -0
  79. package/dist/modules/customers/commands/deals.js.map +2 -2
  80. package/dist/modules/customers/commands/pipeline-stages.js +48 -13
  81. package/dist/modules/customers/commands/pipeline-stages.js.map +3 -3
  82. package/dist/modules/customers/commands/settings.js +45 -5
  83. package/dist/modules/customers/commands/settings.js.map +2 -2
  84. package/dist/modules/customers/data/enrichers.js +130 -1
  85. package/dist/modules/customers/data/enrichers.js.map +2 -2
  86. package/dist/modules/customers/data/entities.js +4 -0
  87. package/dist/modules/customers/data/entities.js.map +2 -2
  88. package/dist/modules/customers/data/validators.js +29 -2
  89. package/dist/modules/customers/data/validators.js.map +2 -2
  90. package/dist/modules/customers/lib/bulkDeals.js +150 -0
  91. package/dist/modules/customers/lib/bulkDeals.js.map +7 -0
  92. package/dist/modules/customers/lib/kysely.js +12 -0
  93. package/dist/modules/customers/lib/kysely.js.map +7 -0
  94. package/dist/modules/customers/lib/stuckDeals.js +32 -0
  95. package/dist/modules/customers/lib/stuckDeals.js.map +7 -0
  96. package/dist/modules/customers/migrations/Migration20260513203311_customers.js +13 -0
  97. package/dist/modules/customers/migrations/Migration20260513203311_customers.js.map +7 -0
  98. package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js +52 -0
  99. package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +7 -0
  100. package/dist/modules/customers/workers/deals-bulk-update-owner.js +41 -0
  101. package/dist/modules/customers/workers/deals-bulk-update-owner.js.map +7 -0
  102. package/dist/modules/customers/workers/deals-bulk-update-stage.js +41 -0
  103. package/dist/modules/customers/workers/deals-bulk-update-stage.js.map +7 -0
  104. package/generated/entities/customer_settings/index.ts +1 -0
  105. package/generated/entity-fields-registry.ts +1 -0
  106. package/package.json +7 -7
  107. package/src/modules/customers/api/deals/aggregate/route.ts +438 -0
  108. package/src/modules/customers/api/deals/bulk-update-owner/route.ts +216 -0
  109. package/src/modules/customers/api/deals/bulk-update-stage/route.ts +228 -0
  110. package/src/modules/customers/api/deals/route.ts +330 -135
  111. package/src/modules/customers/api/settings/stuck-threshold/route.ts +195 -0
  112. package/src/modules/customers/backend/config/customers/deals/page.meta.ts +36 -0
  113. package/src/modules/customers/backend/config/customers/deals/page.tsx +205 -0
  114. package/src/modules/customers/backend/customers/deals/create/page.tsx +25 -6
  115. package/src/modules/customers/backend/customers/deals/page.tsx +2 -0
  116. package/src/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.tsx +277 -0
  117. package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.tsx +425 -0
  118. package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.tsx +31 -0
  119. package/src/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.tsx +171 -0
  120. package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.tsx +196 -0
  121. package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.tsx +138 -0
  122. package/src/modules/customers/backend/customers/deals/pipeline/components/ChipButton.tsx +47 -0
  123. package/src/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.tsx +232 -0
  124. package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.tsx +110 -0
  125. package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.tsx +310 -0
  126. package/src/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.tsx +169 -0
  127. package/src/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.tsx +54 -0
  128. package/src/modules/customers/backend/customers/deals/pipeline/components/DealCard.tsx +562 -0
  129. package/src/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.tsx +214 -0
  130. package/src/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.tsx +257 -0
  131. package/src/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.tsx +62 -0
  132. package/src/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.tsx +101 -0
  133. package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +378 -0
  134. package/src/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.tsx +192 -0
  135. package/src/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.tsx +107 -0
  136. package/src/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.tsx +196 -0
  137. package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +408 -0
  138. package/src/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.tsx +164 -0
  139. package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx +171 -0
  140. package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +84 -0
  141. package/src/modules/customers/backend/customers/deals/pipeline/components/constants.ts +16 -0
  142. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +2841 -550
  143. package/src/modules/customers/commands/deals.ts +19 -0
  144. package/src/modules/customers/commands/pipeline-stages.ts +68 -13
  145. package/src/modules/customers/commands/settings.ts +64 -5
  146. package/src/modules/customers/data/enrichers.ts +209 -2
  147. package/src/modules/customers/data/entities.ts +4 -1
  148. package/src/modules/customers/data/validators.ts +36 -2
  149. package/src/modules/customers/i18n/de.json +244 -0
  150. package/src/modules/customers/i18n/en.json +244 -0
  151. package/src/modules/customers/i18n/es.json +244 -0
  152. package/src/modules/customers/i18n/pl.json +244 -0
  153. package/src/modules/customers/lib/bulkDeals.ts +231 -0
  154. package/src/modules/customers/lib/kysely.ts +60 -0
  155. package/src/modules/customers/lib/stuckDeals.ts +88 -0
  156. package/src/modules/customers/migrations/.snapshot-open-mercato.json +16 -0
  157. package/src/modules/customers/migrations/Migration20260513203311_customers.ts +13 -0
  158. package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +71 -0
  159. package/src/modules/customers/workers/deals-bulk-update-owner.ts +46 -0
  160. package/src/modules/customers/workers/deals-bulk-update-stage.ts +46 -0
@@ -5,6 +5,7 @@ import { CustomerDeal, CustomerDealPersonLink, CustomerDealCompanyLink } from ".
5
5
  import { dealCreateSchema, dealUpdateSchema } from "../../data/validators.js";
6
6
  import { E } from "../../../../generated/entities.ids.generated.js";
7
7
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
8
+ import { parseBooleanFromUnknown } from "@open-mercato/shared/lib/boolean";
8
9
  import {
9
10
  applyEntityIdRestriction,
10
11
  findMatchingEntityIdsWithQueryEngine,
@@ -17,21 +18,44 @@ import {
17
18
  defaultOkResponseSchema
18
19
  } from "../openapi.js";
19
20
  import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
21
+ import { isTenantDataEncryptionEnabled } from "@open-mercato/shared/lib/encryption/toggles";
20
22
  import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
21
23
  import { consumeAdvancedFilterState, mergeAdvancedFilterTree } from "@open-mercato/shared/lib/crud/advanced-filter-integration";
24
+ import { fetchStuckDealIds } from "../../lib/stuckDeals.js";
22
25
  const rawBodySchema = z.object({}).passthrough();
23
- const listSchema = z.object({
26
+ const stringOrStringArray = z.union([z.string(), z.array(z.string())]);
27
+ const booleanQueryParam = z.preprocess((value) => {
28
+ const parsed = parseBooleanFromUnknown(value);
29
+ return parsed === null ? value : parsed;
30
+ }, z.boolean()).optional();
31
+ const dealListQuerySchema = z.object({
24
32
  page: z.coerce.number().min(1).default(1),
25
33
  pageSize: z.coerce.number().min(1).max(100).default(50),
26
34
  search: z.string().optional(),
27
- status: z.string().optional(),
35
+ status: stringOrStringArray.optional(),
28
36
  pipelineStage: z.string().optional(),
29
- pipelineId: z.string().uuid().optional(),
30
- pipelineStageId: z.string().uuid().optional(),
37
+ pipelineId: stringOrStringArray.optional(),
38
+ pipelineStageId: z.union([z.string().uuid(), z.literal("__unassigned")]).optional(),
39
+ ownerUserId: stringOrStringArray.optional(),
40
+ expectedCloseAtFrom: z.string().optional(),
41
+ expectedCloseAtTo: z.string().optional(),
42
+ isStuck: booleanQueryParam,
43
+ isOverdue: booleanQueryParam,
44
+ valueCurrency: stringOrStringArray.optional(),
31
45
  sortField: z.string().optional(),
32
46
  sortDir: z.enum(["asc", "desc"]).optional(),
33
- personEntityId: z.string().uuid().optional(),
34
- companyEntityId: z.string().uuid().optional()
47
+ /**
48
+ * @deprecated Use `personId` instead. The `personEntityId` alias is kept for backward
49
+ * compatibility with older clients and will be removed in a future minor release.
50
+ */
51
+ personEntityId: z.string().uuid().optional().describe("Deprecated; use personId"),
52
+ /**
53
+ * @deprecated Use `companyId` instead. The `companyEntityId` alias is kept for backward
54
+ * compatibility with older clients and will be removed in a future minor release.
55
+ */
56
+ companyEntityId: z.string().uuid().optional().describe("Deprecated; use companyId"),
57
+ personId: stringOrStringArray.optional(),
58
+ companyId: stringOrStringArray.optional()
35
59
  }).passthrough();
36
60
  const routeMetadata = {
37
61
  GET: { requireAuth: true, requireFeatures: ["customers.deals.view"] },
@@ -47,6 +71,67 @@ function parseUuid(value) {
47
71
  const result = z.string().uuid().safeParse(trimmed);
48
72
  return result.success ? trimmed : null;
49
73
  }
74
+ function normalizeStringList(value) {
75
+ const set = /* @__PURE__ */ new Set();
76
+ const visit = (entry) => {
77
+ if (entry == null) return;
78
+ if (Array.isArray(entry)) {
79
+ entry.forEach(visit);
80
+ return;
81
+ }
82
+ if (typeof entry !== "string") return;
83
+ entry.split(",").map((token) => token.trim()).filter((token) => token.length > 0).forEach((token) => set.add(token));
84
+ };
85
+ visit(value);
86
+ return Array.from(set);
87
+ }
88
+ function parseDateInput(value) {
89
+ if (!(typeof value === "string") || value.trim().length === 0) return null;
90
+ const date = new Date(value);
91
+ return Number.isNaN(date.getTime()) ? null : date;
92
+ }
93
+ async function fetchDealIdsMatchingAssociations(em, organizationId, tenantId, personIds, companyIds) {
94
+ if (!personIds.length && !companyIds.length) return [];
95
+ const where = [
96
+ "organization_id = ?",
97
+ "tenant_id = ?",
98
+ "deleted_at IS NULL"
99
+ ];
100
+ const values = [organizationId, tenantId];
101
+ if (personIds.length) {
102
+ const placeholders = personIds.map(() => "?").join(",");
103
+ where.push(
104
+ `EXISTS (SELECT 1 FROM customer_deal_people dp WHERE dp.deal_id = customer_deals.id AND dp.person_entity_id IN (${placeholders}))`
105
+ );
106
+ values.push(...personIds);
107
+ }
108
+ if (companyIds.length) {
109
+ const placeholders = companyIds.map(() => "?").join(",");
110
+ where.push(
111
+ `EXISTS (SELECT 1 FROM customer_deal_companies dc WHERE dc.deal_id = customer_deals.id AND dc.company_entity_id IN (${placeholders}))`
112
+ );
113
+ values.push(...companyIds);
114
+ }
115
+ const rows = await em.getConnection().execute(
116
+ `SELECT id FROM customer_deals WHERE ${where.join(" AND ")}`,
117
+ values
118
+ );
119
+ return rows.map((row) => row.id);
120
+ }
121
+ function normalizeCurrencyList(value) {
122
+ const set = /* @__PURE__ */ new Set();
123
+ const visit = (entry) => {
124
+ if (entry == null) return;
125
+ if (Array.isArray(entry)) {
126
+ entry.forEach(visit);
127
+ return;
128
+ }
129
+ if (typeof entry !== "string") return;
130
+ entry.split(",").map((token) => token.trim().toUpperCase()).filter((token) => /^[A-Z]{3}$/.test(token)).forEach((token) => set.add(token));
131
+ };
132
+ visit(value);
133
+ return Array.from(set);
134
+ }
50
135
  function normalizeUuidList(values) {
51
136
  const set = /* @__PURE__ */ new Set();
52
137
  values.forEach((candidate) => {
@@ -69,6 +154,144 @@ function normalizeUuidList(values) {
69
154
  });
70
155
  return Array.from(set);
71
156
  }
157
+ async function buildDealListFilters(query, ctx) {
158
+ const advancedFilterTree = consumeAdvancedFilterState(query);
159
+ const filters = {};
160
+ let restrictedIds = null;
161
+ const intersectIds = (ids) => {
162
+ if (restrictedIds === null) {
163
+ restrictedIds = ids;
164
+ return;
165
+ }
166
+ const lookup = new Set(ids);
167
+ restrictedIds = restrictedIds.filter((id) => lookup.has(id));
168
+ };
169
+ if (query.search) {
170
+ const matchingIds = ctx ? await findMatchingEntityIdsBySearchTokensAcrossSources({
171
+ ctx,
172
+ query: query.search,
173
+ sources: [
174
+ {
175
+ entityType: E.customers.customer_deal,
176
+ fields: [
177
+ "title",
178
+ "description",
179
+ "status",
180
+ "pipeline_stage",
181
+ "source",
182
+ "value_amount",
183
+ "value_currency",
184
+ "cf:competitive_risk",
185
+ "cf:implementation_complexity"
186
+ ]
187
+ }
188
+ ]
189
+ }) : null;
190
+ if (matchingIds !== null && matchingIds.length > 0) {
191
+ intersectIds(matchingIds);
192
+ } else if (isTenantDataEncryptionEnabled()) {
193
+ intersectIds([]);
194
+ } else {
195
+ const searchPattern = `%${escapeLikePattern(query.search)}%`;
196
+ filters.$or = [
197
+ { title: { $ilike: searchPattern } },
198
+ { description: { $ilike: searchPattern } }
199
+ ];
200
+ }
201
+ }
202
+ const statusList = query.status ? normalizeStringList(query.status) : [];
203
+ if (statusList.length > 0) {
204
+ filters.status = statusList.length === 1 ? { $eq: statusList[0] } : { $in: statusList };
205
+ }
206
+ if (query.pipelineStage) {
207
+ filters.pipeline_stage = { $eq: query.pipelineStage };
208
+ }
209
+ const pipelineIds = query.pipelineId ? normalizeUuidList([query.pipelineId]) : [];
210
+ if (pipelineIds.length > 0) {
211
+ filters.pipeline_id = pipelineIds.length === 1 ? { $eq: pipelineIds[0] } : { $in: pipelineIds };
212
+ }
213
+ if (query.pipelineStageId === "__unassigned") {
214
+ filters.pipeline_stage_id = { $eq: null };
215
+ } else if (query.pipelineStageId) {
216
+ filters.pipeline_stage_id = { $eq: query.pipelineStageId };
217
+ }
218
+ const ownerUserIds = query.ownerUserId ? normalizeUuidList([query.ownerUserId]) : [];
219
+ if (ownerUserIds.length > 0) {
220
+ filters.owner_user_id = ownerUserIds.length === 1 ? { $eq: ownerUserIds[0] } : { $in: ownerUserIds };
221
+ }
222
+ const currencyCodes = query.valueCurrency ? normalizeCurrencyList(query.valueCurrency) : [];
223
+ if (currencyCodes.length > 0) {
224
+ filters.value_currency = currencyCodes.length === 1 ? { $eq: currencyCodes[0] } : { $in: currencyCodes };
225
+ }
226
+ const expectedCloseFrom = parseDateInput(query.expectedCloseAtFrom);
227
+ const expectedCloseTo = parseDateInput(query.expectedCloseAtTo);
228
+ if (expectedCloseFrom || expectedCloseTo) {
229
+ const range = {};
230
+ if (expectedCloseFrom) range.$gte = expectedCloseFrom;
231
+ if (expectedCloseTo) range.$lte = expectedCloseTo;
232
+ filters.expected_close_at = range;
233
+ }
234
+ if (query.isOverdue) {
235
+ const today = /* @__PURE__ */ new Date();
236
+ today.setHours(0, 0, 0, 0);
237
+ if (statusList.length === 0) {
238
+ filters.status = { $eq: "open" };
239
+ }
240
+ const existingRange = filters.expected_close_at && typeof filters.expected_close_at === "object" ? filters.expected_close_at : {};
241
+ existingRange.$lt = today;
242
+ filters.expected_close_at = existingRange;
243
+ }
244
+ if (query.isStuck && ctx) {
245
+ const tenantId = ctx.auth?.tenantId;
246
+ const organizationId = ctx.auth?.orgId;
247
+ if (typeof tenantId === "string" && typeof organizationId === "string") {
248
+ const em = ctx.container.resolve("em");
249
+ const stuckIds = await fetchStuckDealIds(em, organizationId, tenantId);
250
+ intersectIds(stuckIds);
251
+ }
252
+ }
253
+ const url = ctx?.request ? new URL(ctx.request.url) : null;
254
+ const personCandidates = [query.personId, query.personEntityId];
255
+ const companyCandidates = [query.companyId, query.companyEntityId];
256
+ if (url) {
257
+ personCandidates.push(url.searchParams.getAll("personId"));
258
+ personCandidates.push(url.searchParams.getAll("personEntityId"));
259
+ companyCandidates.push(url.searchParams.getAll("companyId"));
260
+ companyCandidates.push(url.searchParams.getAll("companyEntityId"));
261
+ }
262
+ const selectedPersonIds = normalizeUuidList(personCandidates);
263
+ const selectedCompanyIds = normalizeUuidList(companyCandidates);
264
+ if ((selectedPersonIds.length > 0 || selectedCompanyIds.length > 0) && ctx) {
265
+ const tenantId = ctx.auth?.tenantId;
266
+ const organizationId = ctx.auth?.orgId;
267
+ if (typeof tenantId === "string" && typeof organizationId === "string") {
268
+ const em = ctx.container.resolve("em");
269
+ const matchedIds = await fetchDealIdsMatchingAssociations(
270
+ em,
271
+ organizationId,
272
+ tenantId,
273
+ selectedPersonIds,
274
+ selectedCompanyIds
275
+ );
276
+ intersectIds(matchedIds);
277
+ }
278
+ }
279
+ if (ctx && advancedFilterTree) {
280
+ const advancedFilters = mergeAdvancedFilterTree({ ...filters }, advancedFilterTree);
281
+ const matchedIds = await findMatchingEntityIdsWithQueryEngine({
282
+ ctx,
283
+ entityId: E.customers.customer_deal,
284
+ filters: advancedFilters
285
+ });
286
+ if (matchedIds !== null) {
287
+ intersectIds(matchedIds);
288
+ }
289
+ }
290
+ if (restrictedIds !== null) {
291
+ applyEntityIdRestriction(filters, restrictedIds);
292
+ }
293
+ return filters;
294
+ }
72
295
  const crud = makeCrudRoute({
73
296
  metadata: routeMetadata,
74
297
  orm: {
@@ -81,8 +304,9 @@ const crud = makeCrudRoute({
81
304
  indexer: {
82
305
  entityType: E.customers.customer_deal
83
306
  },
307
+ enrichers: { entityId: "customers.deal" },
84
308
  list: {
85
- schema: listSchema,
309
+ schema: dealListQuerySchema,
86
310
  entityId: E.customers.customer_deal,
87
311
  fields: [
88
312
  "id",
@@ -113,66 +337,11 @@ const crud = makeCrudRoute({
113
337
  createdAt: "created_at",
114
338
  updatedAt: "updated_at",
115
339
  title: "title",
116
- value: "value_amount"
340
+ value: "value_amount",
341
+ probability: "probability",
342
+ expectedCloseAt: "expected_close_at"
117
343
  },
118
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
- buildFilters: async (query, ctx) => {
120
- const advancedFilterTree = consumeAdvancedFilterState(query);
121
- const filters = {};
122
- if (query.search) {
123
- const matchingIds = ctx ? await findMatchingEntityIdsBySearchTokensAcrossSources({
124
- ctx,
125
- query: query.search,
126
- sources: [
127
- {
128
- entityType: E.customers.customer_deal,
129
- fields: [
130
- "title",
131
- "description",
132
- "status",
133
- "pipeline_stage",
134
- "source",
135
- "value_amount",
136
- "value_currency",
137
- "cf:competitive_risk",
138
- "cf:implementation_complexity"
139
- ]
140
- }
141
- ]
142
- }) : null;
143
- if (matchingIds !== null && matchingIds.length > 0) {
144
- applyEntityIdRestriction(filters, matchingIds);
145
- } else {
146
- const searchPattern = `%${escapeLikePattern(query.search)}%`;
147
- filters.$or = [
148
- { title: { $ilike: searchPattern } },
149
- { description: { $ilike: searchPattern } }
150
- ];
151
- }
152
- }
153
- if (query.status) {
154
- filters.status = { $eq: query.status };
155
- }
156
- if (query.pipelineStage) {
157
- filters.pipeline_stage = { $eq: query.pipelineStage };
158
- }
159
- if (query.pipelineId) {
160
- filters.pipeline_id = { $eq: query.pipelineId };
161
- }
162
- if (query.pipelineStageId) {
163
- filters.pipeline_stage_id = { $eq: query.pipelineStageId };
164
- }
165
- if (ctx && advancedFilterTree) {
166
- const advancedFilters = mergeAdvancedFilterTree({ ...filters }, advancedFilterTree);
167
- const matchedIds = await findMatchingEntityIdsWithQueryEngine({
168
- ctx,
169
- entityId: E.customers.customer_deal,
170
- filters: advancedFilters
171
- });
172
- applyEntityIdRestriction(filters, matchedIds);
173
- }
174
- return filters;
175
- }
344
+ buildFilters: buildDealListFilters
176
345
  },
177
346
  actions: {
178
347
  create: {
@@ -207,42 +376,18 @@ const crud = makeCrudRoute({
207
376
  }
208
377
  },
209
378
  hooks: {
210
- beforeList: (query, ctx) => {
211
- const url = ctx.request ? new URL(ctx.request.url) : null;
212
- const legacyPersonId = query.personEntityId ?? null;
213
- const legacyCompanyId = query.companyEntityId ?? null;
214
- const allPersonCandidates = [];
215
- const allCompanyCandidates = [];
216
- if (legacyPersonId) allPersonCandidates.push(legacyPersonId);
217
- if (legacyCompanyId) allCompanyCandidates.push(legacyCompanyId);
218
- if (url) {
219
- const personParams = url.searchParams.getAll("personId");
220
- const companyParams = url.searchParams.getAll("companyId");
221
- if (personParams.length) allPersonCandidates.push(...personParams);
222
- if (companyParams.length) allCompanyCandidates.push(...companyParams);
223
- const legacyRepeatPerson = url.searchParams.getAll("personEntityId");
224
- const legacyRepeatCompany = url.searchParams.getAll("companyEntityId");
225
- if (legacyRepeatPerson.length) allPersonCandidates.push(...legacyRepeatPerson);
226
- if (legacyRepeatCompany.length) allCompanyCandidates.push(...legacyRepeatCompany);
227
- }
228
- const personIds = normalizeUuidList(allPersonCandidates);
229
- const companyIds = normalizeUuidList(allCompanyCandidates);
230
- ctx.__dealsFilters = {
231
- personIds,
232
- companyIds
233
- };
234
- },
379
+ // afterList only DECORATES results with `people`/`companies` arrays — it must not filter,
380
+ // because filtering after pagination would drop deals on later pages and rewrite `total`
381
+ // to a misleading value. Association filtering happens pre-pagination in `buildFilters`
382
+ // via `fetchDealIdsMatchingAssociations`.
235
383
  afterList: async (payload, ctx) => {
236
- const filters = ctx.__dealsFilters || {};
237
- const selectedPersonIds = Array.isArray(filters.personIds) ? filters.personIds : [];
238
- const selectedCompanyIds = Array.isArray(filters.companyIds) ? filters.companyIds : [];
239
384
  const items = Array.isArray(payload.items) ? payload.items : [];
240
385
  if (!items.length) return;
241
386
  const scopeSource = items[0] ?? {};
242
387
  const tenantIdRaw = scopeSource.tenantId ?? scopeSource.tenant_id;
243
- const fallbackTenantId = (typeof tenantIdRaw === "string" && tenantIdRaw.trim().length ? tenantIdRaw : null) ?? ctx?.auth?.tenantId ?? null;
388
+ const fallbackTenantId = (typeof tenantIdRaw === "string" && tenantIdRaw.trim().length ? tenantIdRaw : null) ?? ctx.auth?.tenantId ?? null;
244
389
  const orgIdRaw = scopeSource.organizationId ?? scopeSource.organization_id;
245
- const fallbackOrganizationId = (typeof orgIdRaw === "string" && orgIdRaw.trim().length ? orgIdRaw : null) ?? ctx?.auth?.orgId ?? null;
390
+ const fallbackOrganizationId = (typeof orgIdRaw === "string" && orgIdRaw.trim().length ? orgIdRaw : null) ?? ctx.auth?.orgId ?? null;
246
391
  const ids = items.map((item) => {
247
392
  if (!item || typeof item !== "object") return null;
248
393
  const candidate = item.id;
@@ -272,7 +417,6 @@ const crud = makeCrudRoute({
272
417
  )
273
418
  ]);
274
419
  const personAssignments = /* @__PURE__ */ new Map();
275
- const personMemberships = /* @__PURE__ */ new Map();
276
420
  allPersonLinks.forEach((link) => {
277
421
  const deal = link.deal;
278
422
  const dealRecord = deal && typeof deal === "object" ? deal : null;
@@ -288,12 +432,8 @@ const crud = makeCrudRoute({
288
432
  bucket.push({ id: personId, label });
289
433
  personAssignments.set(dealId, bucket);
290
434
  }
291
- const membership = personMemberships.get(dealId) ?? /* @__PURE__ */ new Set();
292
- membership.add(personId);
293
- personMemberships.set(dealId, membership);
294
435
  });
295
436
  const companyAssignments = /* @__PURE__ */ new Map();
296
- const companyMemberships = /* @__PURE__ */ new Map();
297
437
  allCompanyLinks.forEach((link) => {
298
438
  const deal = link.deal;
299
439
  const dealRecord = deal && typeof deal === "object" ? deal : null;
@@ -309,18 +449,7 @@ const crud = makeCrudRoute({
309
449
  bucket.push({ id: companyId, label });
310
450
  companyAssignments.set(dealId, bucket);
311
451
  }
312
- const membership = companyMemberships.get(dealId) ?? /* @__PURE__ */ new Set();
313
- membership.add(companyId);
314
- companyMemberships.set(dealId, membership);
315
452
  });
316
- const hasPersonFilter = selectedPersonIds.length > 0;
317
- const hasCompanyFilter = selectedCompanyIds.length > 0;
318
- const matchesAll = (selected, memberships, dealId) => {
319
- if (!selected.length) return true;
320
- const membership = memberships.get(dealId);
321
- if (!membership || membership.size === 0) return false;
322
- return selected.every((id) => membership.has(id));
323
- };
324
453
  const enhancedItems = items.map((item) => {
325
454
  if (!item || typeof item !== "object") return null;
326
455
  const data = item;
@@ -328,9 +457,6 @@ const crud = makeCrudRoute({
328
457
  if (!candidate || !candidate.trim().length) return null;
329
458
  const people = personAssignments.get(candidate) ?? [];
330
459
  const companies = companyAssignments.get(candidate) ?? [];
331
- const matchesPerson = matchesAll(selectedPersonIds, personMemberships, candidate);
332
- const matchesCompany = matchesAll(selectedCompanyIds, companyMemberships, candidate);
333
- if (!matchesPerson || !matchesCompany) return null;
334
460
  const tenantIdRaw2 = typeof data.tenantId === "string" ? data.tenantId : typeof data.tenant_id === "string" ? data.tenant_id : null;
335
461
  const organizationIdRaw = typeof data.organizationId === "string" ? data.organizationId : typeof data.organization_id === "string" ? data.organization_id : null;
336
462
  const tenantId = tenantIdRaw2 && tenantIdRaw2.trim().length ? tenantIdRaw2.trim() : null;
@@ -348,13 +474,18 @@ const crud = makeCrudRoute({
348
474
  (item) => item !== null
349
475
  );
350
476
  payload.items = enhancedItems;
351
- if (hasPersonFilter || hasCompanyFilter) {
352
- payload.total = enhancedItems.length;
353
- payload.totalPages = 1;
354
- payload.page = 1;
355
- }
356
477
  } catch (err) {
357
- console.warn("[customers.deals] failed to filter by person/company link", err);
478
+ console.warn("[customers.deals] failed to decorate items with person/company links", err);
479
+ payload.items = items.map((item) => {
480
+ if (!item || typeof item !== "object") return item;
481
+ return {
482
+ ...item,
483
+ _associations: {
484
+ ok: false,
485
+ reason: err instanceof Error ? err.message : "unknown"
486
+ }
487
+ };
488
+ });
358
489
  }
359
490
  }
360
491
  }
@@ -395,7 +526,7 @@ const dealCreateResponseSchema = z.object({
395
526
  });
396
527
  const openApi = createCustomersCrudOpenApi({
397
528
  resourceName: "Deal",
398
- querySchema: listSchema,
529
+ querySchema: dealListQuerySchema,
399
530
  listResponseSchema: createPagedListResponseSchema(dealListItemSchema),
400
531
  create: {
401
532
  schema: dealCreateSchema,
@@ -418,6 +549,8 @@ export {
418
549
  GET,
419
550
  POST,
420
551
  PUT,
552
+ buildDealListFilters,
553
+ dealListQuerySchema,
421
554
  metadata,
422
555
  openApi
423
556
  };