@hed-hog/contact 0.0.295 → 0.0.297

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/contact-type/contact-type.controller.d.ts +1 -1
  2. package/dist/contact-type/contact-type.service.d.ts +1 -1
  3. package/dist/document-type/document-type.controller.d.ts +1 -1
  4. package/dist/document-type/document-type.service.d.ts +1 -1
  5. package/dist/person/dto/reports-query.dto.d.ts +8 -0
  6. package/dist/person/dto/reports-query.dto.d.ts.map +1 -0
  7. package/dist/person/dto/reports-query.dto.js +33 -0
  8. package/dist/person/dto/reports-query.dto.js.map +1 -0
  9. package/dist/person/person.controller.d.ts +67 -10
  10. package/dist/person/person.controller.d.ts.map +1 -1
  11. package/dist/person/person.controller.js +26 -6
  12. package/dist/person/person.controller.js.map +1 -1
  13. package/dist/person/person.service.d.ts +61 -5
  14. package/dist/person/person.service.d.ts.map +1 -1
  15. package/dist/person/person.service.js +656 -298
  16. package/dist/person/person.service.js.map +1 -1
  17. package/dist/person-relation-type/person-relation-type.controller.d.ts +2 -2
  18. package/dist/person-relation-type/person-relation-type.service.d.ts +2 -2
  19. package/hedhog/data/menu.yaml +163 -163
  20. package/hedhog/data/route.yaml +68 -60
  21. package/hedhog/frontend/app/_lib/crm-sections.tsx.ejs +9 -9
  22. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +573 -573
  23. package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +9 -9
  24. package/hedhog/frontend/app/accounts/page.tsx.ejs +970 -970
  25. package/hedhog/frontend/app/activities/_components/activity-detail-sheet.tsx.ejs +240 -240
  26. package/hedhog/frontend/app/activities/_components/activity-types.ts.ejs +66 -66
  27. package/hedhog/frontend/app/activities/page.tsx.ejs +460 -460
  28. package/hedhog/frontend/app/dashboard/_components/dashboard-types.ts.ejs +70 -70
  29. package/hedhog/frontend/app/dashboard/page.tsx.ejs +639 -639
  30. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +785 -785
  31. package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +10 -12
  32. package/hedhog/frontend/app/reports/_components/report-types.ts.ejs +84 -0
  33. package/hedhog/frontend/app/reports/page.tsx.ejs +1196 -15
  34. package/hedhog/frontend/messages/en.json +242 -123
  35. package/hedhog/frontend/messages/pt.json +242 -123
  36. package/hedhog/table/crm_activity.yaml +68 -68
  37. package/hedhog/table/crm_stage_history.yaml +34 -0
  38. package/hedhog/table/person_company.yaml +27 -27
  39. package/package.json +9 -9
  40. package/src/person/dto/account.dto.ts +100 -100
  41. package/src/person/dto/activity.dto.ts +54 -54
  42. package/src/person/dto/dashboard-query.dto.ts +25 -25
  43. package/src/person/dto/followup-query.dto.ts +25 -25
  44. package/src/person/dto/reports-query.dto.ts +25 -0
  45. package/src/person/person.controller.ts +176 -159
  46. package/src/person/person.service.ts +4825 -4288
@@ -56,6 +56,14 @@ const CRM_DASHBOARD_SOURCE_ORDER = [
56
56
  'outbound',
57
57
  'other',
58
58
  ];
59
+ const CRM_REPORT_ACTIVITY_ORDER = [
60
+ 'call',
61
+ 'email',
62
+ 'meeting',
63
+ 'whatsapp',
64
+ 'task',
65
+ 'note',
66
+ ];
59
67
  let PersonService = class PersonService {
60
68
  constructor(prismaService, paginationService, fileService, settingService) {
61
69
  this.prismaService = prismaService;
@@ -197,6 +205,235 @@ let PersonService = class PersonService {
197
205
  lists,
198
206
  };
199
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
+ }
200
437
  async getOwnerOptions(currentUserId) {
201
438
  const where = {
202
439
  OR: [
@@ -281,13 +518,13 @@ let PersonService = class PersonService {
281
518
  const excludedPersonFilter = excludedPersonId > 0
282
519
  ? api_prisma_1.Prisma.sql ` AND c.person_id <> ${excludedPersonId}`
283
520
  : api_prisma_1.Prisma.empty;
284
- const phoneRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
285
- SELECT DISTINCT c.person_id
286
- FROM contact c
287
- JOIN contact_type ct ON ct.id = c.contact_type_id
288
- WHERE UPPER(ct.code) IN ('PHONE', 'MOBILE', 'WHATSAPP')
289
- AND regexp_replace(COALESCE(c.value, ''), '[^0-9]+', '', 'g') = ${normalizedPhone}
290
- ${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}
291
528
  `);
292
529
  for (const row of phoneRows) {
293
530
  this.addDuplicateReason(reasonsByPersonId, row.person_id, 'phone');
@@ -300,12 +537,12 @@ let PersonService = class PersonService {
300
537
  const documentTypeFilter = documentTypeId > 0
301
538
  ? api_prisma_1.Prisma.sql ` AND d.document_type_id = ${documentTypeId}`
302
539
  : api_prisma_1.Prisma.empty;
303
- const documentRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
304
- SELECT DISTINCT d.person_id
305
- FROM document d
306
- WHERE regexp_replace(COALESCE(d.value, ''), '[^0-9]+', '', 'g') = ${normalizedDocument}
307
- ${excludedPersonFilter}
308
- ${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}
309
546
  `);
310
547
  for (const row of documentRows) {
311
548
  this.addDuplicateReason(reasonsByPersonId, row.person_id, 'document');
@@ -480,26 +717,26 @@ let PersonService = class PersonService {
480
717
  lifecycleStage: paginationParams.lifecycle_stage,
481
718
  });
482
719
  const orderBy = this.getAccountOrderBySql(paginationParams.sortField, paginationParams.sortOrder);
483
- const totalRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
484
- SELECT COUNT(*) AS total
485
- FROM person p
486
- INNER JOIN person_company pc ON pc.id = p.id
487
- WHERE p.type = 'company'
488
- ${filters}
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}
489
726
  `);
490
727
  const total = this.coerceCount((_a = totalRows[0]) === null || _a === void 0 ? void 0 : _a.total);
491
728
  if (total === 0) {
492
729
  return this.createEmptyAccountPagination(page, pageSize);
493
730
  }
494
- const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
495
- SELECT p.id AS person_id
496
- FROM person p
497
- INNER JOIN person_company pc ON pc.id = p.id
498
- WHERE p.type = 'company'
499
- ${filters}
500
- ORDER BY ${orderBy}, p.id ASC
501
- LIMIT ${pageSize}
502
- OFFSET ${skip}
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}
503
740
  `);
504
741
  const personIds = rows.map((row) => row.person_id).filter((id) => id > 0);
505
742
  if (personIds.length === 0) {
@@ -547,15 +784,15 @@ let PersonService = class PersonService {
547
784
  prospects: 0,
548
785
  };
549
786
  }
550
- const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
551
- SELECT
552
- COUNT(*) AS total,
553
- COUNT(*) FILTER (WHERE p.status = 'active') AS active,
554
- COUNT(*) FILTER (WHERE pc.account_lifecycle_stage = 'customer') AS customers,
555
- COUNT(*) FILTER (WHERE pc.account_lifecycle_stage = 'prospect') AS prospects
556
- FROM person p
557
- INNER JOIN person_company pc ON pc.id = p.id
558
- WHERE p.type = 'company'
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'
559
796
  `);
560
797
  return {
561
798
  total: this.coerceCount((_a = rows[0]) === null || _a === void 0 ? void 0 : _a.total),
@@ -665,44 +902,44 @@ let PersonService = class PersonService {
665
902
  type: paginationParams.type,
666
903
  priority: paginationParams.priority,
667
904
  });
668
- const totalRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
669
- SELECT COUNT(*) AS total
670
- FROM crm_activity a
671
- INNER JOIN person p ON p.id = a.person_id
672
- LEFT JOIN "user" owner_user ON owner_user.id = a.owner_user_id
673
- WHERE 1 = 1
674
- ${filters}
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}
675
912
  `);
676
913
  const total = this.coerceCount((_a = totalRows[0]) === null || _a === void 0 ? void 0 : _a.total);
677
914
  if (total === 0) {
678
915
  return this.createEmptyCrmActivityPagination(page, pageSize);
679
916
  }
680
- const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
681
- SELECT
682
- a.id,
683
- a.person_id,
684
- a.owner_user_id,
685
- a.type,
686
- a.subject,
687
- a.notes,
688
- a.due_at,
689
- a.completed_at,
690
- a.created_at,
691
- a.priority,
692
- p.name AS person_name,
693
- p.type AS person_type,
694
- p.status AS person_status,
695
- pc.trade_name AS person_trade_name,
696
- owner_user.name AS owner_user_name
697
- FROM crm_activity a
698
- INNER JOIN person p ON p.id = a.person_id
699
- LEFT JOIN person_company pc ON pc.id = p.id
700
- LEFT JOIN "user" owner_user ON owner_user.id = a.owner_user_id
701
- WHERE 1 = 1
702
- ${filters}
703
- ORDER BY a.due_at ASC, a.id ASC
704
- LIMIT ${pageSize}
705
- OFFSET ${skip}
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}
706
943
  `);
707
944
  const data = rows.map((row) => this.mapCrmActivityListRow(row));
708
945
  const lastPage = Math.max(1, Math.ceil(total / pageSize));
@@ -722,24 +959,24 @@ let PersonService = class PersonService {
722
959
  const visibilityFilter = allowCompanyRegistration
723
960
  ? api_prisma_1.Prisma.empty
724
961
  : api_prisma_1.Prisma.sql `AND p.type = 'individual'`;
725
- const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
726
- SELECT
727
- COUNT(*) AS total,
728
- COUNT(*) FILTER (
729
- WHERE a.completed_at IS NULL
730
- AND a.due_at >= NOW()
731
- ) AS pending,
732
- COUNT(*) FILTER (
733
- WHERE a.completed_at IS NULL
734
- AND a.due_at < NOW()
735
- ) AS overdue,
736
- COUNT(*) FILTER (
737
- WHERE a.completed_at IS NOT NULL
738
- ) AS completed
739
- FROM crm_activity a
740
- INNER JOIN person p ON p.id = a.person_id
741
- WHERE 1 = 1
742
- ${visibilityFilter}
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}
743
980
  `);
744
981
  return {
745
982
  total: this.coerceCount((_a = rows[0]) === null || _a === void 0 ? void 0 : _a.total),
@@ -753,37 +990,37 @@ let PersonService = class PersonService {
753
990
  const visibilityFilter = allowCompanyRegistration
754
991
  ? api_prisma_1.Prisma.empty
755
992
  : api_prisma_1.Prisma.sql `AND p.type = 'individual'`;
756
- const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
757
- SELECT
758
- a.id,
759
- a.person_id,
760
- a.owner_user_id,
761
- a.created_by_user_id,
762
- a.completed_by_user_id,
763
- a.type,
764
- a.subject,
765
- a.notes,
766
- a.due_at,
767
- a.completed_at,
768
- a.created_at,
769
- a.priority,
770
- a.source_kind,
771
- p.name AS person_name,
772
- p.type AS person_type,
773
- p.status AS person_status,
774
- pc.trade_name AS person_trade_name,
775
- owner_user.name AS owner_user_name,
776
- created_by_user.name AS created_by_user_name,
777
- completed_by_user.name AS completed_by_user_name
778
- FROM crm_activity a
779
- INNER JOIN person p ON p.id = a.person_id
780
- LEFT JOIN person_company pc ON pc.id = p.id
781
- LEFT JOIN "user" owner_user ON owner_user.id = a.owner_user_id
782
- LEFT JOIN "user" created_by_user ON created_by_user.id = a.created_by_user_id
783
- LEFT JOIN "user" completed_by_user ON completed_by_user.id = a.completed_by_user_id
784
- WHERE a.id = ${id}
785
- ${visibilityFilter}
786
- LIMIT 1
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
787
1024
  `);
788
1025
  const row = rows[0];
789
1026
  if (!row) {
@@ -798,17 +1035,17 @@ let PersonService = class PersonService {
798
1035
  ? api_prisma_1.Prisma.empty
799
1036
  : api_prisma_1.Prisma.sql `AND p.type = 'individual'`;
800
1037
  return this.prismaService.$transaction(async (tx) => {
801
- const rows = (await tx.$queryRaw(api_prisma_1.Prisma.sql `
802
- SELECT
803
- a.id,
804
- a.person_id,
805
- a.completed_at,
806
- a.source_kind
807
- FROM crm_activity a
808
- INNER JOIN person p ON p.id = a.person_id
809
- WHERE a.id = ${id}
810
- ${visibilityFilter}
811
- LIMIT 1
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
812
1049
  `));
813
1050
  const activity = rows[0];
814
1051
  if (!activity) {
@@ -821,13 +1058,13 @@ let PersonService = class PersonService {
821
1058
  };
822
1059
  }
823
1060
  const completedAt = new Date();
824
- await tx.$executeRaw(api_prisma_1.Prisma.sql `
825
- UPDATE crm_activity
826
- SET
827
- completed_at = CAST(${completedAt.toISOString()} AS TIMESTAMPTZ),
828
- completed_by_user_id = ${actorUserId},
829
- updated_at = NOW()
830
- WHERE id = ${id}
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}
831
1068
  `);
832
1069
  if (activity.source_kind === 'followup') {
833
1070
  await this.upsertMetadataValue(tx, activity.person_id, NEXT_ACTION_AT_METADATA_KEY, null);
@@ -856,30 +1093,30 @@ let PersonService = class PersonService {
856
1093
  dateTo: paginationParams.date_to,
857
1094
  });
858
1095
  const followupTimestampSql = this.getFollowupTimestampSql();
859
- const totalRows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
860
- SELECT COUNT(*) AS total
861
- FROM person p
862
- INNER JOIN person_metadata pm_next
863
- ON pm_next.person_id = p.id
864
- AND pm_next.key = ${NEXT_ACTION_AT_METADATA_KEY}
865
- WHERE 1 = 1
866
- ${filters}
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}
867
1104
  `);
868
1105
  const total = this.coerceCount((_a = totalRows[0]) === null || _a === void 0 ? void 0 : _a.total);
869
1106
  if (total === 0) {
870
1107
  return this.createEmptyFollowupPagination(page, pageSize);
871
1108
  }
872
- const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
873
- SELECT p.id AS person_id
874
- FROM person p
875
- INNER JOIN person_metadata pm_next
876
- ON pm_next.person_id = p.id
877
- AND pm_next.key = ${NEXT_ACTION_AT_METADATA_KEY}
878
- WHERE 1 = 1
879
- ${filters}
880
- ORDER BY ${followupTimestampSql} ASC, p.id ASC
881
- LIMIT ${pageSize}
882
- OFFSET ${skip}
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}
883
1120
  `);
884
1121
  const personIds = rows.map((row) => row.person_id).filter((id) => id > 0);
885
1122
  const people = personIds.length > 0
@@ -948,25 +1185,25 @@ let PersonService = class PersonService {
948
1185
  });
949
1186
  const followupTimestampSql = this.getFollowupTimestampSql();
950
1187
  const { todayStartIso, tomorrowStartIso } = this.getFollowupDayBoundaryIsoStrings();
951
- const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
952
- SELECT
953
- COUNT(*) AS total,
954
- COUNT(*) FILTER (
955
- WHERE ${followupTimestampSql} >= CAST(${todayStartIso} AS TIMESTAMPTZ)
956
- AND ${followupTimestampSql} < CAST(${tomorrowStartIso} AS TIMESTAMPTZ)
957
- ) AS today,
958
- COUNT(*) FILTER (
959
- WHERE ${followupTimestampSql} < CAST(${todayStartIso} AS TIMESTAMPTZ)
960
- ) AS overdue,
961
- COUNT(*) FILTER (
962
- WHERE ${followupTimestampSql} >= CAST(${tomorrowStartIso} AS TIMESTAMPTZ)
963
- ) AS upcoming
964
- FROM person p
965
- INNER JOIN person_metadata pm_next
966
- ON pm_next.person_id = p.id
967
- AND pm_next.key = ${NEXT_ACTION_AT_METADATA_KEY}
968
- WHERE 1 = 1
969
- ${filters}
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}
970
1207
  `);
971
1208
  return {
972
1209
  total: this.coerceCount((_a = rows[0]) === null || _a === void 0 ? void 0 : _a.total),
@@ -1069,10 +1306,21 @@ let PersonService = class PersonService {
1069
1306
  next_action_at: normalizedNextActionAt,
1070
1307
  };
1071
1308
  }
1072
- async updateLifecycleStage(id, data, locale) {
1309
+ async updateLifecycleStage(id, data, locale, user) {
1073
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;
1074
1314
  await this.prismaService.$transaction(async (tx) => {
1075
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
+ }
1076
1324
  });
1077
1325
  return {
1078
1326
  success: true,
@@ -1112,7 +1360,7 @@ let PersonService = class PersonService {
1112
1360
  return person;
1113
1361
  });
1114
1362
  }
1115
- async update(id, data, locale) {
1363
+ async update(id, data, locale, user) {
1116
1364
  const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
1117
1365
  const person = await this.prismaService.person.findUnique({ where: { id } });
1118
1366
  if (!person) {
@@ -1126,6 +1374,8 @@ let PersonService = class PersonService {
1126
1374
  nextType: data.type,
1127
1375
  locale,
1128
1376
  });
1377
+ const currentLifecycleStage = await this.getPersonLifecycleStage(id);
1378
+ const nextLifecycleStage = this.normalizeTextOrNull(data.lifecycle_stage);
1129
1379
  const incomingContacts = Array.isArray(data.contacts) ? data.contacts : [];
1130
1380
  const incomingAddresses = Array.isArray(data.addresses) ? data.addresses : [];
1131
1381
  const incomingDocuments = Array.isArray(data.documents) ? data.documents : [];
@@ -1164,6 +1414,14 @@ let PersonService = class PersonService {
1164
1414
  await this.syncContacts(tx, id, incomingContacts);
1165
1415
  await this.syncAddresses(tx, id, incomingAddresses, locale);
1166
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
+ }
1167
1425
  return { success: true };
1168
1426
  })
1169
1427
  .then(async (result) => {
@@ -1401,6 +1659,64 @@ let PersonService = class PersonService {
1401
1659
  },
1402
1660
  };
1403
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
+ }
1404
1720
  buildDashboardOwnerPerformance(people) {
1405
1721
  var _a, _b;
1406
1722
  const byOwnerId = new Map();
@@ -1476,7 +1792,7 @@ let PersonService = class PersonService {
1476
1792
  return [];
1477
1793
  }
1478
1794
  const personIds = people.map((person) => person.id);
1479
- const [companies, individuals, companyBranches, employerMetadata] = await Promise.all([
1795
+ const [companies, individuals, companyBranches, employerMetadataRaw] = await Promise.all([
1480
1796
  this.prismaService.person_company.findMany({
1481
1797
  where: { id: { in: personIds } },
1482
1798
  }),
@@ -1496,8 +1812,9 @@ let PersonService = class PersonService {
1496
1812
  person_id: true,
1497
1813
  value: true,
1498
1814
  },
1499
- }).then((metadata) => (allowCompanyRegistration ? metadata : [])),
1815
+ }),
1500
1816
  ]);
1817
+ const employerMetadata = allowCompanyRegistration ? employerMetadataRaw : [];
1501
1818
  const companyById = new Map(companies.map((item) => [item.id, item]));
1502
1819
  const individualById = new Map(individuals.map((item) => [item.id, item]));
1503
1820
  const branchesByHeadquarterId = new Map();
@@ -1722,6 +2039,47 @@ let PersonService = class PersonService {
1722
2039
  })
1723
2040
  .filter((item) => item != null));
1724
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
+ }
1725
2083
  async syncPersonSubtypeData(tx, personId, currentType, data, locale) {
1726
2084
  var _a, _b, _c;
1727
2085
  const targetType = (_a = data.type) !== null && _a !== void 0 ? _a : currentType;
@@ -2365,94 +2723,94 @@ let PersonService = class PersonService {
2365
2723
  return ownerUserId && ownerUserId > 0 ? ownerUserId : null;
2366
2724
  }
2367
2725
  async upsertFollowupActivity(tx, { personId, ownerUserId, dueAt, notes, actorUserId, }) {
2368
- const existingRows = (await tx.$queryRaw(api_prisma_1.Prisma.sql `
2369
- SELECT id
2370
- FROM crm_activity
2371
- WHERE person_id = ${personId}
2372
- AND source_kind = 'followup'
2373
- AND completed_at IS NULL
2374
- ORDER BY id DESC
2375
- LIMIT 1
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
2376
2734
  `));
2377
2735
  const existing = existingRows[0];
2378
2736
  const normalizedNotes = this.normalizeTextOrNull(notes);
2379
2737
  if (existing) {
2380
- await tx.$executeRaw(api_prisma_1.Prisma.sql `
2381
- UPDATE crm_activity
2382
- SET
2383
- owner_user_id = ${ownerUserId},
2384
- type = CAST(${'task'} AS crm_activity_type_enum),
2385
- subject = ${this.getFollowupActivitySubject()},
2386
- notes = ${normalizedNotes},
2387
- due_at = CAST(${dueAt} AS TIMESTAMPTZ),
2388
- priority = CAST(${'medium'} AS crm_activity_priority_enum),
2389
- updated_at = NOW()
2390
- WHERE id = ${existing.id}
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}
2391
2749
  `);
2392
2750
  return;
2393
2751
  }
2394
- await tx.$executeRaw(api_prisma_1.Prisma.sql `
2395
- INSERT INTO crm_activity (
2396
- person_id,
2397
- owner_user_id,
2398
- created_by_user_id,
2399
- type,
2400
- subject,
2401
- notes,
2402
- due_at,
2403
- priority,
2404
- source_kind,
2405
- created_at,
2406
- updated_at
2407
- )
2408
- VALUES (
2409
- ${personId},
2410
- ${ownerUserId},
2411
- ${actorUserId},
2412
- CAST(${'task'} AS crm_activity_type_enum),
2413
- ${this.getFollowupActivitySubject()},
2414
- ${normalizedNotes},
2415
- CAST(${dueAt} AS TIMESTAMPTZ),
2416
- CAST(${'medium'} AS crm_activity_priority_enum),
2417
- CAST(${'followup'} AS crm_activity_source_kind_enum),
2418
- NOW(),
2419
- NOW()
2420
- )
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
+ )
2421
2779
  `);
2422
2780
  }
2423
2781
  async createCompletedInteractionActivity(tx, { personId, ownerUserId, interaction, actorUserId, }) {
2424
2782
  const completedAt = new Date(interaction.created_at);
2425
- await tx.$executeRaw(api_prisma_1.Prisma.sql `
2426
- INSERT INTO crm_activity (
2427
- person_id,
2428
- owner_user_id,
2429
- created_by_user_id,
2430
- completed_by_user_id,
2431
- type,
2432
- subject,
2433
- notes,
2434
- due_at,
2435
- completed_at,
2436
- priority,
2437
- source_kind,
2438
- created_at,
2439
- updated_at
2440
- )
2441
- VALUES (
2442
- ${personId},
2443
- ${ownerUserId},
2444
- ${actorUserId},
2445
- ${actorUserId},
2446
- CAST(${interaction.type} AS crm_activity_type_enum),
2447
- ${this.getInteractionActivitySubject(interaction.type)},
2448
- ${this.normalizeTextOrNull(interaction.notes)},
2449
- CAST(${interaction.created_at} AS TIMESTAMPTZ),
2450
- CAST(${interaction.created_at} AS TIMESTAMPTZ),
2451
- CAST(${'medium'} AS crm_activity_priority_enum),
2452
- CAST(${'interaction'} AS crm_activity_source_kind_enum),
2453
- CAST(${interaction.created_at} AS TIMESTAMPTZ),
2454
- NOW()
2455
- )
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
+ )
2456
2814
  `);
2457
2815
  }
2458
2816
  getFollowupActivitySubject() {
@@ -2722,33 +3080,33 @@ let PersonService = class PersonService {
2722
3080
  const searchLike = `%${search}%`;
2723
3081
  const normalizedDigits = this.normalizeDigits(search);
2724
3082
  const digitsLike = `%${normalizedDigits}%`;
2725
- filters.push(api_prisma_1.Prisma.sql `
2726
- AND (
2727
- p.name ILIKE ${searchLike}
2728
- OR COALESCE(pc.trade_name, '') ILIKE ${searchLike}
2729
- OR COALESCE(pc.city, '') ILIKE ${searchLike}
2730
- OR COALESCE(pc.state, '') ILIKE ${searchLike}
2731
- OR EXISTS (
2732
- SELECT 1
2733
- FROM contact c
2734
- INNER JOIN contact_type ct ON ct.id = c.contact_type_id
2735
- WHERE c.person_id = p.id
2736
- AND UPPER(ct.code) = 'EMAIL'
2737
- AND c.value ILIKE ${searchLike}
2738
- )
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
+ )
2739
3097
  ${normalizedDigits.length > 0
2740
- ? api_prisma_1.Prisma.sql `
2741
- OR EXISTS (
2742
- SELECT 1
2743
- FROM contact c
2744
- INNER JOIN contact_type ct ON ct.id = c.contact_type_id
2745
- WHERE c.person_id = p.id
2746
- AND UPPER(ct.code) IN ('PHONE', 'MOBILE', 'WHATSAPP')
2747
- AND regexp_replace(COALESCE(c.value, ''), '[^0-9]+', '', 'g') ILIKE ${digitsLike}
2748
- )
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
+ )
2749
3107
  `
2750
- : api_prisma_1.Prisma.empty}
2751
- )
3108
+ : api_prisma_1.Prisma.empty}
3109
+ )
2752
3110
  `);
2753
3111
  }
2754
3112
  return filters.length > 0 ? api_prisma_1.Prisma.join(filters, '\n') : api_prisma_1.Prisma.empty;
@@ -2798,13 +3156,13 @@ let PersonService = class PersonService {
2798
3156
  }
2799
3157
  if (search) {
2800
3158
  const searchLike = `%${search}%`;
2801
- filters.push(api_prisma_1.Prisma.sql `
2802
- AND (
2803
- a.subject ILIKE ${searchLike}
2804
- OR COALESCE(a.notes, '') ILIKE ${searchLike}
2805
- OR p.name ILIKE ${searchLike}
2806
- OR COALESCE(owner_user.name, '') ILIKE ${searchLike}
2807
- )
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
+ )
2808
3166
  `);
2809
3167
  }
2810
3168
  return filters.length > 0 ? api_prisma_1.Prisma.join(filters, '\n') : api_prisma_1.Prisma.empty;
@@ -2885,16 +3243,16 @@ let PersonService = class PersonService {
2885
3243
  }
2886
3244
  async findPersonIdsByNormalizedDigits(normalizedDigits) {
2887
3245
  const likeValue = `%${normalizedDigits}%`;
2888
- const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
2889
- SELECT DISTINCT p.id
2890
- FROM person p
2891
- LEFT JOIN contact c ON c.person_id = p.id
2892
- LEFT JOIN document d ON d.person_id = p.id
2893
- LEFT JOIN person_address pa ON pa.person_id = p.id
2894
- LEFT JOIN address a ON a.id = pa.address_id
2895
- WHERE regexp_replace(COALESCE(c.value, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
2896
- OR regexp_replace(COALESCE(d.value, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
2897
- 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}
2898
3256
  `);
2899
3257
  return rows.map((row) => row.id);
2900
3258
  }