@hed-hog/contact 0.0.294 → 0.0.296
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/dist/person/dto/account.dto.d.ts +28 -0
- package/dist/person/dto/account.dto.d.ts.map +1 -0
- package/dist/person/dto/account.dto.js +123 -0
- package/dist/person/dto/account.dto.js.map +1 -0
- package/dist/person/dto/activity.dto.d.ts +15 -0
- package/dist/person/dto/activity.dto.d.ts.map +1 -0
- package/dist/person/dto/activity.dto.js +65 -0
- package/dist/person/dto/activity.dto.js.map +1 -0
- package/dist/person/dto/dashboard-query.dto.d.ts +9 -0
- package/dist/person/dto/dashboard-query.dto.d.ts.map +1 -0
- package/dist/person/dto/dashboard-query.dto.js +40 -0
- package/dist/person/dto/dashboard-query.dto.js.map +1 -0
- package/dist/person/dto/followup-query.dto.d.ts +10 -0
- package/dist/person/dto/followup-query.dto.d.ts.map +1 -0
- package/dist/person/dto/followup-query.dto.js +45 -0
- package/dist/person/dto/followup-query.dto.js.map +1 -0
- package/dist/person/dto/reports-query.dto.d.ts +8 -0
- package/dist/person/dto/reports-query.dto.d.ts.map +1 -0
- package/dist/person/dto/reports-query.dto.js +33 -0
- package/dist/person/dto/reports-query.dto.js.map +1 -0
- package/dist/person/person.controller.d.ts +266 -5
- package/dist/person/person.controller.d.ts.map +1 -1
- package/dist/person/person.controller.js +164 -6
- package/dist/person/person.controller.js.map +1 -1
- package/dist/person/person.service.d.ts +295 -5
- package/dist/person/person.service.d.ts.map +1 -1
- package/dist/person/person.service.js +1752 -27
- package/dist/person/person.service.js.map +1 -1
- package/dist/person-relation-type/person-relation-type.controller.d.ts +2 -2
- package/dist/person-relation-type/person-relation-type.service.d.ts +2 -2
- package/hedhog/data/route.yaml +68 -19
- package/hedhog/frontend/app/_lib/crm-sections.tsx.ejs +9 -9
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +573 -477
- package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +9 -6
- package/hedhog/frontend/app/accounts/page.tsx.ejs +970 -892
- package/hedhog/frontend/app/activities/_components/activity-detail-sheet.tsx.ejs +240 -0
- package/hedhog/frontend/app/activities/_components/activity-types.ts.ejs +66 -0
- package/hedhog/frontend/app/activities/page.tsx.ejs +460 -812
- package/hedhog/frontend/app/dashboard/_components/dashboard-types.ts.ejs +70 -0
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +639 -491
- package/hedhog/frontend/app/follow-ups/page.tsx.ejs +785 -696
- package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +10 -12
- package/hedhog/frontend/app/reports/_components/report-types.ts.ejs +84 -0
- package/hedhog/frontend/app/reports/page.tsx.ejs +1196 -15
- package/hedhog/frontend/messages/en.json +242 -38
- package/hedhog/frontend/messages/pt.json +242 -38
- package/hedhog/table/crm_activity.yaml +68 -0
- package/hedhog/table/crm_stage_history.yaml +34 -0
- package/hedhog/table/person_company.yaml +27 -5
- package/package.json +9 -9
- package/src/person/dto/account.dto.ts +100 -0
- package/src/person/dto/activity.dto.ts +54 -0
- package/src/person/dto/dashboard-query.dto.ts +25 -0
- package/src/person/dto/followup-query.dto.ts +25 -0
- package/src/person/dto/reports-query.dto.ts +25 -0
- package/src/person/person.controller.ts +176 -43
- package/src/person/person.service.ts +4825 -2226
|
@@ -18,6 +18,7 @@ const api_pagination_1 = require("@hed-hog/api-pagination");
|
|
|
18
18
|
const api_prisma_1 = require("@hed-hog/api-prisma");
|
|
19
19
|
const core_1 = require("@hed-hog/core");
|
|
20
20
|
const common_1 = require("@nestjs/common");
|
|
21
|
+
const account_dto_1 = require("./dto/account.dto");
|
|
21
22
|
const create_interaction_dto_1 = require("./dto/create-interaction.dto");
|
|
22
23
|
const CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING = 'contact-allow-company-registration';
|
|
23
24
|
const CONTACT_OWNER_ROLE_SLUG = 'owner-contact';
|
|
@@ -38,6 +39,31 @@ const DEAL_VALUE_METADATA_KEY = 'deal_value';
|
|
|
38
39
|
const TAGS_METADATA_KEY = 'tags';
|
|
39
40
|
const LAST_INTERACTION_AT_METADATA_KEY = 'last_interaction_at';
|
|
40
41
|
const INTERACTIONS_METADATA_KEY = 'interactions';
|
|
42
|
+
const CRM_DASHBOARD_STAGE_ORDER = [
|
|
43
|
+
'new',
|
|
44
|
+
'contacted',
|
|
45
|
+
'qualified',
|
|
46
|
+
'proposal',
|
|
47
|
+
'negotiation',
|
|
48
|
+
'customer',
|
|
49
|
+
'lost',
|
|
50
|
+
];
|
|
51
|
+
const CRM_DASHBOARD_SOURCE_ORDER = [
|
|
52
|
+
'website',
|
|
53
|
+
'referral',
|
|
54
|
+
'social',
|
|
55
|
+
'inbound',
|
|
56
|
+
'outbound',
|
|
57
|
+
'other',
|
|
58
|
+
];
|
|
59
|
+
const CRM_REPORT_ACTIVITY_ORDER = [
|
|
60
|
+
'call',
|
|
61
|
+
'email',
|
|
62
|
+
'meeting',
|
|
63
|
+
'whatsapp',
|
|
64
|
+
'task',
|
|
65
|
+
'note',
|
|
66
|
+
];
|
|
41
67
|
let PersonService = class PersonService {
|
|
42
68
|
constructor(prismaService, paginationService, fileService, settingService) {
|
|
43
69
|
this.prismaService = prismaService;
|
|
@@ -80,6 +106,334 @@ let PersonService = class PersonService {
|
|
|
80
106
|
inactive,
|
|
81
107
|
};
|
|
82
108
|
}
|
|
109
|
+
async getDashboard(query, locale) {
|
|
110
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
111
|
+
const ownerUserId = this.coerceNumber(query.owner_user_id);
|
|
112
|
+
const ranges = this.resolveDashboardRanges(query, locale);
|
|
113
|
+
const where = {};
|
|
114
|
+
if (!allowCompanyRegistration) {
|
|
115
|
+
where.type = 'individual';
|
|
116
|
+
}
|
|
117
|
+
if (ownerUserId > 0) {
|
|
118
|
+
where.AND = [
|
|
119
|
+
{
|
|
120
|
+
person_metadata: {
|
|
121
|
+
some: {
|
|
122
|
+
key: OWNER_USER_METADATA_KEY,
|
|
123
|
+
value: ownerUserId,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
const people = await this.prismaService.person.findMany({
|
|
130
|
+
where,
|
|
131
|
+
select: {
|
|
132
|
+
id: true,
|
|
133
|
+
name: true,
|
|
134
|
+
type: true,
|
|
135
|
+
status: true,
|
|
136
|
+
avatar_id: true,
|
|
137
|
+
created_at: true,
|
|
138
|
+
updated_at: true,
|
|
139
|
+
person_metadata: true,
|
|
140
|
+
},
|
|
141
|
+
orderBy: {
|
|
142
|
+
created_at: 'desc',
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
const enriched = await this.enrichPeople(people, allowCompanyRegistration);
|
|
146
|
+
const createdScoped = enriched.filter((person) => this.isDateWithinRange(person.created_at, ranges.created));
|
|
147
|
+
const nextActionScoped = enriched.filter((person) => !!person.next_action_at &&
|
|
148
|
+
this.isDateWithinRange(person.next_action_at, ranges.operational));
|
|
149
|
+
const kpis = {
|
|
150
|
+
total_leads: createdScoped.length,
|
|
151
|
+
qualified: createdScoped.filter((person) => {
|
|
152
|
+
var _a;
|
|
153
|
+
return ['qualified', 'proposal', 'negotiation', 'customer'].includes((_a = person.lifecycle_stage) !== null && _a !== void 0 ? _a : 'new');
|
|
154
|
+
}).length,
|
|
155
|
+
proposal: createdScoped.filter((person) => person.lifecycle_stage === 'proposal').length,
|
|
156
|
+
customers: createdScoped.filter((person) => person.lifecycle_stage === 'customer').length,
|
|
157
|
+
lost: createdScoped.filter((person) => person.lifecycle_stage === 'lost')
|
|
158
|
+
.length,
|
|
159
|
+
unassigned: createdScoped.filter((person) => !person.owner_user_id).length,
|
|
160
|
+
overdue: nextActionScoped.filter((person) => {
|
|
161
|
+
const nextActionAt = this.parseDateOrNull(person.next_action_at);
|
|
162
|
+
return !!nextActionAt && nextActionAt.getTime() < Date.now();
|
|
163
|
+
}).length,
|
|
164
|
+
next_actions: nextActionScoped.length,
|
|
165
|
+
};
|
|
166
|
+
const charts = {
|
|
167
|
+
stage: CRM_DASHBOARD_STAGE_ORDER.map((key) => ({
|
|
168
|
+
key,
|
|
169
|
+
total: createdScoped.filter((person) => { var _a; return ((_a = person.lifecycle_stage) !== null && _a !== void 0 ? _a : 'new') === key; }).length,
|
|
170
|
+
})),
|
|
171
|
+
source: CRM_DASHBOARD_SOURCE_ORDER.map((key) => ({
|
|
172
|
+
key,
|
|
173
|
+
total: createdScoped.filter((person) => { var _a; return ((_a = person.source) !== null && _a !== void 0 ? _a : 'other') === key; }).length,
|
|
174
|
+
})),
|
|
175
|
+
owner_performance: this.buildDashboardOwnerPerformance(createdScoped),
|
|
176
|
+
};
|
|
177
|
+
const lists = {
|
|
178
|
+
next_actions: [...nextActionScoped]
|
|
179
|
+
.sort((left, right) => {
|
|
180
|
+
var _a, _b;
|
|
181
|
+
return new Date((_a = left.next_action_at) !== null && _a !== void 0 ? _a : 0).getTime() -
|
|
182
|
+
new Date((_b = right.next_action_at) !== null && _b !== void 0 ? _b : 0).getTime();
|
|
183
|
+
})
|
|
184
|
+
.slice(0, 5)
|
|
185
|
+
.map((person) => this.mapDashboardListItem(person, {
|
|
186
|
+
includeCreatedAt: false,
|
|
187
|
+
includeNextActionAt: true,
|
|
188
|
+
})),
|
|
189
|
+
unattended: [...createdScoped]
|
|
190
|
+
.filter((person) => { var _a; return !person.owner_user_id || ((_a = person.lifecycle_stage) !== null && _a !== void 0 ? _a : 'new') === 'new'; })
|
|
191
|
+
.sort((left, right) => {
|
|
192
|
+
var _a, _b;
|
|
193
|
+
return new Date((_a = right.created_at) !== null && _a !== void 0 ? _a : 0).getTime() -
|
|
194
|
+
new Date((_b = left.created_at) !== null && _b !== void 0 ? _b : 0).getTime();
|
|
195
|
+
})
|
|
196
|
+
.slice(0, 5)
|
|
197
|
+
.map((person) => this.mapDashboardListItem(person, {
|
|
198
|
+
includeCreatedAt: true,
|
|
199
|
+
includeNextActionAt: false,
|
|
200
|
+
})),
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
kpis,
|
|
204
|
+
charts,
|
|
205
|
+
lists,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async getReports(query, locale) {
|
|
209
|
+
var _a, _b;
|
|
210
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
211
|
+
const { start, end } = this.resolveReportsRange(query, locale);
|
|
212
|
+
const startIso = start.toISOString();
|
|
213
|
+
const endIso = end.toISOString();
|
|
214
|
+
const createdPeople = await this.prismaService.person.findMany({
|
|
215
|
+
where: Object.assign({ created_at: {
|
|
216
|
+
gte: start,
|
|
217
|
+
lte: end,
|
|
218
|
+
} }, (allowCompanyRegistration ? {} : { type: 'individual' })),
|
|
219
|
+
select: {
|
|
220
|
+
id: true,
|
|
221
|
+
name: true,
|
|
222
|
+
type: true,
|
|
223
|
+
status: true,
|
|
224
|
+
avatar_id: true,
|
|
225
|
+
created_at: true,
|
|
226
|
+
updated_at: true,
|
|
227
|
+
person_metadata: true,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
const enrichedCreatedPeople = await this.enrichPeople(createdPeople, allowCompanyRegistration);
|
|
231
|
+
const visibilityFilter = allowCompanyRegistration
|
|
232
|
+
? api_prisma_1.Prisma.empty
|
|
233
|
+
: api_prisma_1.Prisma.sql `AND p.type = 'individual'`;
|
|
234
|
+
const stageRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
235
|
+
SELECT
|
|
236
|
+
sh.to_stage,
|
|
237
|
+
sh.from_stage,
|
|
238
|
+
sh.changed_at,
|
|
239
|
+
sh.changed_by_user_id
|
|
240
|
+
FROM crm_stage_history sh
|
|
241
|
+
INNER JOIN person p ON p.id = sh.person_id
|
|
242
|
+
WHERE 1 = 1
|
|
243
|
+
${visibilityFilter}
|
|
244
|
+
AND sh.changed_at >= CAST(${startIso} AS TIMESTAMPTZ)
|
|
245
|
+
AND sh.changed_at <= CAST(${endIso} AS TIMESTAMPTZ)
|
|
246
|
+
`);
|
|
247
|
+
const activityRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
248
|
+
SELECT
|
|
249
|
+
a.type,
|
|
250
|
+
a.source_kind,
|
|
251
|
+
a.created_at,
|
|
252
|
+
a.completed_at,
|
|
253
|
+
a.owner_user_id,
|
|
254
|
+
a.created_by_user_id,
|
|
255
|
+
a.completed_by_user_id
|
|
256
|
+
FROM crm_activity a
|
|
257
|
+
INNER JOIN person p ON p.id = a.person_id
|
|
258
|
+
WHERE 1 = 1
|
|
259
|
+
${visibilityFilter}
|
|
260
|
+
AND (
|
|
261
|
+
(
|
|
262
|
+
a.created_at >= CAST(${startIso} AS TIMESTAMPTZ)
|
|
263
|
+
AND a.created_at <= CAST(${endIso} AS TIMESTAMPTZ)
|
|
264
|
+
)
|
|
265
|
+
OR (
|
|
266
|
+
a.source_kind = 'followup'
|
|
267
|
+
AND a.completed_at IS NOT NULL
|
|
268
|
+
AND a.completed_at >= CAST(${startIso} AS TIMESTAMPTZ)
|
|
269
|
+
AND a.completed_at <= CAST(${endIso} AS TIMESTAMPTZ)
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
`);
|
|
273
|
+
const summary = {
|
|
274
|
+
new_leads: enrichedCreatedPeople.length,
|
|
275
|
+
qualified_moves: 0,
|
|
276
|
+
customer_moves: 0,
|
|
277
|
+
lost_moves: 0,
|
|
278
|
+
interactions: 0,
|
|
279
|
+
followups_completed: 0,
|
|
280
|
+
conversion_rate: 0,
|
|
281
|
+
};
|
|
282
|
+
const sourceTotals = new Map(CRM_DASHBOARD_SOURCE_ORDER.map((key) => [key, 0]));
|
|
283
|
+
const currentStageTotals = new Map(CRM_DASHBOARD_STAGE_ORDER.map((key) => [key, 0]));
|
|
284
|
+
const activityTypeTotals = new Map(CRM_REPORT_ACTIVITY_ORDER.map((key) => [key, 0]));
|
|
285
|
+
const timelineByPeriod = new Map();
|
|
286
|
+
const ownersById = new Map();
|
|
287
|
+
for (const person of enrichedCreatedPeople) {
|
|
288
|
+
const source = ((_a = person.source) !== null && _a !== void 0 ? _a : 'other');
|
|
289
|
+
const lifecycleStage = ((_b = person.lifecycle_stage) !== null && _b !== void 0 ? _b : 'new');
|
|
290
|
+
sourceTotals.set(source, (sourceTotals.get(source) || 0) + 1);
|
|
291
|
+
currentStageTotals.set(lifecycleStage, (currentStageTotals.get(lifecycleStage) || 0) + 1);
|
|
292
|
+
const period = this.getReportPeriodKey(person.created_at, query.group_by);
|
|
293
|
+
if (!period) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const bucket = this.getOrCreateReportTimelineBucket(timelineByPeriod, period);
|
|
297
|
+
bucket.new_leads += 1;
|
|
298
|
+
}
|
|
299
|
+
for (const row of stageRows) {
|
|
300
|
+
if (row.from_stage === row.to_stage) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const period = this.getReportPeriodKey(row.changed_at, query.group_by);
|
|
304
|
+
if (!period) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const bucket = this.getOrCreateReportTimelineBucket(timelineByPeriod, period);
|
|
308
|
+
const ownerUserId = this.coerceNumber(row.changed_by_user_id);
|
|
309
|
+
if (row.to_stage === 'qualified') {
|
|
310
|
+
summary.qualified_moves += 1;
|
|
311
|
+
bucket.qualified_moves += 1;
|
|
312
|
+
}
|
|
313
|
+
if (row.to_stage === 'customer') {
|
|
314
|
+
summary.customer_moves += 1;
|
|
315
|
+
bucket.customer_moves += 1;
|
|
316
|
+
if (ownerUserId > 0) {
|
|
317
|
+
const current = ownersById.get(ownerUserId) ||
|
|
318
|
+
{
|
|
319
|
+
owner_user_id: ownerUserId,
|
|
320
|
+
owner_name: `#${ownerUserId}`,
|
|
321
|
+
interactions: 0,
|
|
322
|
+
followups_completed: 0,
|
|
323
|
+
customer_moves: 0,
|
|
324
|
+
};
|
|
325
|
+
current.customer_moves += 1;
|
|
326
|
+
ownersById.set(ownerUserId, current);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (row.to_stage === 'lost') {
|
|
330
|
+
summary.lost_moves += 1;
|
|
331
|
+
bucket.lost_moves += 1;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const row of activityRows) {
|
|
335
|
+
const createdInRange = this.isDateWithinRange(row.created_at, { start, end });
|
|
336
|
+
const completedInRange = row.completed_at != null && this.isDateWithinRange(row.completed_at, { start, end });
|
|
337
|
+
if (createdInRange && CRM_REPORT_ACTIVITY_ORDER.includes(row.type)) {
|
|
338
|
+
const type = row.type;
|
|
339
|
+
activityTypeTotals.set(type, (activityTypeTotals.get(type) || 0) + 1);
|
|
340
|
+
}
|
|
341
|
+
if (createdInRange && row.source_kind === 'interaction') {
|
|
342
|
+
summary.interactions += 1;
|
|
343
|
+
const period = this.getReportPeriodKey(row.created_at, query.group_by);
|
|
344
|
+
if (period) {
|
|
345
|
+
const bucket = this.getOrCreateReportTimelineBucket(timelineByPeriod, period);
|
|
346
|
+
bucket.interactions += 1;
|
|
347
|
+
}
|
|
348
|
+
const ownerUserId = this.coerceNumber(row.owner_user_id) ||
|
|
349
|
+
this.coerceNumber(row.completed_by_user_id) ||
|
|
350
|
+
this.coerceNumber(row.created_by_user_id);
|
|
351
|
+
if (ownerUserId > 0) {
|
|
352
|
+
const current = ownersById.get(ownerUserId) ||
|
|
353
|
+
{
|
|
354
|
+
owner_user_id: ownerUserId,
|
|
355
|
+
owner_name: `#${ownerUserId}`,
|
|
356
|
+
interactions: 0,
|
|
357
|
+
followups_completed: 0,
|
|
358
|
+
customer_moves: 0,
|
|
359
|
+
};
|
|
360
|
+
current.interactions += 1;
|
|
361
|
+
ownersById.set(ownerUserId, current);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (completedInRange && row.source_kind === 'followup') {
|
|
365
|
+
summary.followups_completed += 1;
|
|
366
|
+
const period = this.getReportPeriodKey(row.completed_at, query.group_by);
|
|
367
|
+
if (period) {
|
|
368
|
+
const bucket = this.getOrCreateReportTimelineBucket(timelineByPeriod, period);
|
|
369
|
+
bucket.followups_completed += 1;
|
|
370
|
+
}
|
|
371
|
+
const ownerUserId = this.coerceNumber(row.completed_by_user_id) ||
|
|
372
|
+
this.coerceNumber(row.owner_user_id) ||
|
|
373
|
+
this.coerceNumber(row.created_by_user_id);
|
|
374
|
+
if (ownerUserId > 0) {
|
|
375
|
+
const current = ownersById.get(ownerUserId) ||
|
|
376
|
+
{
|
|
377
|
+
owner_user_id: ownerUserId,
|
|
378
|
+
owner_name: `#${ownerUserId}`,
|
|
379
|
+
interactions: 0,
|
|
380
|
+
followups_completed: 0,
|
|
381
|
+
customer_moves: 0,
|
|
382
|
+
};
|
|
383
|
+
current.followups_completed += 1;
|
|
384
|
+
ownersById.set(ownerUserId, current);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
summary.conversion_rate =
|
|
389
|
+
summary.new_leads > 0 ? summary.customer_moves / summary.new_leads : 0;
|
|
390
|
+
const ownerIds = Array.from(ownersById.keys());
|
|
391
|
+
if (ownerIds.length > 0) {
|
|
392
|
+
const owners = await this.prismaService.user.findMany({
|
|
393
|
+
where: {
|
|
394
|
+
id: {
|
|
395
|
+
in: ownerIds,
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
select: {
|
|
399
|
+
id: true,
|
|
400
|
+
name: true,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
for (const owner of owners) {
|
|
404
|
+
const current = ownersById.get(owner.id);
|
|
405
|
+
if (current) {
|
|
406
|
+
current.owner_name = owner.name || `#${owner.id}`;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const timeline = Array.from(timelineByPeriod.keys())
|
|
411
|
+
.sort((left, right) => left.localeCompare(right))
|
|
412
|
+
.map((period) => {
|
|
413
|
+
const bucket = timelineByPeriod.get(period);
|
|
414
|
+
return Object.assign(Object.assign({}, bucket), { conversion_rate: bucket.new_leads > 0 ? bucket.customer_moves / bucket.new_leads : 0 });
|
|
415
|
+
});
|
|
416
|
+
return {
|
|
417
|
+
summary,
|
|
418
|
+
timeline,
|
|
419
|
+
breakdowns: {
|
|
420
|
+
source: CRM_DASHBOARD_SOURCE_ORDER.map((key) => ({
|
|
421
|
+
key,
|
|
422
|
+
total: sourceTotals.get(key) || 0,
|
|
423
|
+
})),
|
|
424
|
+
current_stage: CRM_DASHBOARD_STAGE_ORDER.map((key) => ({
|
|
425
|
+
key,
|
|
426
|
+
total: currentStageTotals.get(key) || 0,
|
|
427
|
+
})),
|
|
428
|
+
activity_type: CRM_REPORT_ACTIVITY_ORDER.map((key) => ({
|
|
429
|
+
key,
|
|
430
|
+
total: activityTypeTotals.get(key) || 0,
|
|
431
|
+
})),
|
|
432
|
+
},
|
|
433
|
+
owners: Array.from(ownersById.values()).sort((left, right) => left.owner_name.localeCompare(right.owner_name)),
|
|
434
|
+
table: timeline,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
83
437
|
async getOwnerOptions(currentUserId) {
|
|
84
438
|
const where = {
|
|
85
439
|
OR: [
|
|
@@ -164,13 +518,13 @@ let PersonService = class PersonService {
|
|
|
164
518
|
const excludedPersonFilter = excludedPersonId > 0
|
|
165
519
|
? api_prisma_1.Prisma.sql ` AND c.person_id <> ${excludedPersonId}`
|
|
166
520
|
: api_prisma_1.Prisma.empty;
|
|
167
|
-
const phoneRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
168
|
-
SELECT DISTINCT c.person_id
|
|
169
|
-
FROM contact c
|
|
170
|
-
JOIN contact_type ct ON ct.id = c.contact_type_id
|
|
171
|
-
WHERE UPPER(ct.code) IN ('PHONE', 'MOBILE', 'WHATSAPP')
|
|
172
|
-
AND regexp_replace(COALESCE(c.value, ''), '[^0-9]+', '', 'g') = ${normalizedPhone}
|
|
173
|
-
${excludedPersonFilter}
|
|
521
|
+
const phoneRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
522
|
+
SELECT DISTINCT c.person_id
|
|
523
|
+
FROM contact c
|
|
524
|
+
JOIN contact_type ct ON ct.id = c.contact_type_id
|
|
525
|
+
WHERE UPPER(ct.code) IN ('PHONE', 'MOBILE', 'WHATSAPP')
|
|
526
|
+
AND regexp_replace(COALESCE(c.value, ''), '[^0-9]+', '', 'g') = ${normalizedPhone}
|
|
527
|
+
${excludedPersonFilter}
|
|
174
528
|
`);
|
|
175
529
|
for (const row of phoneRows) {
|
|
176
530
|
this.addDuplicateReason(reasonsByPersonId, row.person_id, 'phone');
|
|
@@ -183,12 +537,12 @@ let PersonService = class PersonService {
|
|
|
183
537
|
const documentTypeFilter = documentTypeId > 0
|
|
184
538
|
? api_prisma_1.Prisma.sql ` AND d.document_type_id = ${documentTypeId}`
|
|
185
539
|
: api_prisma_1.Prisma.empty;
|
|
186
|
-
const documentRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
187
|
-
SELECT DISTINCT d.person_id
|
|
188
|
-
FROM document d
|
|
189
|
-
WHERE regexp_replace(COALESCE(d.value, ''), '[^0-9]+', '', 'g') = ${normalizedDocument}
|
|
190
|
-
${excludedPersonFilter}
|
|
191
|
-
${documentTypeFilter}
|
|
540
|
+
const documentRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
541
|
+
SELECT DISTINCT d.person_id
|
|
542
|
+
FROM document d
|
|
543
|
+
WHERE regexp_replace(COALESCE(d.value, ''), '[^0-9]+', '', 'g') = ${normalizedDocument}
|
|
544
|
+
${excludedPersonFilter}
|
|
545
|
+
${documentTypeFilter}
|
|
192
546
|
`);
|
|
193
547
|
for (const row of documentRows) {
|
|
194
548
|
this.addDuplicateReason(reasonsByPersonId, row.person_id, 'document');
|
|
@@ -347,6 +701,517 @@ let PersonService = class PersonService {
|
|
|
347
701
|
const enriched = await this.enrichPeople(result.data, allowCompanyRegistration);
|
|
348
702
|
return Object.assign(Object.assign({}, result), { data: enriched });
|
|
349
703
|
}
|
|
704
|
+
async listAccounts(paginationParams) {
|
|
705
|
+
var _a;
|
|
706
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
707
|
+
const page = Math.max(Number(paginationParams.page || 1), 1);
|
|
708
|
+
const pageSize = Math.max(Number(paginationParams.pageSize || 12), 1);
|
|
709
|
+
const skip = (page - 1) * pageSize;
|
|
710
|
+
if (!allowCompanyRegistration) {
|
|
711
|
+
return this.createEmptyAccountPagination(page, pageSize);
|
|
712
|
+
}
|
|
713
|
+
const search = this.normalizeTextOrNull(paginationParams.search);
|
|
714
|
+
const filters = this.buildAccountSqlFilters({
|
|
715
|
+
search,
|
|
716
|
+
status: paginationParams.status,
|
|
717
|
+
lifecycleStage: paginationParams.lifecycle_stage,
|
|
718
|
+
});
|
|
719
|
+
const orderBy = this.getAccountOrderBySql(paginationParams.sortField, paginationParams.sortOrder);
|
|
720
|
+
const totalRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
721
|
+
SELECT COUNT(*) AS total
|
|
722
|
+
FROM person p
|
|
723
|
+
INNER JOIN person_company pc ON pc.id = p.id
|
|
724
|
+
WHERE p.type = 'company'
|
|
725
|
+
${filters}
|
|
726
|
+
`);
|
|
727
|
+
const total = this.coerceCount((_a = totalRows[0]) === null || _a === void 0 ? void 0 : _a.total);
|
|
728
|
+
if (total === 0) {
|
|
729
|
+
return this.createEmptyAccountPagination(page, pageSize);
|
|
730
|
+
}
|
|
731
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
732
|
+
SELECT p.id AS person_id
|
|
733
|
+
FROM person p
|
|
734
|
+
INNER JOIN person_company pc ON pc.id = p.id
|
|
735
|
+
WHERE p.type = 'company'
|
|
736
|
+
${filters}
|
|
737
|
+
ORDER BY ${orderBy}, p.id ASC
|
|
738
|
+
LIMIT ${pageSize}
|
|
739
|
+
OFFSET ${skip}
|
|
740
|
+
`);
|
|
741
|
+
const personIds = rows.map((row) => row.person_id).filter((id) => id > 0);
|
|
742
|
+
if (personIds.length === 0) {
|
|
743
|
+
return this.createEmptyAccountPagination(page, pageSize);
|
|
744
|
+
}
|
|
745
|
+
const people = await this.loadAccountPeopleByIds(personIds);
|
|
746
|
+
const companies = await this.prismaService.person_company.findMany({
|
|
747
|
+
where: {
|
|
748
|
+
id: {
|
|
749
|
+
in: personIds,
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
const personById = new Map(people.map((item) => [item.id, item]));
|
|
754
|
+
const companyById = new Map(companies.map((item) => [item.id, item]));
|
|
755
|
+
const data = rows
|
|
756
|
+
.map((row) => {
|
|
757
|
+
const person = personById.get(row.person_id);
|
|
758
|
+
const company = companyById.get(row.person_id);
|
|
759
|
+
if (!person || !company) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
return this.mapAccountFromPerson(person, company);
|
|
763
|
+
})
|
|
764
|
+
.filter((item) => item != null);
|
|
765
|
+
const lastPage = Math.max(1, Math.ceil(total / pageSize));
|
|
766
|
+
return {
|
|
767
|
+
total,
|
|
768
|
+
lastPage,
|
|
769
|
+
page,
|
|
770
|
+
pageSize,
|
|
771
|
+
prev: page > 1 ? page - 1 : null,
|
|
772
|
+
next: page < lastPage ? page + 1 : null,
|
|
773
|
+
data,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
async getAccountStats() {
|
|
777
|
+
var _a, _b, _c, _d;
|
|
778
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
779
|
+
if (!allowCompanyRegistration) {
|
|
780
|
+
return {
|
|
781
|
+
total: 0,
|
|
782
|
+
active: 0,
|
|
783
|
+
customers: 0,
|
|
784
|
+
prospects: 0,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
788
|
+
SELECT
|
|
789
|
+
COUNT(*) AS total,
|
|
790
|
+
COUNT(*) FILTER (WHERE p.status = 'active') AS active,
|
|
791
|
+
COUNT(*) FILTER (WHERE pc.account_lifecycle_stage = 'customer') AS customers,
|
|
792
|
+
COUNT(*) FILTER (WHERE pc.account_lifecycle_stage = 'prospect') AS prospects
|
|
793
|
+
FROM person p
|
|
794
|
+
INNER JOIN person_company pc ON pc.id = p.id
|
|
795
|
+
WHERE p.type = 'company'
|
|
796
|
+
`);
|
|
797
|
+
return {
|
|
798
|
+
total: this.coerceCount((_a = rows[0]) === null || _a === void 0 ? void 0 : _a.total),
|
|
799
|
+
active: this.coerceCount((_b = rows[0]) === null || _b === void 0 ? void 0 : _b.active),
|
|
800
|
+
customers: this.coerceCount((_c = rows[0]) === null || _c === void 0 ? void 0 : _c.customers),
|
|
801
|
+
prospects: this.coerceCount((_d = rows[0]) === null || _d === void 0 ? void 0 : _d.prospects),
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
async createAccount(data, locale) {
|
|
805
|
+
await this.ensureCompanyRegistrationAllowed({
|
|
806
|
+
nextType: 'company',
|
|
807
|
+
locale,
|
|
808
|
+
});
|
|
809
|
+
const normalizedName = this.normalizeTextOrNull(data.name);
|
|
810
|
+
if (!normalizedName) {
|
|
811
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.nameMustBeString', locale, 'Name is required.'));
|
|
812
|
+
}
|
|
813
|
+
return this.prismaService.$transaction(async (tx) => {
|
|
814
|
+
const person = await tx.person.create({
|
|
815
|
+
data: {
|
|
816
|
+
name: normalizedName,
|
|
817
|
+
type: 'company',
|
|
818
|
+
status: data.status,
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
await this.syncPersonSubtypeData(tx, person.id, null, Object.assign(Object.assign({}, data), { type: 'company', name: normalizedName }), locale);
|
|
822
|
+
await this.syncPersonMetadata(tx, person.id, {
|
|
823
|
+
owner_user_id: data.owner_user_id,
|
|
824
|
+
});
|
|
825
|
+
await this.upsertPrimaryAccountContact(tx, person.id, 'EMAIL', data.email);
|
|
826
|
+
await this.upsertPrimaryAccountContact(tx, person.id, 'PHONE', data.phone);
|
|
827
|
+
return {
|
|
828
|
+
success: true,
|
|
829
|
+
id: person.id,
|
|
830
|
+
};
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
async updateAccount(id, data, locale) {
|
|
834
|
+
var _a;
|
|
835
|
+
await this.ensureCompanyRegistrationAllowed({
|
|
836
|
+
currentType: 'company',
|
|
837
|
+
nextType: 'company',
|
|
838
|
+
locale,
|
|
839
|
+
});
|
|
840
|
+
const person = await this.ensureCompanyAccountAccessible(id, locale);
|
|
841
|
+
const nextName = (_a = this.normalizeTextOrNull(data.name)) !== null && _a !== void 0 ? _a : person.name;
|
|
842
|
+
return this.prismaService.$transaction(async (tx) => {
|
|
843
|
+
var _a;
|
|
844
|
+
await tx.person.update({
|
|
845
|
+
where: { id },
|
|
846
|
+
data: {
|
|
847
|
+
name: nextName,
|
|
848
|
+
type: 'company',
|
|
849
|
+
status: (_a = data.status) !== null && _a !== void 0 ? _a : person.status,
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
await this.syncPersonSubtypeData(tx, id, 'company', Object.assign(Object.assign({}, data), { type: 'company', name: nextName }), locale);
|
|
853
|
+
await this.syncPersonMetadata(tx, id, {
|
|
854
|
+
owner_user_id: data.owner_user_id,
|
|
855
|
+
});
|
|
856
|
+
await this.upsertPrimaryAccountContact(tx, id, 'EMAIL', data.email);
|
|
857
|
+
await this.upsertPrimaryAccountContact(tx, id, 'PHONE', data.phone);
|
|
858
|
+
return {
|
|
859
|
+
success: true,
|
|
860
|
+
id,
|
|
861
|
+
};
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
async deleteAccounts({ ids }, locale) {
|
|
865
|
+
if (ids == undefined || ids == null) {
|
|
866
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('deleteItemsRequired', locale, 'You must select at least one item to delete.'));
|
|
867
|
+
}
|
|
868
|
+
const companies = await this.prismaService.person.findMany({
|
|
869
|
+
where: {
|
|
870
|
+
id: {
|
|
871
|
+
in: ids,
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
select: {
|
|
875
|
+
id: true,
|
|
876
|
+
type: true,
|
|
877
|
+
},
|
|
878
|
+
});
|
|
879
|
+
const existingIds = companies.map((item) => item.id);
|
|
880
|
+
const missingIds = ids.filter((itemId) => !existingIds.includes(itemId));
|
|
881
|
+
if (missingIds.length > 0) {
|
|
882
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person(s) with ID(s) ${missingIds.join(', ')} not found.`));
|
|
883
|
+
}
|
|
884
|
+
const invalidIds = companies
|
|
885
|
+
.filter((item) => item.type !== 'company')
|
|
886
|
+
.map((item) => item.id);
|
|
887
|
+
if (invalidIds.length > 0) {
|
|
888
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('companyRelationInvalidTarget', locale, 'Only company records can be managed as accounts.'));
|
|
889
|
+
}
|
|
890
|
+
return this.delete({ ids }, locale);
|
|
891
|
+
}
|
|
892
|
+
async listActivities(paginationParams) {
|
|
893
|
+
var _a;
|
|
894
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
895
|
+
const page = Math.max(Number(paginationParams.page || 1), 1);
|
|
896
|
+
const pageSize = Math.max(Number(paginationParams.pageSize || 12), 1);
|
|
897
|
+
const skip = (page - 1) * pageSize;
|
|
898
|
+
const filters = this.buildCrmActivitySqlFilters({
|
|
899
|
+
allowCompanyRegistration,
|
|
900
|
+
search: this.normalizeTextOrNull(paginationParams.search),
|
|
901
|
+
status: paginationParams.status,
|
|
902
|
+
type: paginationParams.type,
|
|
903
|
+
priority: paginationParams.priority,
|
|
904
|
+
});
|
|
905
|
+
const totalRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
906
|
+
SELECT COUNT(*) AS total
|
|
907
|
+
FROM crm_activity a
|
|
908
|
+
INNER JOIN person p ON p.id = a.person_id
|
|
909
|
+
LEFT JOIN "user" owner_user ON owner_user.id = a.owner_user_id
|
|
910
|
+
WHERE 1 = 1
|
|
911
|
+
${filters}
|
|
912
|
+
`);
|
|
913
|
+
const total = this.coerceCount((_a = totalRows[0]) === null || _a === void 0 ? void 0 : _a.total);
|
|
914
|
+
if (total === 0) {
|
|
915
|
+
return this.createEmptyCrmActivityPagination(page, pageSize);
|
|
916
|
+
}
|
|
917
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
918
|
+
SELECT
|
|
919
|
+
a.id,
|
|
920
|
+
a.person_id,
|
|
921
|
+
a.owner_user_id,
|
|
922
|
+
a.type,
|
|
923
|
+
a.subject,
|
|
924
|
+
a.notes,
|
|
925
|
+
a.due_at,
|
|
926
|
+
a.completed_at,
|
|
927
|
+
a.created_at,
|
|
928
|
+
a.priority,
|
|
929
|
+
p.name AS person_name,
|
|
930
|
+
p.type AS person_type,
|
|
931
|
+
p.status AS person_status,
|
|
932
|
+
pc.trade_name AS person_trade_name,
|
|
933
|
+
owner_user.name AS owner_user_name
|
|
934
|
+
FROM crm_activity a
|
|
935
|
+
INNER JOIN person p ON p.id = a.person_id
|
|
936
|
+
LEFT JOIN person_company pc ON pc.id = p.id
|
|
937
|
+
LEFT JOIN "user" owner_user ON owner_user.id = a.owner_user_id
|
|
938
|
+
WHERE 1 = 1
|
|
939
|
+
${filters}
|
|
940
|
+
ORDER BY a.due_at ASC, a.id ASC
|
|
941
|
+
LIMIT ${pageSize}
|
|
942
|
+
OFFSET ${skip}
|
|
943
|
+
`);
|
|
944
|
+
const data = rows.map((row) => this.mapCrmActivityListRow(row));
|
|
945
|
+
const lastPage = Math.max(1, Math.ceil(total / pageSize));
|
|
946
|
+
return {
|
|
947
|
+
total,
|
|
948
|
+
lastPage,
|
|
949
|
+
page,
|
|
950
|
+
pageSize,
|
|
951
|
+
prev: page > 1 ? page - 1 : null,
|
|
952
|
+
next: page < lastPage ? page + 1 : null,
|
|
953
|
+
data,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
async getActivityStats() {
|
|
957
|
+
var _a, _b, _c, _d;
|
|
958
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
959
|
+
const visibilityFilter = allowCompanyRegistration
|
|
960
|
+
? api_prisma_1.Prisma.empty
|
|
961
|
+
: api_prisma_1.Prisma.sql `AND p.type = 'individual'`;
|
|
962
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
963
|
+
SELECT
|
|
964
|
+
COUNT(*) AS total,
|
|
965
|
+
COUNT(*) FILTER (
|
|
966
|
+
WHERE a.completed_at IS NULL
|
|
967
|
+
AND a.due_at >= NOW()
|
|
968
|
+
) AS pending,
|
|
969
|
+
COUNT(*) FILTER (
|
|
970
|
+
WHERE a.completed_at IS NULL
|
|
971
|
+
AND a.due_at < NOW()
|
|
972
|
+
) AS overdue,
|
|
973
|
+
COUNT(*) FILTER (
|
|
974
|
+
WHERE a.completed_at IS NOT NULL
|
|
975
|
+
) AS completed
|
|
976
|
+
FROM crm_activity a
|
|
977
|
+
INNER JOIN person p ON p.id = a.person_id
|
|
978
|
+
WHERE 1 = 1
|
|
979
|
+
${visibilityFilter}
|
|
980
|
+
`);
|
|
981
|
+
return {
|
|
982
|
+
total: this.coerceCount((_a = rows[0]) === null || _a === void 0 ? void 0 : _a.total),
|
|
983
|
+
pending: this.coerceCount((_b = rows[0]) === null || _b === void 0 ? void 0 : _b.pending),
|
|
984
|
+
overdue: this.coerceCount((_c = rows[0]) === null || _c === void 0 ? void 0 : _c.overdue),
|
|
985
|
+
completed: this.coerceCount((_d = rows[0]) === null || _d === void 0 ? void 0 : _d.completed),
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
async getActivity(id, locale) {
|
|
989
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
990
|
+
const visibilityFilter = allowCompanyRegistration
|
|
991
|
+
? api_prisma_1.Prisma.empty
|
|
992
|
+
: api_prisma_1.Prisma.sql `AND p.type = 'individual'`;
|
|
993
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
994
|
+
SELECT
|
|
995
|
+
a.id,
|
|
996
|
+
a.person_id,
|
|
997
|
+
a.owner_user_id,
|
|
998
|
+
a.created_by_user_id,
|
|
999
|
+
a.completed_by_user_id,
|
|
1000
|
+
a.type,
|
|
1001
|
+
a.subject,
|
|
1002
|
+
a.notes,
|
|
1003
|
+
a.due_at,
|
|
1004
|
+
a.completed_at,
|
|
1005
|
+
a.created_at,
|
|
1006
|
+
a.priority,
|
|
1007
|
+
a.source_kind,
|
|
1008
|
+
p.name AS person_name,
|
|
1009
|
+
p.type AS person_type,
|
|
1010
|
+
p.status AS person_status,
|
|
1011
|
+
pc.trade_name AS person_trade_name,
|
|
1012
|
+
owner_user.name AS owner_user_name,
|
|
1013
|
+
created_by_user.name AS created_by_user_name,
|
|
1014
|
+
completed_by_user.name AS completed_by_user_name
|
|
1015
|
+
FROM crm_activity a
|
|
1016
|
+
INNER JOIN person p ON p.id = a.person_id
|
|
1017
|
+
LEFT JOIN person_company pc ON pc.id = p.id
|
|
1018
|
+
LEFT JOIN "user" owner_user ON owner_user.id = a.owner_user_id
|
|
1019
|
+
LEFT JOIN "user" created_by_user ON created_by_user.id = a.created_by_user_id
|
|
1020
|
+
LEFT JOIN "user" completed_by_user ON completed_by_user.id = a.completed_by_user_id
|
|
1021
|
+
WHERE a.id = ${id}
|
|
1022
|
+
${visibilityFilter}
|
|
1023
|
+
LIMIT 1
|
|
1024
|
+
`);
|
|
1025
|
+
const row = rows[0];
|
|
1026
|
+
if (!row) {
|
|
1027
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Activity with ID ${id} not found`));
|
|
1028
|
+
}
|
|
1029
|
+
return this.mapCrmActivityDetailRow(row);
|
|
1030
|
+
}
|
|
1031
|
+
async completeActivity(id, locale, user) {
|
|
1032
|
+
const actorUserId = Number((user === null || user === void 0 ? void 0 : user.id) || 0) || null;
|
|
1033
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
1034
|
+
const visibilityFilter = allowCompanyRegistration
|
|
1035
|
+
? api_prisma_1.Prisma.empty
|
|
1036
|
+
: api_prisma_1.Prisma.sql `AND p.type = 'individual'`;
|
|
1037
|
+
return this.prismaService.$transaction(async (tx) => {
|
|
1038
|
+
const rows = (await tx.$queryRaw(api_prisma_1.Prisma.sql `
|
|
1039
|
+
SELECT
|
|
1040
|
+
a.id,
|
|
1041
|
+
a.person_id,
|
|
1042
|
+
a.completed_at,
|
|
1043
|
+
a.source_kind
|
|
1044
|
+
FROM crm_activity a
|
|
1045
|
+
INNER JOIN person p ON p.id = a.person_id
|
|
1046
|
+
WHERE a.id = ${id}
|
|
1047
|
+
${visibilityFilter}
|
|
1048
|
+
LIMIT 1
|
|
1049
|
+
`));
|
|
1050
|
+
const activity = rows[0];
|
|
1051
|
+
if (!activity) {
|
|
1052
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Activity with ID ${id} not found`));
|
|
1053
|
+
}
|
|
1054
|
+
if (activity.completed_at) {
|
|
1055
|
+
return {
|
|
1056
|
+
success: true,
|
|
1057
|
+
completed_at: activity.completed_at.toISOString(),
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
const completedAt = new Date();
|
|
1061
|
+
await tx.$executeRaw(api_prisma_1.Prisma.sql `
|
|
1062
|
+
UPDATE crm_activity
|
|
1063
|
+
SET
|
|
1064
|
+
completed_at = CAST(${completedAt.toISOString()} AS TIMESTAMPTZ),
|
|
1065
|
+
completed_by_user_id = ${actorUserId},
|
|
1066
|
+
updated_at = NOW()
|
|
1067
|
+
WHERE id = ${id}
|
|
1068
|
+
`);
|
|
1069
|
+
if (activity.source_kind === 'followup') {
|
|
1070
|
+
await this.upsertMetadataValue(tx, activity.person_id, NEXT_ACTION_AT_METADATA_KEY, null);
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
success: true,
|
|
1074
|
+
completed_at: completedAt.toISOString(),
|
|
1075
|
+
};
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
async listFollowups(paginationParams, _currentUserId) {
|
|
1079
|
+
var _a;
|
|
1080
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
1081
|
+
const page = Math.max(Number(paginationParams.page || 1), 1);
|
|
1082
|
+
const pageSize = Math.max(Number(paginationParams.pageSize || 12), 1);
|
|
1083
|
+
const skip = (page - 1) * pageSize;
|
|
1084
|
+
const searchPersonIds = await this.findFollowupSearchPersonIds(paginationParams.search, allowCompanyRegistration);
|
|
1085
|
+
if (searchPersonIds && searchPersonIds.length === 0) {
|
|
1086
|
+
return this.createEmptyFollowupPagination(page, pageSize);
|
|
1087
|
+
}
|
|
1088
|
+
const filters = this.buildFollowupSqlFilters({
|
|
1089
|
+
allowCompanyRegistration,
|
|
1090
|
+
searchPersonIds,
|
|
1091
|
+
status: paginationParams.status,
|
|
1092
|
+
dateFrom: paginationParams.date_from,
|
|
1093
|
+
dateTo: paginationParams.date_to,
|
|
1094
|
+
});
|
|
1095
|
+
const followupTimestampSql = this.getFollowupTimestampSql();
|
|
1096
|
+
const totalRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
1097
|
+
SELECT COUNT(*) AS total
|
|
1098
|
+
FROM person p
|
|
1099
|
+
INNER JOIN person_metadata pm_next
|
|
1100
|
+
ON pm_next.person_id = p.id
|
|
1101
|
+
AND pm_next.key = ${NEXT_ACTION_AT_METADATA_KEY}
|
|
1102
|
+
WHERE 1 = 1
|
|
1103
|
+
${filters}
|
|
1104
|
+
`);
|
|
1105
|
+
const total = this.coerceCount((_a = totalRows[0]) === null || _a === void 0 ? void 0 : _a.total);
|
|
1106
|
+
if (total === 0) {
|
|
1107
|
+
return this.createEmptyFollowupPagination(page, pageSize);
|
|
1108
|
+
}
|
|
1109
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
1110
|
+
SELECT p.id AS person_id
|
|
1111
|
+
FROM person p
|
|
1112
|
+
INNER JOIN person_metadata pm_next
|
|
1113
|
+
ON pm_next.person_id = p.id
|
|
1114
|
+
AND pm_next.key = ${NEXT_ACTION_AT_METADATA_KEY}
|
|
1115
|
+
WHERE 1 = 1
|
|
1116
|
+
${filters}
|
|
1117
|
+
ORDER BY ${followupTimestampSql} ASC, p.id ASC
|
|
1118
|
+
LIMIT ${pageSize}
|
|
1119
|
+
OFFSET ${skip}
|
|
1120
|
+
`);
|
|
1121
|
+
const personIds = rows.map((row) => row.person_id).filter((id) => id > 0);
|
|
1122
|
+
const people = personIds.length > 0
|
|
1123
|
+
? await this.prismaService.person.findMany({
|
|
1124
|
+
where: {
|
|
1125
|
+
id: {
|
|
1126
|
+
in: personIds,
|
|
1127
|
+
},
|
|
1128
|
+
},
|
|
1129
|
+
include: {
|
|
1130
|
+
person_address: {
|
|
1131
|
+
include: {
|
|
1132
|
+
address: true,
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
contact: true,
|
|
1136
|
+
document: true,
|
|
1137
|
+
person_metadata: true,
|
|
1138
|
+
},
|
|
1139
|
+
})
|
|
1140
|
+
: [];
|
|
1141
|
+
const enrichedPeople = await this.enrichPeople(people, allowCompanyRegistration);
|
|
1142
|
+
const personById = new Map(enrichedPeople.map((person) => [person.id, person]));
|
|
1143
|
+
const data = rows
|
|
1144
|
+
.map((row) => {
|
|
1145
|
+
var _a;
|
|
1146
|
+
const person = personById.get(row.person_id);
|
|
1147
|
+
const nextActionAt = this.normalizeDateTimeOrNull(person === null || person === void 0 ? void 0 : person.next_action_at);
|
|
1148
|
+
if (!person || !nextActionAt) {
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
return {
|
|
1152
|
+
person,
|
|
1153
|
+
next_action_at: nextActionAt,
|
|
1154
|
+
last_interaction_at: (_a = this.normalizeDateTimeOrNull(person.last_interaction_at)) !== null && _a !== void 0 ? _a : null,
|
|
1155
|
+
status: this.getFollowupStatus(nextActionAt),
|
|
1156
|
+
};
|
|
1157
|
+
})
|
|
1158
|
+
.filter((item) => item != null);
|
|
1159
|
+
const lastPage = Math.max(1, Math.ceil(total / pageSize));
|
|
1160
|
+
return {
|
|
1161
|
+
total,
|
|
1162
|
+
lastPage,
|
|
1163
|
+
page,
|
|
1164
|
+
pageSize,
|
|
1165
|
+
prev: page > 1 ? page - 1 : null,
|
|
1166
|
+
next: page < lastPage ? page + 1 : null,
|
|
1167
|
+
data,
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
async getFollowupStats(query, _currentUserId) {
|
|
1171
|
+
var _a, _b, _c, _d;
|
|
1172
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
1173
|
+
const searchPersonIds = await this.findFollowupSearchPersonIds(query.search, allowCompanyRegistration);
|
|
1174
|
+
if (searchPersonIds && searchPersonIds.length === 0) {
|
|
1175
|
+
return {
|
|
1176
|
+
total: 0,
|
|
1177
|
+
today: 0,
|
|
1178
|
+
overdue: 0,
|
|
1179
|
+
upcoming: 0,
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
const filters = this.buildFollowupSqlFilters({
|
|
1183
|
+
allowCompanyRegistration,
|
|
1184
|
+
searchPersonIds,
|
|
1185
|
+
});
|
|
1186
|
+
const followupTimestampSql = this.getFollowupTimestampSql();
|
|
1187
|
+
const { todayStartIso, tomorrowStartIso } = this.getFollowupDayBoundaryIsoStrings();
|
|
1188
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
1189
|
+
SELECT
|
|
1190
|
+
COUNT(*) AS total,
|
|
1191
|
+
COUNT(*) FILTER (
|
|
1192
|
+
WHERE ${followupTimestampSql} >= CAST(${todayStartIso} AS TIMESTAMPTZ)
|
|
1193
|
+
AND ${followupTimestampSql} < CAST(${tomorrowStartIso} AS TIMESTAMPTZ)
|
|
1194
|
+
) AS today,
|
|
1195
|
+
COUNT(*) FILTER (
|
|
1196
|
+
WHERE ${followupTimestampSql} < CAST(${todayStartIso} AS TIMESTAMPTZ)
|
|
1197
|
+
) AS overdue,
|
|
1198
|
+
COUNT(*) FILTER (
|
|
1199
|
+
WHERE ${followupTimestampSql} >= CAST(${tomorrowStartIso} AS TIMESTAMPTZ)
|
|
1200
|
+
) AS upcoming
|
|
1201
|
+
FROM person p
|
|
1202
|
+
INNER JOIN person_metadata pm_next
|
|
1203
|
+
ON pm_next.person_id = p.id
|
|
1204
|
+
AND pm_next.key = ${NEXT_ACTION_AT_METADATA_KEY}
|
|
1205
|
+
WHERE 1 = 1
|
|
1206
|
+
${filters}
|
|
1207
|
+
`);
|
|
1208
|
+
return {
|
|
1209
|
+
total: this.coerceCount((_a = rows[0]) === null || _a === void 0 ? void 0 : _a.total),
|
|
1210
|
+
today: this.coerceCount((_b = rows[0]) === null || _b === void 0 ? void 0 : _b.today),
|
|
1211
|
+
overdue: this.coerceCount((_c = rows[0]) === null || _c === void 0 ? void 0 : _c.overdue),
|
|
1212
|
+
upcoming: this.coerceCount((_d = rows[0]) === null || _d === void 0 ? void 0 : _d.upcoming),
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
350
1215
|
async get(locale, id) {
|
|
351
1216
|
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
352
1217
|
const person = await this.prismaService.person.findUnique({
|
|
@@ -387,6 +1252,7 @@ let PersonService = class PersonService {
|
|
|
387
1252
|
async createInteraction(id, data, locale, user) {
|
|
388
1253
|
const person = await this.ensurePersonAccessible(id, locale);
|
|
389
1254
|
const interaction = this.buildInteractionRecord(data, user);
|
|
1255
|
+
const ownerUserId = await this.getPersonOwnerUserId(person.id);
|
|
390
1256
|
await this.prismaService.$transaction(async (tx) => {
|
|
391
1257
|
const currentInteractions = await this.loadInteractionsFromTx(tx, person.id);
|
|
392
1258
|
const nextInteractions = this.sortInteractions([
|
|
@@ -395,12 +1261,19 @@ let PersonService = class PersonService {
|
|
|
395
1261
|
]);
|
|
396
1262
|
await this.upsertMetadataValue(tx, person.id, INTERACTIONS_METADATA_KEY, nextInteractions);
|
|
397
1263
|
await this.upsertMetadataValue(tx, person.id, LAST_INTERACTION_AT_METADATA_KEY, interaction.created_at);
|
|
1264
|
+
await this.createCompletedInteractionActivity(tx, {
|
|
1265
|
+
personId: person.id,
|
|
1266
|
+
ownerUserId,
|
|
1267
|
+
interaction,
|
|
1268
|
+
actorUserId: Number((user === null || user === void 0 ? void 0 : user.id) || 0) || null,
|
|
1269
|
+
});
|
|
398
1270
|
});
|
|
399
1271
|
return interaction;
|
|
400
1272
|
}
|
|
401
1273
|
async scheduleFollowup(id, data, locale, user) {
|
|
402
1274
|
const person = await this.ensurePersonAccessible(id, locale);
|
|
403
1275
|
const normalizedNextActionAt = this.normalizeDateTimeOrNull(data.next_action_at);
|
|
1276
|
+
const ownerUserId = await this.getPersonOwnerUserId(person.id);
|
|
404
1277
|
if (!normalizedNextActionAt) {
|
|
405
1278
|
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.dateMustBeString', locale, 'next_action_at must be a valid datetime.'));
|
|
406
1279
|
}
|
|
@@ -420,16 +1293,34 @@ let PersonService = class PersonService {
|
|
|
420
1293
|
await this.upsertMetadataValue(tx, person.id, INTERACTIONS_METADATA_KEY, nextInteractions);
|
|
421
1294
|
await this.upsertMetadataValue(tx, person.id, LAST_INTERACTION_AT_METADATA_KEY, interaction.created_at);
|
|
422
1295
|
}
|
|
1296
|
+
await this.upsertFollowupActivity(tx, {
|
|
1297
|
+
personId: person.id,
|
|
1298
|
+
ownerUserId,
|
|
1299
|
+
dueAt: normalizedNextActionAt,
|
|
1300
|
+
notes: data.notes,
|
|
1301
|
+
actorUserId: Number((user === null || user === void 0 ? void 0 : user.id) || 0) || null,
|
|
1302
|
+
});
|
|
423
1303
|
});
|
|
424
1304
|
return {
|
|
425
1305
|
success: true,
|
|
426
1306
|
next_action_at: normalizedNextActionAt,
|
|
427
1307
|
};
|
|
428
1308
|
}
|
|
429
|
-
async updateLifecycleStage(id, data, locale) {
|
|
1309
|
+
async updateLifecycleStage(id, data, locale, user) {
|
|
430
1310
|
const person = await this.ensurePersonAccessible(id, locale);
|
|
1311
|
+
const currentLifecycleStage = await this.getPersonLifecycleStage(person.id);
|
|
1312
|
+
const nextLifecycleStage = this.normalizeTextOrNull(data.lifecycle_stage);
|
|
1313
|
+
const actorUserId = this.coerceNumber(user === null || user === void 0 ? void 0 : user.id) || null;
|
|
431
1314
|
await this.prismaService.$transaction(async (tx) => {
|
|
432
1315
|
await this.upsertMetadataValue(tx, person.id, LIFECYCLE_STAGE_METADATA_KEY, data.lifecycle_stage);
|
|
1316
|
+
if (nextLifecycleStage && currentLifecycleStage !== nextLifecycleStage) {
|
|
1317
|
+
await this.registerStageTransition(tx, {
|
|
1318
|
+
personId: person.id,
|
|
1319
|
+
fromStage: currentLifecycleStage,
|
|
1320
|
+
toStage: nextLifecycleStage,
|
|
1321
|
+
changedByUserId: actorUserId,
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
433
1324
|
});
|
|
434
1325
|
return {
|
|
435
1326
|
success: true,
|
|
@@ -469,7 +1360,7 @@ let PersonService = class PersonService {
|
|
|
469
1360
|
return person;
|
|
470
1361
|
});
|
|
471
1362
|
}
|
|
472
|
-
async update(id, data, locale) {
|
|
1363
|
+
async update(id, data, locale, user) {
|
|
473
1364
|
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
474
1365
|
const person = await this.prismaService.person.findUnique({ where: { id } });
|
|
475
1366
|
if (!person) {
|
|
@@ -483,6 +1374,8 @@ let PersonService = class PersonService {
|
|
|
483
1374
|
nextType: data.type,
|
|
484
1375
|
locale,
|
|
485
1376
|
});
|
|
1377
|
+
const currentLifecycleStage = await this.getPersonLifecycleStage(id);
|
|
1378
|
+
const nextLifecycleStage = this.normalizeTextOrNull(data.lifecycle_stage);
|
|
486
1379
|
const incomingContacts = Array.isArray(data.contacts) ? data.contacts : [];
|
|
487
1380
|
const incomingAddresses = Array.isArray(data.addresses) ? data.addresses : [];
|
|
488
1381
|
const incomingDocuments = Array.isArray(data.documents) ? data.documents : [];
|
|
@@ -521,6 +1414,14 @@ let PersonService = class PersonService {
|
|
|
521
1414
|
await this.syncContacts(tx, id, incomingContacts);
|
|
522
1415
|
await this.syncAddresses(tx, id, incomingAddresses, locale);
|
|
523
1416
|
await this.syncDocuments(tx, id, incomingDocuments);
|
|
1417
|
+
if (nextLifecycleStage && currentLifecycleStage !== nextLifecycleStage) {
|
|
1418
|
+
await this.registerStageTransition(tx, {
|
|
1419
|
+
personId: id,
|
|
1420
|
+
fromStage: currentLifecycleStage,
|
|
1421
|
+
toStage: nextLifecycleStage,
|
|
1422
|
+
changedByUserId: this.coerceNumber(user === null || user === void 0 ? void 0 : user.id) || null,
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
524
1425
|
return { success: true };
|
|
525
1426
|
})
|
|
526
1427
|
.then(async (result) => {
|
|
@@ -592,6 +1493,118 @@ let PersonService = class PersonService {
|
|
|
592
1493
|
}),
|
|
593
1494
|
]);
|
|
594
1495
|
}
|
|
1496
|
+
async findFollowupSearchPersonIds(search, allowCompanyRegistration) {
|
|
1497
|
+
const normalizedSearch = this.normalizeTextOrNull(search);
|
|
1498
|
+
if (!normalizedSearch) {
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
const where = {
|
|
1502
|
+
OR: await this.buildSearchFilters(normalizedSearch),
|
|
1503
|
+
};
|
|
1504
|
+
if (!allowCompanyRegistration) {
|
|
1505
|
+
where.type = 'individual';
|
|
1506
|
+
}
|
|
1507
|
+
const people = await this.prismaService.person.findMany({
|
|
1508
|
+
where,
|
|
1509
|
+
select: {
|
|
1510
|
+
id: true,
|
|
1511
|
+
},
|
|
1512
|
+
});
|
|
1513
|
+
return people.map((person) => person.id).filter((id) => id > 0);
|
|
1514
|
+
}
|
|
1515
|
+
buildFollowupSqlFilters({ allowCompanyRegistration, searchPersonIds, status, dateFrom, dateTo, }) {
|
|
1516
|
+
const filters = [];
|
|
1517
|
+
const followupTimestampSql = this.getFollowupTimestampSql();
|
|
1518
|
+
if (!allowCompanyRegistration) {
|
|
1519
|
+
filters.push(api_prisma_1.Prisma.sql `AND p.type = 'individual'`);
|
|
1520
|
+
}
|
|
1521
|
+
if (searchPersonIds && searchPersonIds.length > 0) {
|
|
1522
|
+
filters.push(api_prisma_1.Prisma.sql `AND p.id IN (${api_prisma_1.Prisma.join(searchPersonIds)})`);
|
|
1523
|
+
}
|
|
1524
|
+
const { todayStartIso, tomorrowStartIso } = this.getFollowupDayBoundaryIsoStrings();
|
|
1525
|
+
if (status === 'overdue') {
|
|
1526
|
+
filters.push(api_prisma_1.Prisma.sql `AND ${followupTimestampSql} < CAST(${todayStartIso} AS TIMESTAMPTZ)`);
|
|
1527
|
+
}
|
|
1528
|
+
else if (status === 'today') {
|
|
1529
|
+
filters.push(api_prisma_1.Prisma.sql `AND ${followupTimestampSql} >= CAST(${todayStartIso} AS TIMESTAMPTZ)`);
|
|
1530
|
+
filters.push(api_prisma_1.Prisma.sql `AND ${followupTimestampSql} < CAST(${tomorrowStartIso} AS TIMESTAMPTZ)`);
|
|
1531
|
+
}
|
|
1532
|
+
else if (status === 'upcoming') {
|
|
1533
|
+
filters.push(api_prisma_1.Prisma.sql `AND ${followupTimestampSql} >= CAST(${tomorrowStartIso} AS TIMESTAMPTZ)`);
|
|
1534
|
+
}
|
|
1535
|
+
const dateFromIso = this.normalizeDateOnlyBoundary(dateFrom, 'start');
|
|
1536
|
+
if (dateFromIso) {
|
|
1537
|
+
filters.push(api_prisma_1.Prisma.sql `AND ${followupTimestampSql} >= CAST(${dateFromIso} AS TIMESTAMPTZ)`);
|
|
1538
|
+
}
|
|
1539
|
+
const dateToIso = this.normalizeDateOnlyBoundary(dateTo, 'endExclusive');
|
|
1540
|
+
if (dateToIso) {
|
|
1541
|
+
filters.push(api_prisma_1.Prisma.sql `AND ${followupTimestampSql} < CAST(${dateToIso} AS TIMESTAMPTZ)`);
|
|
1542
|
+
}
|
|
1543
|
+
return filters.length > 0
|
|
1544
|
+
? api_prisma_1.Prisma.join(filters, '\n')
|
|
1545
|
+
: api_prisma_1.Prisma.empty;
|
|
1546
|
+
}
|
|
1547
|
+
createEmptyFollowupPagination(page, pageSize) {
|
|
1548
|
+
return {
|
|
1549
|
+
total: 0,
|
|
1550
|
+
lastPage: 1,
|
|
1551
|
+
page,
|
|
1552
|
+
pageSize,
|
|
1553
|
+
prev: page > 1 ? page - 1 : null,
|
|
1554
|
+
next: null,
|
|
1555
|
+
data: [],
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
getFollowupTimestampSql() {
|
|
1559
|
+
return api_prisma_1.Prisma.sql `CAST(TRIM(BOTH '"' FROM pm_next.value::text) AS TIMESTAMPTZ)`;
|
|
1560
|
+
}
|
|
1561
|
+
getFollowupDayBoundaryDates(reference = new Date()) {
|
|
1562
|
+
const todayStart = new Date(reference.getFullYear(), reference.getMonth(), reference.getDate());
|
|
1563
|
+
const tomorrowStart = new Date(todayStart);
|
|
1564
|
+
tomorrowStart.setDate(tomorrowStart.getDate() + 1);
|
|
1565
|
+
return {
|
|
1566
|
+
todayStart,
|
|
1567
|
+
tomorrowStart,
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
getFollowupDayBoundaryIsoStrings(reference = new Date()) {
|
|
1571
|
+
const { todayStart, tomorrowStart } = this.getFollowupDayBoundaryDates(reference);
|
|
1572
|
+
return {
|
|
1573
|
+
todayStartIso: todayStart.toISOString(),
|
|
1574
|
+
tomorrowStartIso: tomorrowStart.toISOString(),
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
normalizeDateOnlyBoundary(value, mode) {
|
|
1578
|
+
if (!value) {
|
|
1579
|
+
return null;
|
|
1580
|
+
}
|
|
1581
|
+
const parsed = new Date(`${value}T00:00:00`);
|
|
1582
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
1583
|
+
return null;
|
|
1584
|
+
}
|
|
1585
|
+
if (mode === 'endExclusive') {
|
|
1586
|
+
parsed.setDate(parsed.getDate() + 1);
|
|
1587
|
+
}
|
|
1588
|
+
return parsed.toISOString();
|
|
1589
|
+
}
|
|
1590
|
+
getFollowupStatus(nextActionAt) {
|
|
1591
|
+
const parsed = new Date(nextActionAt);
|
|
1592
|
+
const { todayStart, tomorrowStart } = this.getFollowupDayBoundaryDates();
|
|
1593
|
+
if (parsed < todayStart) {
|
|
1594
|
+
return 'overdue';
|
|
1595
|
+
}
|
|
1596
|
+
if (parsed < tomorrowStart) {
|
|
1597
|
+
return 'today';
|
|
1598
|
+
}
|
|
1599
|
+
return 'upcoming';
|
|
1600
|
+
}
|
|
1601
|
+
coerceCount(value) {
|
|
1602
|
+
if (typeof value === 'bigint') {
|
|
1603
|
+
return Number(value);
|
|
1604
|
+
}
|
|
1605
|
+
const parsed = Number(value);
|
|
1606
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1607
|
+
}
|
|
595
1608
|
async openPublicAvatar(locale, fileId, res) {
|
|
596
1609
|
const personWithAvatar = await this.prismaService.person.findFirst({
|
|
597
1610
|
where: {
|
|
@@ -612,12 +1625,174 @@ let PersonService = class PersonService {
|
|
|
612
1625
|
});
|
|
613
1626
|
res.send(buffer);
|
|
614
1627
|
}
|
|
1628
|
+
resolveDashboardRanges(query, locale) {
|
|
1629
|
+
var _a;
|
|
1630
|
+
const period = (_a = query.period) !== null && _a !== void 0 ? _a : '30d';
|
|
1631
|
+
const now = new Date();
|
|
1632
|
+
if (period === 'custom') {
|
|
1633
|
+
if (!query.date_from || !query.date_to) {
|
|
1634
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.dateMustBeString', locale, 'date_from and date_to are required when period is custom.'));
|
|
1635
|
+
}
|
|
1636
|
+
const start = this.startOfDay(this.parseDateOrThrow(query.date_from, locale));
|
|
1637
|
+
const end = this.endOfDay(this.parseDateOrThrow(query.date_to, locale));
|
|
1638
|
+
if (start.getTime() > end.getTime()) {
|
|
1639
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.dateMustBeString', locale, 'date_from must be less than or equal to date_to.'));
|
|
1640
|
+
}
|
|
1641
|
+
return {
|
|
1642
|
+
created: { start, end },
|
|
1643
|
+
operational: { start, end },
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
const dayCount = Number(period.replace('d', ''));
|
|
1647
|
+
const createdStart = this.startOfDay(this.addDays(now, -(dayCount - 1)));
|
|
1648
|
+
const createdEnd = this.endOfDay(now);
|
|
1649
|
+
const operationalStart = this.startOfDay(this.addDays(now, -(dayCount - 1)));
|
|
1650
|
+
const operationalEnd = this.endOfDay(this.addDays(now, dayCount - 1));
|
|
1651
|
+
return {
|
|
1652
|
+
created: {
|
|
1653
|
+
start: createdStart,
|
|
1654
|
+
end: createdEnd,
|
|
1655
|
+
},
|
|
1656
|
+
operational: {
|
|
1657
|
+
start: operationalStart,
|
|
1658
|
+
end: operationalEnd,
|
|
1659
|
+
},
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
resolveReportsRange(query, locale) {
|
|
1663
|
+
const start = this.startOfDay(this.parseDateOrThrow(query.date_from, locale));
|
|
1664
|
+
const end = this.endOfDay(this.parseDateOrThrow(query.date_to, locale));
|
|
1665
|
+
if (start.getTime() > end.getTime()) {
|
|
1666
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.dateMustBeString', locale, 'date_from must be less than or equal to date_to.'));
|
|
1667
|
+
}
|
|
1668
|
+
return { start, end };
|
|
1669
|
+
}
|
|
1670
|
+
getReportPeriodKey(dateValue, groupBy) {
|
|
1671
|
+
const date = this.parseDateOrNull(dateValue);
|
|
1672
|
+
if (!date) {
|
|
1673
|
+
return null;
|
|
1674
|
+
}
|
|
1675
|
+
if (groupBy === 'day') {
|
|
1676
|
+
return this.toDateKey(date);
|
|
1677
|
+
}
|
|
1678
|
+
if (groupBy === 'week') {
|
|
1679
|
+
return this.toDateKey(this.startOfWeek(date));
|
|
1680
|
+
}
|
|
1681
|
+
if (groupBy === 'month') {
|
|
1682
|
+
const year = date.getFullYear();
|
|
1683
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
1684
|
+
return `${year}-${month}`;
|
|
1685
|
+
}
|
|
1686
|
+
return String(date.getFullYear());
|
|
1687
|
+
}
|
|
1688
|
+
getOrCreateReportTimelineBucket(buckets, period) {
|
|
1689
|
+
const existing = buckets.get(period);
|
|
1690
|
+
if (existing) {
|
|
1691
|
+
return existing;
|
|
1692
|
+
}
|
|
1693
|
+
const next = {
|
|
1694
|
+
period,
|
|
1695
|
+
label: period,
|
|
1696
|
+
new_leads: 0,
|
|
1697
|
+
qualified_moves: 0,
|
|
1698
|
+
customer_moves: 0,
|
|
1699
|
+
lost_moves: 0,
|
|
1700
|
+
interactions: 0,
|
|
1701
|
+
followups_completed: 0,
|
|
1702
|
+
conversion_rate: 0,
|
|
1703
|
+
};
|
|
1704
|
+
buckets.set(period, next);
|
|
1705
|
+
return next;
|
|
1706
|
+
}
|
|
1707
|
+
startOfWeek(value) {
|
|
1708
|
+
const date = new Date(value);
|
|
1709
|
+
date.setHours(0, 0, 0, 0);
|
|
1710
|
+
const day = (date.getDay() + 6) % 7;
|
|
1711
|
+
date.setDate(date.getDate() - day);
|
|
1712
|
+
return date;
|
|
1713
|
+
}
|
|
1714
|
+
toDateKey(value) {
|
|
1715
|
+
const year = value.getFullYear();
|
|
1716
|
+
const month = String(value.getMonth() + 1).padStart(2, '0');
|
|
1717
|
+
const day = String(value.getDate()).padStart(2, '0');
|
|
1718
|
+
return `${year}-${month}-${day}`;
|
|
1719
|
+
}
|
|
1720
|
+
buildDashboardOwnerPerformance(people) {
|
|
1721
|
+
var _a, _b;
|
|
1722
|
+
const byOwnerId = new Map();
|
|
1723
|
+
for (const person of people) {
|
|
1724
|
+
const ownerUserId = this.coerceNumber(person.owner_user_id);
|
|
1725
|
+
if (ownerUserId <= 0) {
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
const current = (_a = byOwnerId.get(ownerUserId)) !== null && _a !== void 0 ? _a : {
|
|
1729
|
+
owner_user_id: ownerUserId,
|
|
1730
|
+
owner_name: ((_b = person.owner_user) === null || _b === void 0 ? void 0 : _b.name) || `#${ownerUserId}`,
|
|
1731
|
+
leads: 0,
|
|
1732
|
+
customers: 0,
|
|
1733
|
+
pipeline_value: 0,
|
|
1734
|
+
};
|
|
1735
|
+
current.leads += 1;
|
|
1736
|
+
if (person.lifecycle_stage === 'customer') {
|
|
1737
|
+
current.customers += 1;
|
|
1738
|
+
}
|
|
1739
|
+
if (person.lifecycle_stage !== 'lost') {
|
|
1740
|
+
current.pipeline_value += this.coerceNumber(person.deal_value);
|
|
1741
|
+
}
|
|
1742
|
+
byOwnerId.set(ownerUserId, current);
|
|
1743
|
+
}
|
|
1744
|
+
return Array.from(byOwnerId.values()).sort((left, right) => left.owner_name.localeCompare(right.owner_name));
|
|
1745
|
+
}
|
|
1746
|
+
mapDashboardListItem(person, { includeCreatedAt, includeNextActionAt, }) {
|
|
1747
|
+
var _a, _b, _c, _d, _e;
|
|
1748
|
+
const source = ((_a = person.source) !== null && _a !== void 0 ? _a : 'other');
|
|
1749
|
+
const lifecycleStage = ((_b = person.lifecycle_stage) !== null && _b !== void 0 ? _b : 'new');
|
|
1750
|
+
return Object.assign(Object.assign({ id: person.id, name: person.name, trade_name: (_c = person.trade_name) !== null && _c !== void 0 ? _c : null, owner_user: (_d = person.owner_user) !== null && _d !== void 0 ? _d : null, source, lifecycle_stage: lifecycleStage }, (includeNextActionAt && person.next_action_at
|
|
1751
|
+
? { next_action_at: person.next_action_at }
|
|
1752
|
+
: {})), (includeCreatedAt && person.created_at
|
|
1753
|
+
? {
|
|
1754
|
+
created_at: (_e = this.normalizeDateTimeOrNull(person.created_at)) !== null && _e !== void 0 ? _e : person.created_at,
|
|
1755
|
+
}
|
|
1756
|
+
: {}));
|
|
1757
|
+
}
|
|
1758
|
+
isDateWithinRange(value, range) {
|
|
1759
|
+
const parsed = this.parseDateOrNull(value);
|
|
1760
|
+
if (!parsed) {
|
|
1761
|
+
return false;
|
|
1762
|
+
}
|
|
1763
|
+
return (parsed.getTime() >= range.start.getTime() &&
|
|
1764
|
+
parsed.getTime() <= range.end.getTime());
|
|
1765
|
+
}
|
|
1766
|
+
parseDateOrThrow(value, locale) {
|
|
1767
|
+
const parsed = /^\d{4}-\d{2}-\d{2}$/.test(value)
|
|
1768
|
+
? this.parseDateOrNull(`${value}T00:00:00`)
|
|
1769
|
+
: this.parseDateOrNull(value);
|
|
1770
|
+
if (!parsed) {
|
|
1771
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.dateMustBeString', locale, `Invalid date value: ${value}`));
|
|
1772
|
+
}
|
|
1773
|
+
return parsed;
|
|
1774
|
+
}
|
|
1775
|
+
addDays(date, amount) {
|
|
1776
|
+
const next = new Date(date);
|
|
1777
|
+
next.setDate(next.getDate() + amount);
|
|
1778
|
+
return next;
|
|
1779
|
+
}
|
|
1780
|
+
startOfDay(date) {
|
|
1781
|
+
const next = new Date(date);
|
|
1782
|
+
next.setHours(0, 0, 0, 0);
|
|
1783
|
+
return next;
|
|
1784
|
+
}
|
|
1785
|
+
endOfDay(date) {
|
|
1786
|
+
const next = new Date(date);
|
|
1787
|
+
next.setHours(23, 59, 59, 999);
|
|
1788
|
+
return next;
|
|
1789
|
+
}
|
|
615
1790
|
async enrichPeople(people, allowCompanyRegistration = true) {
|
|
616
1791
|
if (people.length === 0) {
|
|
617
1792
|
return [];
|
|
618
1793
|
}
|
|
619
1794
|
const personIds = people.map((person) => person.id);
|
|
620
|
-
const [companies, individuals, companyBranches,
|
|
1795
|
+
const [companies, individuals, companyBranches, employerMetadataRaw] = await Promise.all([
|
|
621
1796
|
this.prismaService.person_company.findMany({
|
|
622
1797
|
where: { id: { in: personIds } },
|
|
623
1798
|
}),
|
|
@@ -637,8 +1812,9 @@ let PersonService = class PersonService {
|
|
|
637
1812
|
person_id: true,
|
|
638
1813
|
value: true,
|
|
639
1814
|
},
|
|
640
|
-
})
|
|
1815
|
+
}),
|
|
641
1816
|
]);
|
|
1817
|
+
const employerMetadata = allowCompanyRegistration ? employerMetadataRaw : [];
|
|
642
1818
|
const companyById = new Map(companies.map((item) => [item.id, item]));
|
|
643
1819
|
const individualById = new Map(individuals.map((item) => [item.id, item]));
|
|
644
1820
|
const branchesByHeadquarterId = new Map();
|
|
@@ -863,6 +2039,47 @@ let PersonService = class PersonService {
|
|
|
863
2039
|
})
|
|
864
2040
|
.filter((item) => item != null));
|
|
865
2041
|
}
|
|
2042
|
+
async getPersonLifecycleStage(personId) {
|
|
2043
|
+
var _a;
|
|
2044
|
+
const metadata = await this.prismaService.person_metadata.findFirst({
|
|
2045
|
+
where: {
|
|
2046
|
+
person_id: personId,
|
|
2047
|
+
key: LIFECYCLE_STAGE_METADATA_KEY,
|
|
2048
|
+
},
|
|
2049
|
+
select: {
|
|
2050
|
+
value: true,
|
|
2051
|
+
},
|
|
2052
|
+
});
|
|
2053
|
+
return (_a = this.metadataToString(metadata === null || metadata === void 0 ? void 0 : metadata.value)) !== null && _a !== void 0 ? _a : 'new';
|
|
2054
|
+
}
|
|
2055
|
+
async registerStageTransition(tx, { personId, fromStage, toStage, changedByUserId, }) {
|
|
2056
|
+
if (fromStage === toStage) {
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
const fromStageSql = fromStage
|
|
2060
|
+
? api_prisma_1.Prisma.sql `CAST(${fromStage} AS crm_stage_history_from_stage_enum)`
|
|
2061
|
+
: api_prisma_1.Prisma.sql `NULL`;
|
|
2062
|
+
await tx.$executeRaw(api_prisma_1.Prisma.sql `
|
|
2063
|
+
INSERT INTO crm_stage_history (
|
|
2064
|
+
person_id,
|
|
2065
|
+
from_stage,
|
|
2066
|
+
to_stage,
|
|
2067
|
+
changed_by_user_id,
|
|
2068
|
+
changed_at,
|
|
2069
|
+
created_at,
|
|
2070
|
+
updated_at
|
|
2071
|
+
)
|
|
2072
|
+
VALUES (
|
|
2073
|
+
${personId},
|
|
2074
|
+
${fromStageSql},
|
|
2075
|
+
CAST(${toStage} AS crm_stage_history_to_stage_enum),
|
|
2076
|
+
${changedByUserId},
|
|
2077
|
+
NOW(),
|
|
2078
|
+
NOW(),
|
|
2079
|
+
NOW()
|
|
2080
|
+
)
|
|
2081
|
+
`);
|
|
2082
|
+
}
|
|
866
2083
|
async syncPersonSubtypeData(tx, personId, currentType, data, locale) {
|
|
867
2084
|
var _a, _b, _c;
|
|
868
2085
|
const targetType = (_a = data.type) !== null && _a !== void 0 ? _a : currentType;
|
|
@@ -917,6 +2134,13 @@ let PersonService = class PersonService {
|
|
|
917
2134
|
create: {
|
|
918
2135
|
id: personId,
|
|
919
2136
|
trade_name: this.normalizeTextOrNull(data.trade_name),
|
|
2137
|
+
industry: this.normalizeTextOrNull(data.industry),
|
|
2138
|
+
website: this.normalizeTextOrNull(data.website),
|
|
2139
|
+
annual_revenue: this.normalizeDecimalOrNull(data.annual_revenue),
|
|
2140
|
+
employee_count: this.normalizeIntegerOrNull(data.employee_count),
|
|
2141
|
+
account_lifecycle_stage: this.normalizeAccountLifecycleStage(data.lifecycle_stage),
|
|
2142
|
+
city: this.normalizeTextOrNull(data.city),
|
|
2143
|
+
state: this.normalizeStateOrNull(data.state),
|
|
920
2144
|
foundation_date: this.parseDateOrNull(data.foundation_date),
|
|
921
2145
|
legal_nature: this.normalizeTextOrNull(data.legal_nature),
|
|
922
2146
|
headquarter_id: normalizedHeadquarterId > 0 ? normalizedHeadquarterId : null,
|
|
@@ -925,6 +2149,27 @@ let PersonService = class PersonService {
|
|
|
925
2149
|
trade_name: data.trade_name === undefined
|
|
926
2150
|
? undefined
|
|
927
2151
|
: this.normalizeTextOrNull(data.trade_name),
|
|
2152
|
+
industry: data.industry === undefined
|
|
2153
|
+
? undefined
|
|
2154
|
+
: this.normalizeTextOrNull(data.industry),
|
|
2155
|
+
website: data.website === undefined
|
|
2156
|
+
? undefined
|
|
2157
|
+
: this.normalizeTextOrNull(data.website),
|
|
2158
|
+
annual_revenue: data.annual_revenue === undefined
|
|
2159
|
+
? undefined
|
|
2160
|
+
: this.normalizeDecimalOrNull(data.annual_revenue),
|
|
2161
|
+
employee_count: data.employee_count === undefined
|
|
2162
|
+
? undefined
|
|
2163
|
+
: this.normalizeIntegerOrNull(data.employee_count),
|
|
2164
|
+
account_lifecycle_stage: data.lifecycle_stage === undefined
|
|
2165
|
+
? undefined
|
|
2166
|
+
: this.normalizeAccountLifecycleStage(data.lifecycle_stage),
|
|
2167
|
+
city: data.city === undefined
|
|
2168
|
+
? undefined
|
|
2169
|
+
: this.normalizeTextOrNull(data.city),
|
|
2170
|
+
state: data.state === undefined
|
|
2171
|
+
? undefined
|
|
2172
|
+
: this.normalizeStateOrNull(data.state),
|
|
928
2173
|
foundation_date: data.foundation_date === undefined
|
|
929
2174
|
? undefined
|
|
930
2175
|
: this.parseDateOrNull(data.foundation_date),
|
|
@@ -1394,6 +2639,20 @@ let PersonService = class PersonService {
|
|
|
1394
2639
|
const date = value instanceof Date ? value : new Date(String(value));
|
|
1395
2640
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
1396
2641
|
}
|
|
2642
|
+
normalizeDecimalOrNull(value) {
|
|
2643
|
+
if (value == null || value === '') {
|
|
2644
|
+
return null;
|
|
2645
|
+
}
|
|
2646
|
+
const parsed = Number(value);
|
|
2647
|
+
return Number.isFinite(parsed) ? new api_prisma_1.Prisma.Decimal(parsed) : null;
|
|
2648
|
+
}
|
|
2649
|
+
normalizeIntegerOrNull(value) {
|
|
2650
|
+
if (value == null || value === '') {
|
|
2651
|
+
return null;
|
|
2652
|
+
}
|
|
2653
|
+
const parsed = Number(value);
|
|
2654
|
+
return Number.isInteger(parsed) ? parsed : null;
|
|
2655
|
+
}
|
|
1397
2656
|
normalizeTextOrNull(value) {
|
|
1398
2657
|
if (typeof value !== 'string') {
|
|
1399
2658
|
return value == null ? null : String(value);
|
|
@@ -1411,6 +2670,20 @@ let PersonService = class PersonService {
|
|
|
1411
2670
|
const parsed = Number(value);
|
|
1412
2671
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
1413
2672
|
}
|
|
2673
|
+
normalizeStateOrNull(value) {
|
|
2674
|
+
var _a, _b;
|
|
2675
|
+
const normalized = (_b = (_a = this.normalizeTextOrNull(value)) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : null;
|
|
2676
|
+
return normalized ? normalized.slice(0, 2) : null;
|
|
2677
|
+
}
|
|
2678
|
+
normalizeAccountLifecycleStage(value) {
|
|
2679
|
+
const normalized = this.normalizeTextOrNull(value);
|
|
2680
|
+
if (!normalized) {
|
|
2681
|
+
return null;
|
|
2682
|
+
}
|
|
2683
|
+
return account_dto_1.ACCOUNT_LIFECYCLE_STAGES.includes(normalized)
|
|
2684
|
+
? normalized
|
|
2685
|
+
: null;
|
|
2686
|
+
}
|
|
1414
2687
|
resolveRequestedOwnerUserId(ownerUserId, mine, currentUserId) {
|
|
1415
2688
|
if (mine === true ||
|
|
1416
2689
|
mine === 'true' ||
|
|
@@ -1436,6 +2709,288 @@ let PersonService = class PersonService {
|
|
|
1436
2709
|
}
|
|
1437
2710
|
return person;
|
|
1438
2711
|
}
|
|
2712
|
+
async getPersonOwnerUserId(personId) {
|
|
2713
|
+
const metadata = await this.prismaService.person_metadata.findFirst({
|
|
2714
|
+
where: {
|
|
2715
|
+
person_id: personId,
|
|
2716
|
+
key: OWNER_USER_METADATA_KEY,
|
|
2717
|
+
},
|
|
2718
|
+
select: {
|
|
2719
|
+
value: true,
|
|
2720
|
+
},
|
|
2721
|
+
});
|
|
2722
|
+
const ownerUserId = this.metadataToNumber(metadata === null || metadata === void 0 ? void 0 : metadata.value);
|
|
2723
|
+
return ownerUserId && ownerUserId > 0 ? ownerUserId : null;
|
|
2724
|
+
}
|
|
2725
|
+
async upsertFollowupActivity(tx, { personId, ownerUserId, dueAt, notes, actorUserId, }) {
|
|
2726
|
+
const existingRows = (await tx.$queryRaw(api_prisma_1.Prisma.sql `
|
|
2727
|
+
SELECT id
|
|
2728
|
+
FROM crm_activity
|
|
2729
|
+
WHERE person_id = ${personId}
|
|
2730
|
+
AND source_kind = 'followup'
|
|
2731
|
+
AND completed_at IS NULL
|
|
2732
|
+
ORDER BY id DESC
|
|
2733
|
+
LIMIT 1
|
|
2734
|
+
`));
|
|
2735
|
+
const existing = existingRows[0];
|
|
2736
|
+
const normalizedNotes = this.normalizeTextOrNull(notes);
|
|
2737
|
+
if (existing) {
|
|
2738
|
+
await tx.$executeRaw(api_prisma_1.Prisma.sql `
|
|
2739
|
+
UPDATE crm_activity
|
|
2740
|
+
SET
|
|
2741
|
+
owner_user_id = ${ownerUserId},
|
|
2742
|
+
type = CAST(${'task'} AS crm_activity_type_enum),
|
|
2743
|
+
subject = ${this.getFollowupActivitySubject()},
|
|
2744
|
+
notes = ${normalizedNotes},
|
|
2745
|
+
due_at = CAST(${dueAt} AS TIMESTAMPTZ),
|
|
2746
|
+
priority = CAST(${'medium'} AS crm_activity_priority_enum),
|
|
2747
|
+
updated_at = NOW()
|
|
2748
|
+
WHERE id = ${existing.id}
|
|
2749
|
+
`);
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
await tx.$executeRaw(api_prisma_1.Prisma.sql `
|
|
2753
|
+
INSERT INTO crm_activity (
|
|
2754
|
+
person_id,
|
|
2755
|
+
owner_user_id,
|
|
2756
|
+
created_by_user_id,
|
|
2757
|
+
type,
|
|
2758
|
+
subject,
|
|
2759
|
+
notes,
|
|
2760
|
+
due_at,
|
|
2761
|
+
priority,
|
|
2762
|
+
source_kind,
|
|
2763
|
+
created_at,
|
|
2764
|
+
updated_at
|
|
2765
|
+
)
|
|
2766
|
+
VALUES (
|
|
2767
|
+
${personId},
|
|
2768
|
+
${ownerUserId},
|
|
2769
|
+
${actorUserId},
|
|
2770
|
+
CAST(${'task'} AS crm_activity_type_enum),
|
|
2771
|
+
${this.getFollowupActivitySubject()},
|
|
2772
|
+
${normalizedNotes},
|
|
2773
|
+
CAST(${dueAt} AS TIMESTAMPTZ),
|
|
2774
|
+
CAST(${'medium'} AS crm_activity_priority_enum),
|
|
2775
|
+
CAST(${'followup'} AS crm_activity_source_kind_enum),
|
|
2776
|
+
NOW(),
|
|
2777
|
+
NOW()
|
|
2778
|
+
)
|
|
2779
|
+
`);
|
|
2780
|
+
}
|
|
2781
|
+
async createCompletedInteractionActivity(tx, { personId, ownerUserId, interaction, actorUserId, }) {
|
|
2782
|
+
const completedAt = new Date(interaction.created_at);
|
|
2783
|
+
await tx.$executeRaw(api_prisma_1.Prisma.sql `
|
|
2784
|
+
INSERT INTO crm_activity (
|
|
2785
|
+
person_id,
|
|
2786
|
+
owner_user_id,
|
|
2787
|
+
created_by_user_id,
|
|
2788
|
+
completed_by_user_id,
|
|
2789
|
+
type,
|
|
2790
|
+
subject,
|
|
2791
|
+
notes,
|
|
2792
|
+
due_at,
|
|
2793
|
+
completed_at,
|
|
2794
|
+
priority,
|
|
2795
|
+
source_kind,
|
|
2796
|
+
created_at,
|
|
2797
|
+
updated_at
|
|
2798
|
+
)
|
|
2799
|
+
VALUES (
|
|
2800
|
+
${personId},
|
|
2801
|
+
${ownerUserId},
|
|
2802
|
+
${actorUserId},
|
|
2803
|
+
${actorUserId},
|
|
2804
|
+
CAST(${interaction.type} AS crm_activity_type_enum),
|
|
2805
|
+
${this.getInteractionActivitySubject(interaction.type)},
|
|
2806
|
+
${this.normalizeTextOrNull(interaction.notes)},
|
|
2807
|
+
CAST(${interaction.created_at} AS TIMESTAMPTZ),
|
|
2808
|
+
CAST(${interaction.created_at} AS TIMESTAMPTZ),
|
|
2809
|
+
CAST(${'medium'} AS crm_activity_priority_enum),
|
|
2810
|
+
CAST(${'interaction'} AS crm_activity_source_kind_enum),
|
|
2811
|
+
CAST(${interaction.created_at} AS TIMESTAMPTZ),
|
|
2812
|
+
NOW()
|
|
2813
|
+
)
|
|
2814
|
+
`);
|
|
2815
|
+
}
|
|
2816
|
+
getFollowupActivitySubject() {
|
|
2817
|
+
return 'Follow-up';
|
|
2818
|
+
}
|
|
2819
|
+
getInteractionActivitySubject(type) {
|
|
2820
|
+
switch (type) {
|
|
2821
|
+
case create_interaction_dto_1.PersonInteractionTypeDTO.CALL:
|
|
2822
|
+
return 'Call';
|
|
2823
|
+
case create_interaction_dto_1.PersonInteractionTypeDTO.EMAIL:
|
|
2824
|
+
return 'Email';
|
|
2825
|
+
case create_interaction_dto_1.PersonInteractionTypeDTO.WHATSAPP:
|
|
2826
|
+
return 'WhatsApp';
|
|
2827
|
+
case create_interaction_dto_1.PersonInteractionTypeDTO.MEETING:
|
|
2828
|
+
return 'Meeting';
|
|
2829
|
+
case create_interaction_dto_1.PersonInteractionTypeDTO.NOTE:
|
|
2830
|
+
default:
|
|
2831
|
+
return 'Note';
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
async ensureCompanyAccountAccessible(id, locale) {
|
|
2835
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
2836
|
+
if (!allowCompanyRegistration) {
|
|
2837
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person with ID ${id} not found`));
|
|
2838
|
+
}
|
|
2839
|
+
const person = await this.prismaService.person.findUnique({
|
|
2840
|
+
where: { id },
|
|
2841
|
+
select: {
|
|
2842
|
+
id: true,
|
|
2843
|
+
name: true,
|
|
2844
|
+
status: true,
|
|
2845
|
+
type: true,
|
|
2846
|
+
},
|
|
2847
|
+
});
|
|
2848
|
+
if (!person || person.type !== 'company') {
|
|
2849
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person with ID ${id} not found`));
|
|
2850
|
+
}
|
|
2851
|
+
return person;
|
|
2852
|
+
}
|
|
2853
|
+
async loadAccountPeopleByIds(personIds) {
|
|
2854
|
+
if (personIds.length === 0) {
|
|
2855
|
+
return [];
|
|
2856
|
+
}
|
|
2857
|
+
const people = await this.prismaService.person.findMany({
|
|
2858
|
+
where: {
|
|
2859
|
+
id: {
|
|
2860
|
+
in: personIds,
|
|
2861
|
+
},
|
|
2862
|
+
},
|
|
2863
|
+
include: {
|
|
2864
|
+
contact: {
|
|
2865
|
+
include: {
|
|
2866
|
+
contact_type: true,
|
|
2867
|
+
},
|
|
2868
|
+
},
|
|
2869
|
+
person_metadata: true,
|
|
2870
|
+
},
|
|
2871
|
+
});
|
|
2872
|
+
return this.enrichPeople(people, true);
|
|
2873
|
+
}
|
|
2874
|
+
mapAccountFromPerson(person, company) {
|
|
2875
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
2876
|
+
return {
|
|
2877
|
+
id: person.id,
|
|
2878
|
+
name: person.name,
|
|
2879
|
+
trade_name: (_a = company.trade_name) !== null && _a !== void 0 ? _a : null,
|
|
2880
|
+
status: person.status,
|
|
2881
|
+
industry: (_b = company.industry) !== null && _b !== void 0 ? _b : null,
|
|
2882
|
+
website: (_c = company.website) !== null && _c !== void 0 ? _c : null,
|
|
2883
|
+
email: this.getPrimaryAccountContactValue(person.contact, ['EMAIL']),
|
|
2884
|
+
phone: this.getPrimaryAccountContactValue(person.contact, [
|
|
2885
|
+
'PHONE',
|
|
2886
|
+
'MOBILE',
|
|
2887
|
+
'WHATSAPP',
|
|
2888
|
+
]),
|
|
2889
|
+
owner_user_id: (_d = person.owner_user_id) !== null && _d !== void 0 ? _d : null,
|
|
2890
|
+
owner_user: (_e = person.owner_user) !== null && _e !== void 0 ? _e : null,
|
|
2891
|
+
annual_revenue: company.annual_revenue == null ? null : Number(company.annual_revenue),
|
|
2892
|
+
employee_count: (_f = company.employee_count) !== null && _f !== void 0 ? _f : null,
|
|
2893
|
+
lifecycle_stage: (_g = company.account_lifecycle_stage) !== null && _g !== void 0 ? _g : null,
|
|
2894
|
+
city: (_h = company.city) !== null && _h !== void 0 ? _h : null,
|
|
2895
|
+
state: (_j = company.state) !== null && _j !== void 0 ? _j : null,
|
|
2896
|
+
created_at: (_m = (_l = (_k = person.created_at) === null || _k === void 0 ? void 0 : _k.toISOString) === null || _l === void 0 ? void 0 : _l.call(_k)) !== null && _m !== void 0 ? _m : String(person.created_at),
|
|
2897
|
+
last_interaction_at: (_o = this.normalizeDateTimeOrNull(person.last_interaction_at)) !== null && _o !== void 0 ? _o : null,
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
getPrimaryAccountContactValue(contacts, codes) {
|
|
2901
|
+
var _a;
|
|
2902
|
+
const normalizedCodes = new Set(codes.map((code) => code.toUpperCase()));
|
|
2903
|
+
const items = Array.isArray(contacts)
|
|
2904
|
+
? contacts.filter((contact) => {
|
|
2905
|
+
var _a;
|
|
2906
|
+
return normalizedCodes.has(String(((_a = contact === null || contact === void 0 ? void 0 : contact.contact_type) === null || _a === void 0 ? void 0 : _a.code) || '').toUpperCase());
|
|
2907
|
+
})
|
|
2908
|
+
: [];
|
|
2909
|
+
const primary = items.find((contact) => contact === null || contact === void 0 ? void 0 : contact.is_primary);
|
|
2910
|
+
const fallback = items[0];
|
|
2911
|
+
return this.normalizeTextOrNull((_a = primary === null || primary === void 0 ? void 0 : primary.value) !== null && _a !== void 0 ? _a : fallback === null || fallback === void 0 ? void 0 : fallback.value);
|
|
2912
|
+
}
|
|
2913
|
+
async upsertPrimaryAccountContact(tx, personId, code, value) {
|
|
2914
|
+
const normalizedValue = this.normalizeTextOrNull(value);
|
|
2915
|
+
const allowedCodes = code === 'PHONE' ? ['PHONE', 'MOBILE', 'WHATSAPP'] : ['EMAIL'];
|
|
2916
|
+
const type = await tx.contact_type.findFirst({
|
|
2917
|
+
where: {
|
|
2918
|
+
code: {
|
|
2919
|
+
equals: code,
|
|
2920
|
+
mode: 'insensitive',
|
|
2921
|
+
},
|
|
2922
|
+
},
|
|
2923
|
+
select: {
|
|
2924
|
+
id: true,
|
|
2925
|
+
},
|
|
2926
|
+
});
|
|
2927
|
+
if (!type) {
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
const existingContacts = await tx.contact.findMany({
|
|
2931
|
+
where: {
|
|
2932
|
+
person_id: personId,
|
|
2933
|
+
contact_type: {
|
|
2934
|
+
code: {
|
|
2935
|
+
in: allowedCodes,
|
|
2936
|
+
},
|
|
2937
|
+
},
|
|
2938
|
+
},
|
|
2939
|
+
orderBy: [{ is_primary: 'desc' }, { id: 'asc' }],
|
|
2940
|
+
select: {
|
|
2941
|
+
id: true,
|
|
2942
|
+
is_primary: true,
|
|
2943
|
+
},
|
|
2944
|
+
});
|
|
2945
|
+
if (!normalizedValue) {
|
|
2946
|
+
const contactToDelete = existingContacts[0];
|
|
2947
|
+
if (contactToDelete) {
|
|
2948
|
+
await tx.contact.delete({
|
|
2949
|
+
where: {
|
|
2950
|
+
id: contactToDelete.id,
|
|
2951
|
+
},
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
const primaryContact = existingContacts[0];
|
|
2957
|
+
if (primaryContact) {
|
|
2958
|
+
await tx.contact.update({
|
|
2959
|
+
where: {
|
|
2960
|
+
id: primaryContact.id,
|
|
2961
|
+
},
|
|
2962
|
+
data: {
|
|
2963
|
+
value: normalizedValue,
|
|
2964
|
+
is_primary: true,
|
|
2965
|
+
contact_type_id: type.id,
|
|
2966
|
+
},
|
|
2967
|
+
});
|
|
2968
|
+
const secondaryContacts = existingContacts
|
|
2969
|
+
.slice(1)
|
|
2970
|
+
.filter((contact) => contact.is_primary);
|
|
2971
|
+
if (secondaryContacts.length > 0) {
|
|
2972
|
+
await tx.contact.updateMany({
|
|
2973
|
+
where: {
|
|
2974
|
+
id: {
|
|
2975
|
+
in: secondaryContacts.map((contact) => contact.id),
|
|
2976
|
+
},
|
|
2977
|
+
},
|
|
2978
|
+
data: {
|
|
2979
|
+
is_primary: false,
|
|
2980
|
+
},
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
await tx.contact.create({
|
|
2986
|
+
data: {
|
|
2987
|
+
person_id: personId,
|
|
2988
|
+
contact_type_id: type.id,
|
|
2989
|
+
value: normalizedValue,
|
|
2990
|
+
is_primary: true,
|
|
2991
|
+
},
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
1439
2994
|
async loadInteractionsFromTx(tx, personId) {
|
|
1440
2995
|
const metadata = await tx.person_metadata.findFirst({
|
|
1441
2996
|
where: {
|
|
@@ -1513,21 +3068,191 @@ let PersonService = class PersonService {
|
|
|
1513
3068
|
}
|
|
1514
3069
|
return filters;
|
|
1515
3070
|
}
|
|
3071
|
+
buildAccountSqlFilters({ search, status, lifecycleStage, }) {
|
|
3072
|
+
const filters = [];
|
|
3073
|
+
if (status && status !== 'all') {
|
|
3074
|
+
filters.push(api_prisma_1.Prisma.sql `AND p.status = ${status}`);
|
|
3075
|
+
}
|
|
3076
|
+
if (lifecycleStage && lifecycleStage !== 'all') {
|
|
3077
|
+
filters.push(api_prisma_1.Prisma.sql `AND pc.account_lifecycle_stage = CAST(${lifecycleStage} AS person_company_account_lifecycle_stage_enum)`);
|
|
3078
|
+
}
|
|
3079
|
+
if (search) {
|
|
3080
|
+
const searchLike = `%${search}%`;
|
|
3081
|
+
const normalizedDigits = this.normalizeDigits(search);
|
|
3082
|
+
const digitsLike = `%${normalizedDigits}%`;
|
|
3083
|
+
filters.push(api_prisma_1.Prisma.sql `
|
|
3084
|
+
AND (
|
|
3085
|
+
p.name ILIKE ${searchLike}
|
|
3086
|
+
OR COALESCE(pc.trade_name, '') ILIKE ${searchLike}
|
|
3087
|
+
OR COALESCE(pc.city, '') ILIKE ${searchLike}
|
|
3088
|
+
OR COALESCE(pc.state, '') ILIKE ${searchLike}
|
|
3089
|
+
OR EXISTS (
|
|
3090
|
+
SELECT 1
|
|
3091
|
+
FROM contact c
|
|
3092
|
+
INNER JOIN contact_type ct ON ct.id = c.contact_type_id
|
|
3093
|
+
WHERE c.person_id = p.id
|
|
3094
|
+
AND UPPER(ct.code) = 'EMAIL'
|
|
3095
|
+
AND c.value ILIKE ${searchLike}
|
|
3096
|
+
)
|
|
3097
|
+
${normalizedDigits.length > 0
|
|
3098
|
+
? api_prisma_1.Prisma.sql `
|
|
3099
|
+
OR EXISTS (
|
|
3100
|
+
SELECT 1
|
|
3101
|
+
FROM contact c
|
|
3102
|
+
INNER JOIN contact_type ct ON ct.id = c.contact_type_id
|
|
3103
|
+
WHERE c.person_id = p.id
|
|
3104
|
+
AND UPPER(ct.code) IN ('PHONE', 'MOBILE', 'WHATSAPP')
|
|
3105
|
+
AND regexp_replace(COALESCE(c.value, ''), '[^0-9]+', '', 'g') ILIKE ${digitsLike}
|
|
3106
|
+
)
|
|
3107
|
+
`
|
|
3108
|
+
: api_prisma_1.Prisma.empty}
|
|
3109
|
+
)
|
|
3110
|
+
`);
|
|
3111
|
+
}
|
|
3112
|
+
return filters.length > 0 ? api_prisma_1.Prisma.join(filters, '\n') : api_prisma_1.Prisma.empty;
|
|
3113
|
+
}
|
|
3114
|
+
getAccountOrderBySql(sortField, sortOrder) {
|
|
3115
|
+
const normalizedSortField = sortField === 'created_at' ? 'created_at' : 'name';
|
|
3116
|
+
const normalizedSortOrder = sortOrder === 'desc' ? 'DESC' : 'ASC';
|
|
3117
|
+
if (normalizedSortField === 'created_at') {
|
|
3118
|
+
return normalizedSortOrder === 'DESC'
|
|
3119
|
+
? api_prisma_1.Prisma.sql `p.created_at DESC`
|
|
3120
|
+
: api_prisma_1.Prisma.sql `p.created_at ASC`;
|
|
3121
|
+
}
|
|
3122
|
+
return normalizedSortOrder === 'DESC'
|
|
3123
|
+
? api_prisma_1.Prisma.sql `LOWER(p.name) DESC`
|
|
3124
|
+
: api_prisma_1.Prisma.sql `LOWER(p.name) ASC`;
|
|
3125
|
+
}
|
|
3126
|
+
createEmptyAccountPagination(page, pageSize) {
|
|
3127
|
+
return {
|
|
3128
|
+
total: 0,
|
|
3129
|
+
lastPage: 1,
|
|
3130
|
+
page,
|
|
3131
|
+
pageSize,
|
|
3132
|
+
prev: page > 1 ? page - 1 : null,
|
|
3133
|
+
next: null,
|
|
3134
|
+
data: [],
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
buildCrmActivitySqlFilters({ allowCompanyRegistration, search, status, type, priority, }) {
|
|
3138
|
+
const filters = [];
|
|
3139
|
+
if (!allowCompanyRegistration) {
|
|
3140
|
+
filters.push(api_prisma_1.Prisma.sql `AND p.type = 'individual'`);
|
|
3141
|
+
}
|
|
3142
|
+
if (status === 'pending') {
|
|
3143
|
+
filters.push(api_prisma_1.Prisma.sql `AND a.completed_at IS NULL AND a.due_at >= NOW()`);
|
|
3144
|
+
}
|
|
3145
|
+
else if (status === 'overdue') {
|
|
3146
|
+
filters.push(api_prisma_1.Prisma.sql `AND a.completed_at IS NULL AND a.due_at < NOW()`);
|
|
3147
|
+
}
|
|
3148
|
+
else if (status === 'completed') {
|
|
3149
|
+
filters.push(api_prisma_1.Prisma.sql `AND a.completed_at IS NOT NULL`);
|
|
3150
|
+
}
|
|
3151
|
+
if (type && type !== 'all') {
|
|
3152
|
+
filters.push(api_prisma_1.Prisma.sql `AND a.type = CAST(${type} AS crm_activity_type_enum)`);
|
|
3153
|
+
}
|
|
3154
|
+
if (priority && priority !== 'all') {
|
|
3155
|
+
filters.push(api_prisma_1.Prisma.sql `AND a.priority = CAST(${priority} AS crm_activity_priority_enum)`);
|
|
3156
|
+
}
|
|
3157
|
+
if (search) {
|
|
3158
|
+
const searchLike = `%${search}%`;
|
|
3159
|
+
filters.push(api_prisma_1.Prisma.sql `
|
|
3160
|
+
AND (
|
|
3161
|
+
a.subject ILIKE ${searchLike}
|
|
3162
|
+
OR COALESCE(a.notes, '') ILIKE ${searchLike}
|
|
3163
|
+
OR p.name ILIKE ${searchLike}
|
|
3164
|
+
OR COALESCE(owner_user.name, '') ILIKE ${searchLike}
|
|
3165
|
+
)
|
|
3166
|
+
`);
|
|
3167
|
+
}
|
|
3168
|
+
return filters.length > 0 ? api_prisma_1.Prisma.join(filters, '\n') : api_prisma_1.Prisma.empty;
|
|
3169
|
+
}
|
|
3170
|
+
createEmptyCrmActivityPagination(page, pageSize) {
|
|
3171
|
+
return {
|
|
3172
|
+
total: 0,
|
|
3173
|
+
lastPage: 1,
|
|
3174
|
+
page,
|
|
3175
|
+
pageSize,
|
|
3176
|
+
prev: page > 1 ? page - 1 : null,
|
|
3177
|
+
next: null,
|
|
3178
|
+
data: [],
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
mapCrmActivityListRow(row) {
|
|
3182
|
+
var _a, _b;
|
|
3183
|
+
const completedAt = this.normalizeDateTimeOrNull(row.completed_at);
|
|
3184
|
+
const dueAt = (_a = this.normalizeDateTimeOrNull(row.due_at)) !== null && _a !== void 0 ? _a : new Date().toISOString();
|
|
3185
|
+
const ownerUserId = this.coerceNumber(row.owner_user_id) || null;
|
|
3186
|
+
return {
|
|
3187
|
+
id: this.coerceNumber(row.id),
|
|
3188
|
+
person_id: this.coerceNumber(row.person_id),
|
|
3189
|
+
person: {
|
|
3190
|
+
id: this.coerceNumber(row.person_id),
|
|
3191
|
+
name: this.normalizeTextOrNull(row.person_name) || `#${row.person_id}`,
|
|
3192
|
+
type: this.normalizeTextOrNull(row.person_type) === 'company'
|
|
3193
|
+
? 'company'
|
|
3194
|
+
: 'individual',
|
|
3195
|
+
status: this.normalizeTextOrNull(row.person_status) === 'inactive'
|
|
3196
|
+
? 'inactive'
|
|
3197
|
+
: 'active',
|
|
3198
|
+
trade_name: this.normalizeTextOrNull(row.person_trade_name),
|
|
3199
|
+
},
|
|
3200
|
+
owner_user_id: ownerUserId,
|
|
3201
|
+
owner_user: ownerUserId
|
|
3202
|
+
? {
|
|
3203
|
+
id: ownerUserId,
|
|
3204
|
+
name: this.normalizeTextOrNull(row.owner_user_name) || `#${ownerUserId}`,
|
|
3205
|
+
}
|
|
3206
|
+
: null,
|
|
3207
|
+
type: (this.normalizeTextOrNull(row.type) || 'task'),
|
|
3208
|
+
subject: this.normalizeTextOrNull(row.subject) || 'Activity',
|
|
3209
|
+
notes: this.normalizeTextOrNull(row.notes),
|
|
3210
|
+
due_at: dueAt,
|
|
3211
|
+
completed_at: completedAt,
|
|
3212
|
+
created_at: (_b = this.normalizeDateTimeOrNull(row.created_at)) !== null && _b !== void 0 ? _b : new Date().toISOString(),
|
|
3213
|
+
priority: (this.normalizeTextOrNull(row.priority) || 'medium'),
|
|
3214
|
+
status: this.getCrmActivityStatus(dueAt, completedAt),
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
mapCrmActivityDetailRow(row) {
|
|
3218
|
+
const base = this.mapCrmActivityListRow(row);
|
|
3219
|
+
const createdByUserId = this.coerceNumber(row.created_by_user_id) || null;
|
|
3220
|
+
const completedByUserId = this.coerceNumber(row.completed_by_user_id) || null;
|
|
3221
|
+
return Object.assign(Object.assign({}, base), { source_kind: (this.normalizeTextOrNull(row.source_kind) || 'manual'), created_by_user_id: createdByUserId, created_by_user: createdByUserId
|
|
3222
|
+
? {
|
|
3223
|
+
id: createdByUserId,
|
|
3224
|
+
name: this.normalizeTextOrNull(row.created_by_user_name) ||
|
|
3225
|
+
`#${createdByUserId}`,
|
|
3226
|
+
}
|
|
3227
|
+
: null, completed_by_user_id: completedByUserId, completed_by_user: completedByUserId
|
|
3228
|
+
? {
|
|
3229
|
+
id: completedByUserId,
|
|
3230
|
+
name: this.normalizeTextOrNull(row.completed_by_user_name) ||
|
|
3231
|
+
`#${completedByUserId}`,
|
|
3232
|
+
}
|
|
3233
|
+
: null });
|
|
3234
|
+
}
|
|
3235
|
+
getCrmActivityStatus(dueAt, completedAt) {
|
|
3236
|
+
if (completedAt) {
|
|
3237
|
+
return 'completed';
|
|
3238
|
+
}
|
|
3239
|
+
return new Date(dueAt) < new Date() ? 'overdue' : 'pending';
|
|
3240
|
+
}
|
|
1516
3241
|
normalizeDigits(value) {
|
|
1517
3242
|
return value.replace(/\D/g, '');
|
|
1518
3243
|
}
|
|
1519
3244
|
async findPersonIdsByNormalizedDigits(normalizedDigits) {
|
|
1520
3245
|
const likeValue = `%${normalizedDigits}%`;
|
|
1521
|
-
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
1522
|
-
SELECT DISTINCT p.id
|
|
1523
|
-
FROM person p
|
|
1524
|
-
LEFT JOIN contact c ON c.person_id = p.id
|
|
1525
|
-
LEFT JOIN document d ON d.person_id = p.id
|
|
1526
|
-
LEFT JOIN person_address pa ON pa.person_id = p.id
|
|
1527
|
-
LEFT JOIN address a ON a.id = pa.address_id
|
|
1528
|
-
WHERE regexp_replace(COALESCE(c.value, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
|
|
1529
|
-
OR regexp_replace(COALESCE(d.value, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
|
|
1530
|
-
OR regexp_replace(COALESCE(a.postal_code, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
|
|
3246
|
+
const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
|
|
3247
|
+
SELECT DISTINCT p.id
|
|
3248
|
+
FROM person p
|
|
3249
|
+
LEFT JOIN contact c ON c.person_id = p.id
|
|
3250
|
+
LEFT JOIN document d ON d.person_id = p.id
|
|
3251
|
+
LEFT JOIN person_address pa ON pa.person_id = p.id
|
|
3252
|
+
LEFT JOIN address a ON a.id = pa.address_id
|
|
3253
|
+
WHERE regexp_replace(COALESCE(c.value, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
|
|
3254
|
+
OR regexp_replace(COALESCE(d.value, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
|
|
3255
|
+
OR regexp_replace(COALESCE(a.postal_code, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
|
|
1531
3256
|
`);
|
|
1532
3257
|
return rows.map((row) => row.id);
|
|
1533
3258
|
}
|