@open-mercato/core 0.6.3-develop.3651.1.d3a548a5f1 → 0.6.3-develop.3660.1.53b73d5aa4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/customer_settings/index.js +2 -0
- package/dist/generated/entities/customer_settings/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/customers/api/deals/aggregate/route.js +332 -0
- package/dist/modules/customers/api/deals/aggregate/route.js.map +7 -0
- package/dist/modules/customers/api/deals/bulk-update-owner/route.js +188 -0
- package/dist/modules/customers/api/deals/bulk-update-owner/route.js.map +7 -0
- package/dist/modules/customers/api/deals/bulk-update-stage/route.js +188 -0
- package/dist/modules/customers/api/deals/bulk-update-stage/route.js.map +7 -0
- package/dist/modules/customers/api/deals/route.js +255 -122
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/settings/stuck-threshold/route.js +169 -0
- package/dist/modules/customers/api/settings/stuck-threshold/route.js.map +7 -0
- package/dist/modules/customers/backend/config/customers/deals/page.js +181 -0
- package/dist/modules/customers/backend/config/customers/deals/page.js.map +7 -0
- package/dist/modules/customers/backend/config/customers/deals/page.meta.js +38 -0
- package/dist/modules/customers/backend/config/customers/deals/page.meta.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/create/page.js +17 -6
- package/dist/modules/customers/backend/customers/deals/create/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +2 -0
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js +217 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js +335 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js +29 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js +160 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js +157 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js +114 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js +36 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChipButton.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js +199 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js +71 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js +233 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js +126 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js +31 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js +389 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCard.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js +181 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js +199 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js +44 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js +70 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js +278 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/Lane.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js +135 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js +83 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js +155 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +318 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js +138 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js +126 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +71 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/constants.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +2330 -415
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +3 -3
- package/dist/modules/customers/commands/deals.js +6 -0
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/pipeline-stages.js +48 -13
- package/dist/modules/customers/commands/pipeline-stages.js.map +3 -3
- package/dist/modules/customers/commands/settings.js +45 -5
- package/dist/modules/customers/commands/settings.js.map +2 -2
- package/dist/modules/customers/data/enrichers.js +130 -1
- package/dist/modules/customers/data/enrichers.js.map +2 -2
- package/dist/modules/customers/data/entities.js +4 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +29 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/bulkDeals.js +150 -0
- package/dist/modules/customers/lib/bulkDeals.js.map +7 -0
- package/dist/modules/customers/lib/kysely.js +12 -0
- package/dist/modules/customers/lib/kysely.js.map +7 -0
- package/dist/modules/customers/lib/stuckDeals.js +32 -0
- package/dist/modules/customers/lib/stuckDeals.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260513203311_customers.js +13 -0
- package/dist/modules/customers/migrations/Migration20260513203311_customers.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js +52 -0
- package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +7 -0
- package/dist/modules/customers/workers/deals-bulk-update-owner.js +41 -0
- package/dist/modules/customers/workers/deals-bulk-update-owner.js.map +7 -0
- package/dist/modules/customers/workers/deals-bulk-update-stage.js +41 -0
- package/dist/modules/customers/workers/deals-bulk-update-stage.js.map +7 -0
- package/generated/entities/customer_settings/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/customers/api/deals/aggregate/route.ts +438 -0
- package/src/modules/customers/api/deals/bulk-update-owner/route.ts +216 -0
- package/src/modules/customers/api/deals/bulk-update-stage/route.ts +228 -0
- package/src/modules/customers/api/deals/route.ts +330 -135
- package/src/modules/customers/api/settings/stuck-threshold/route.ts +195 -0
- package/src/modules/customers/backend/config/customers/deals/page.meta.ts +36 -0
- package/src/modules/customers/backend/config/customers/deals/page.tsx +205 -0
- package/src/modules/customers/backend/customers/deals/create/page.tsx +25 -6
- package/src/modules/customers/backend/customers/deals/page.tsx +2 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ActivityComposerDialog.tsx +277 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageDialog.tsx +425 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/AddStageLane.tsx +31 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/BulkActionsBar.tsx +171 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.tsx +196 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeStageDialog.tsx +138 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChipButton.tsx +47 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CloseDateFilterPopover.tsx +232 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyBreakdownTable.tsx +110 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CurrencyFilterPopover.tsx +310 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/CustomizeViewDialog.tsx +169 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/DashedTileButton.tsx +54 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/DealCard.tsx +562 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/DealCardMenu.tsx +214 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/EntityFilterPopover.tsx +257 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.tsx +62 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/FilterPopoverShell.tsx +101 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/Lane.tsx +378 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/LaneCurrencyBreakdown.tsx +192 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/LaneResizeHandle.tsx +107 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/PipelineFilterPopover.tsx +196 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +408 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/SortByPopover.tsx +164 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/StatusFilterPopover.tsx +171 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +84 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/constants.ts +16 -0
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +2841 -550
- package/src/modules/customers/commands/deals.ts +19 -0
- package/src/modules/customers/commands/pipeline-stages.ts +68 -13
- package/src/modules/customers/commands/settings.ts +64 -5
- package/src/modules/customers/data/enrichers.ts +209 -2
- package/src/modules/customers/data/entities.ts +4 -1
- package/src/modules/customers/data/validators.ts +36 -2
- package/src/modules/customers/i18n/de.json +244 -0
- package/src/modules/customers/i18n/en.json +244 -0
- package/src/modules/customers/i18n/es.json +244 -0
- package/src/modules/customers/i18n/pl.json +244 -0
- package/src/modules/customers/lib/bulkDeals.ts +231 -0
- package/src/modules/customers/lib/kysely.ts +60 -0
- package/src/modules/customers/lib/stuckDeals.ts +88 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/customers/migrations/Migration20260513203311_customers.ts +13 -0
- package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +71 -0
- package/src/modules/customers/workers/deals-bulk-update-owner.ts +46 -0
- 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
|
|
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:
|
|
35
|
+
status: stringOrStringArray.optional(),
|
|
28
36
|
pipelineStage: z.string().optional(),
|
|
29
|
-
pipelineId:
|
|
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
|
-
|
|
34
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
};
|