@hed-hog/contact 0.0.279 → 0.0.286

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 (73) hide show
  1. package/README.md +2 -0
  2. package/dist/contact.service.d.ts +2 -148
  3. package/dist/contact.service.d.ts.map +1 -1
  4. package/dist/person/dto/create-followup.dto.d.ts +5 -0
  5. package/dist/person/dto/create-followup.dto.d.ts.map +1 -0
  6. package/dist/person/dto/create-followup.dto.js +31 -0
  7. package/dist/person/dto/create-followup.dto.js.map +1 -0
  8. package/dist/person/dto/create-interaction.dto.d.ts +12 -0
  9. package/dist/person/dto/create-interaction.dto.d.ts.map +1 -0
  10. package/dist/person/dto/create-interaction.dto.js +39 -0
  11. package/dist/person/dto/create-interaction.dto.js.map +1 -0
  12. package/dist/person/dto/create.dto.d.ts +24 -0
  13. package/dist/person/dto/create.dto.d.ts.map +1 -1
  14. package/dist/person/dto/create.dto.js +56 -1
  15. package/dist/person/dto/create.dto.js.map +1 -1
  16. package/dist/person/dto/duplicates-query.dto.d.ts +8 -0
  17. package/dist/person/dto/duplicates-query.dto.d.ts.map +1 -0
  18. package/dist/person/dto/duplicates-query.dto.js +45 -0
  19. package/dist/person/dto/duplicates-query.dto.js.map +1 -0
  20. package/dist/person/dto/merge.dto.d.ts +6 -0
  21. package/dist/person/dto/merge.dto.d.ts.map +1 -0
  22. package/dist/person/dto/merge.dto.js +35 -0
  23. package/dist/person/dto/merge.dto.js.map +1 -0
  24. package/dist/person/dto/update-lifecycle-stage.dto.d.ts +13 -0
  25. package/dist/person/dto/update-lifecycle-stage.dto.d.ts.map +1 -0
  26. package/dist/person/dto/update-lifecycle-stage.dto.js +34 -0
  27. package/dist/person/dto/update-lifecycle-stage.dto.js.map +1 -0
  28. package/dist/person/dto/update.dto.d.ts +8 -1
  29. package/dist/person/dto/update.dto.d.ts.map +1 -1
  30. package/dist/person/dto/update.dto.js +36 -0
  31. package/dist/person/dto/update.dto.js.map +1 -1
  32. package/dist/person/person.controller.d.ts +57 -1
  33. package/dist/person/person.controller.d.ts.map +1 -1
  34. package/dist/person/person.controller.js +85 -3
  35. package/dist/person/person.controller.js.map +1 -1
  36. package/dist/person/person.service.d.ts +79 -0
  37. package/dist/person/person.service.d.ts.map +1 -1
  38. package/dist/person/person.service.js +730 -9
  39. package/dist/person/person.service.js.map +1 -1
  40. package/hedhog/data/route.yaml +18 -0
  41. package/hedhog/frontend/app/_components/crm-coming-soon.tsx.ejs +110 -110
  42. package/hedhog/frontend/app/_components/crm-nav.tsx.ejs +73 -73
  43. package/hedhog/frontend/app/_lib/crm-mocks.ts.ejs +498 -256
  44. package/hedhog/frontend/app/_lib/crm-sections.tsx.ejs +81 -81
  45. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +477 -0
  46. package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +62 -0
  47. package/hedhog/frontend/app/accounts/page.tsx.ejs +892 -15
  48. package/hedhog/frontend/app/activities/page.tsx.ejs +812 -15
  49. package/hedhog/frontend/app/contact-type/page.tsx.ejs +105 -91
  50. package/hedhog/frontend/app/dashboard/page.tsx.ejs +491 -573
  51. package/hedhog/frontend/app/document-type/page.tsx.ejs +105 -91
  52. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +696 -15
  53. package/hedhog/frontend/app/page.tsx.ejs +5 -5
  54. package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +1440 -1103
  55. package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +4 -3
  56. package/hedhog/frontend/app/person/_components/person-types.ts.ejs +14 -0
  57. package/hedhog/frontend/app/person/page.tsx.ejs +112 -190
  58. package/hedhog/frontend/app/pipeline/_components/lead-detail-sheet.tsx.ejs +599 -0
  59. package/hedhog/frontend/app/pipeline/page.tsx.ejs +1048 -299
  60. package/hedhog/frontend/app/reports/page.tsx.ejs +15 -15
  61. package/hedhog/frontend/messages/en.json +268 -0
  62. package/hedhog/frontend/messages/pt.json +233 -0
  63. package/package.json +6 -6
  64. package/src/contact.service.ts +2 -2
  65. package/src/person/dto/create-followup.dto.ts +15 -0
  66. package/src/person/dto/create-interaction.dto.ts +23 -0
  67. package/src/person/dto/create.dto.ts +50 -0
  68. package/src/person/dto/duplicates-query.dto.ts +34 -0
  69. package/src/person/dto/merge.dto.ts +15 -0
  70. package/src/person/dto/update-lifecycle-stage.dto.ts +19 -0
  71. package/src/person/dto/update.dto.ts +31 -1
  72. package/src/person/person.controller.ts +63 -2
  73. package/src/person/person.service.ts +1096 -7
@@ -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 create_interaction_dto_1 = require("./dto/create-interaction.dto");
21
22
  const CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING = 'contact-allow-company-registration';
22
23
  const CONTACT_OWNER_ROLE_SLUG = 'owner-contact';
23
24
  const CONTACT_OWNER_ALLOWED_ROLE_SLUGS = [
@@ -27,6 +28,16 @@ const CONTACT_OWNER_ALLOWED_ROLE_SLUGS = [
27
28
  ];
28
29
  const EMPLOYER_COMPANY_METADATA_KEY = 'employer_company_id';
29
30
  const NOTES_METADATA_KEY = 'notes';
31
+ const MERGED_INTO_PERSON_METADATA_KEY = 'merged_into_person_id';
32
+ const OWNER_USER_METADATA_KEY = 'owner_user_id';
33
+ const SOURCE_METADATA_KEY = 'source';
34
+ const LIFECYCLE_STAGE_METADATA_KEY = 'lifecycle_stage';
35
+ const NEXT_ACTION_AT_METADATA_KEY = 'next_action_at';
36
+ const SCORE_METADATA_KEY = 'score';
37
+ const DEAL_VALUE_METADATA_KEY = 'deal_value';
38
+ const TAGS_METADATA_KEY = 'tags';
39
+ const LAST_INTERACTION_AT_METADATA_KEY = 'last_interaction_at';
40
+ const INTERACTIONS_METADATA_KEY = 'interactions';
30
41
  let PersonService = class PersonService {
31
42
  constructor(prismaService, paginationService, fileService, settingService) {
32
43
  this.prismaService = prismaService;
@@ -114,6 +125,151 @@ let PersonService = class PersonService {
114
125
  }
115
126
  return Array.from(byId.values());
116
127
  }
128
+ async checkDuplicates(query) {
129
+ const excludedPersonId = this.coerceNumber(query.person_id);
130
+ const normalizedEmail = this.normalizeEmail(query.email);
131
+ const normalizedPhone = this.normalizeDigits(query.phone || '');
132
+ const normalizedDocument = this.normalizeDigits(query.document_value || '');
133
+ const documentTypeId = this.coerceNumber(query.document_type_id);
134
+ if (!normalizedEmail && !normalizedPhone && !normalizedDocument) {
135
+ return { hasDuplicates: false, matches: [] };
136
+ }
137
+ const reasonsByPersonId = new Map();
138
+ if (normalizedEmail) {
139
+ const emailRows = await this.prismaService.contact.findMany({
140
+ where: Object.assign(Object.assign({}, (excludedPersonId > 0
141
+ ? {
142
+ person_id: {
143
+ not: excludedPersonId,
144
+ },
145
+ }
146
+ : {})), { contact_type: {
147
+ code: {
148
+ equals: 'EMAIL',
149
+ mode: 'insensitive',
150
+ },
151
+ }, value: {
152
+ equals: normalizedEmail,
153
+ mode: 'insensitive',
154
+ } }),
155
+ select: {
156
+ person_id: true,
157
+ },
158
+ });
159
+ for (const row of emailRows) {
160
+ this.addDuplicateReason(reasonsByPersonId, row.person_id, 'email');
161
+ }
162
+ }
163
+ if (normalizedPhone) {
164
+ const excludedPersonFilter = excludedPersonId > 0
165
+ ? api_prisma_1.Prisma.sql ` AND c.person_id <> ${excludedPersonId}`
166
+ : 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}
174
+ `);
175
+ for (const row of phoneRows) {
176
+ this.addDuplicateReason(reasonsByPersonId, row.person_id, 'phone');
177
+ }
178
+ }
179
+ if (normalizedDocument) {
180
+ const excludedPersonFilter = excludedPersonId > 0
181
+ ? api_prisma_1.Prisma.sql ` AND d.person_id <> ${excludedPersonId}`
182
+ : api_prisma_1.Prisma.empty;
183
+ const documentTypeFilter = documentTypeId > 0
184
+ ? api_prisma_1.Prisma.sql ` AND d.document_type_id = ${documentTypeId}`
185
+ : 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}
192
+ `);
193
+ for (const row of documentRows) {
194
+ this.addDuplicateReason(reasonsByPersonId, row.person_id, 'document');
195
+ }
196
+ }
197
+ const duplicateIds = Array.from(reasonsByPersonId.keys());
198
+ if (duplicateIds.length === 0) {
199
+ return { hasDuplicates: false, matches: [] };
200
+ }
201
+ const people = await this.prismaService.person.findMany({
202
+ where: {
203
+ id: {
204
+ in: duplicateIds,
205
+ },
206
+ },
207
+ select: {
208
+ id: true,
209
+ name: true,
210
+ },
211
+ orderBy: {
212
+ name: 'asc',
213
+ },
214
+ });
215
+ const matches = people.map((person) => ({
216
+ id: person.id,
217
+ name: person.name,
218
+ reasons: Array.from(reasonsByPersonId.get(person.id) || []),
219
+ }));
220
+ return {
221
+ hasDuplicates: matches.length > 0,
222
+ matches,
223
+ };
224
+ }
225
+ async merge(data, locale) {
226
+ const sourcePersonId = this.coerceNumber(data.source_person_id);
227
+ const targetPersonId = this.coerceNumber(data.target_person_id);
228
+ if (sourcePersonId <= 0 || targetPersonId <= 0) {
229
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.idMustBeInteger', locale, 'Source and target person IDs must be valid integers.'));
230
+ }
231
+ if (sourcePersonId === targetPersonId) {
232
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personMergeSameRecord', locale, 'Source and target person must be different records.'));
233
+ }
234
+ const [sourcePerson, targetPerson] = await Promise.all([
235
+ this.prismaService.person.findUnique({
236
+ where: { id: sourcePersonId },
237
+ select: { id: true, name: true, type: true },
238
+ }),
239
+ this.prismaService.person.findUnique({
240
+ where: { id: targetPersonId },
241
+ select: { id: true, name: true, type: true },
242
+ }),
243
+ ]);
244
+ if (!sourcePerson) {
245
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person with ID ${sourcePersonId} not found`));
246
+ }
247
+ if (!targetPerson) {
248
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person with ID ${targetPersonId} not found`));
249
+ }
250
+ if (sourcePerson.type !== targetPerson.type) {
251
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personMergeTypeMismatch', locale, 'Only records with the same type can be merged.'));
252
+ }
253
+ await this.prismaService.$transaction(async (tx) => {
254
+ await this.mergeContacts(tx, sourcePersonId, targetPersonId);
255
+ await this.mergeDocuments(tx, sourcePersonId, targetPersonId);
256
+ await this.mergeAddresses(tx, sourcePersonId, targetPersonId);
257
+ await this.mergeMetadata(tx, sourcePersonId, targetPersonId);
258
+ await tx.person.update({
259
+ where: { id: sourcePersonId },
260
+ data: {
261
+ status: 'inactive',
262
+ },
263
+ });
264
+ await this.upsertMetadataValue(tx, sourcePersonId, MERGED_INTO_PERSON_METADATA_KEY, targetPersonId);
265
+ });
266
+ return {
267
+ success: true,
268
+ source_person_id: sourcePersonId,
269
+ target_person_id: targetPersonId,
270
+ strategy: 'contact_only',
271
+ };
272
+ }
117
273
  async list(paginationParams, currentUserId) {
118
274
  var _a;
119
275
  const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
@@ -136,6 +292,42 @@ let PersonService = class PersonService {
136
292
  if (paginationParams.status && paginationParams.status !== 'all') {
137
293
  where.status = paginationParams.status;
138
294
  }
295
+ const ownerUserId = this.resolveRequestedOwnerUserId(paginationParams.owner_user_id, paginationParams.mine, currentUserId);
296
+ const metadataFilters = [];
297
+ if (ownerUserId > 0) {
298
+ metadataFilters.push({
299
+ person_metadata: {
300
+ some: {
301
+ key: OWNER_USER_METADATA_KEY,
302
+ value: ownerUserId,
303
+ },
304
+ },
305
+ });
306
+ }
307
+ if (paginationParams.source && paginationParams.source !== 'all') {
308
+ metadataFilters.push({
309
+ person_metadata: {
310
+ some: {
311
+ key: SOURCE_METADATA_KEY,
312
+ value: paginationParams.source,
313
+ },
314
+ },
315
+ });
316
+ }
317
+ if (paginationParams.lifecycle_stage &&
318
+ paginationParams.lifecycle_stage !== 'all') {
319
+ metadataFilters.push({
320
+ person_metadata: {
321
+ some: {
322
+ key: LIFECYCLE_STAGE_METADATA_KEY,
323
+ value: paginationParams.lifecycle_stage,
324
+ },
325
+ },
326
+ });
327
+ }
328
+ if (metadataFilters.length > 0) {
329
+ where.AND = [...(Array.isArray(where.AND) ? where.AND : []), ...metadataFilters];
330
+ }
139
331
  if (search) {
140
332
  where.OR = await this.buildSearchFilters(search);
141
333
  }
@@ -179,6 +371,71 @@ let PersonService = class PersonService {
179
371
  const [normalized] = await this.enrichPeople([person], allowCompanyRegistration);
180
372
  return normalized;
181
373
  }
374
+ async listInteractions(id, locale) {
375
+ const person = await this.ensurePersonAccessible(id, locale);
376
+ const metadata = await this.prismaService.person_metadata.findFirst({
377
+ where: {
378
+ person_id: person.id,
379
+ key: INTERACTIONS_METADATA_KEY,
380
+ },
381
+ select: {
382
+ value: true,
383
+ },
384
+ });
385
+ return this.metadataToInteractions(metadata === null || metadata === void 0 ? void 0 : metadata.value);
386
+ }
387
+ async createInteraction(id, data, locale, user) {
388
+ const person = await this.ensurePersonAccessible(id, locale);
389
+ const interaction = this.buildInteractionRecord(data, user);
390
+ await this.prismaService.$transaction(async (tx) => {
391
+ const currentInteractions = await this.loadInteractionsFromTx(tx, person.id);
392
+ const nextInteractions = this.sortInteractions([
393
+ interaction,
394
+ ...currentInteractions,
395
+ ]);
396
+ await this.upsertMetadataValue(tx, person.id, INTERACTIONS_METADATA_KEY, nextInteractions);
397
+ await this.upsertMetadataValue(tx, person.id, LAST_INTERACTION_AT_METADATA_KEY, interaction.created_at);
398
+ });
399
+ return interaction;
400
+ }
401
+ async scheduleFollowup(id, data, locale, user) {
402
+ const person = await this.ensurePersonAccessible(id, locale);
403
+ const normalizedNextActionAt = this.normalizeDateTimeOrNull(data.next_action_at);
404
+ if (!normalizedNextActionAt) {
405
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.dateMustBeString', locale, 'next_action_at must be a valid datetime.'));
406
+ }
407
+ await this.prismaService.$transaction(async (tx) => {
408
+ await this.upsertMetadataValue(tx, person.id, NEXT_ACTION_AT_METADATA_KEY, normalizedNextActionAt);
409
+ const notes = this.normalizeTextOrNull(data.notes);
410
+ if (notes) {
411
+ const interaction = this.buildInteractionRecord({
412
+ type: create_interaction_dto_1.PersonInteractionTypeDTO.NOTE,
413
+ notes,
414
+ }, user);
415
+ const currentInteractions = await this.loadInteractionsFromTx(tx, person.id);
416
+ const nextInteractions = this.sortInteractions([
417
+ interaction,
418
+ ...currentInteractions,
419
+ ]);
420
+ await this.upsertMetadataValue(tx, person.id, INTERACTIONS_METADATA_KEY, nextInteractions);
421
+ await this.upsertMetadataValue(tx, person.id, LAST_INTERACTION_AT_METADATA_KEY, interaction.created_at);
422
+ }
423
+ });
424
+ return {
425
+ success: true,
426
+ next_action_at: normalizedNextActionAt,
427
+ };
428
+ }
429
+ async updateLifecycleStage(id, data, locale) {
430
+ const person = await this.ensurePersonAccessible(id, locale);
431
+ await this.prismaService.$transaction(async (tx) => {
432
+ await this.upsertMetadataValue(tx, person.id, LIFECYCLE_STAGE_METADATA_KEY, data.lifecycle_stage);
433
+ });
434
+ return {
435
+ success: true,
436
+ lifecycle_stage: data.lifecycle_stage,
437
+ };
438
+ }
182
439
  async create(data, locale) {
183
440
  const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
184
441
  await this.ensureCompanyRegistrationAllowed({
@@ -201,6 +458,13 @@ let PersonService = class PersonService {
201
458
  employer_company_id: allowCompanyRegistration
202
459
  ? (_b = data.employer_company_id) !== null && _b !== void 0 ? _b : null
203
460
  : null,
461
+ owner_user_id: data.owner_user_id,
462
+ source: data.source,
463
+ lifecycle_stage: data.lifecycle_stage,
464
+ next_action_at: data.next_action_at,
465
+ score: data.score,
466
+ deal_value: data.deal_value,
467
+ tags: data.tags,
204
468
  });
205
469
  return person;
206
470
  });
@@ -246,6 +510,13 @@ let PersonService = class PersonService {
246
510
  ? undefined
247
511
  : data.employer_company_id
248
512
  : null,
513
+ owner_user_id: data.owner_user_id,
514
+ source: data.source,
515
+ lifecycle_stage: data.lifecycle_stage,
516
+ next_action_at: data.next_action_at,
517
+ score: data.score,
518
+ deal_value: data.deal_value,
519
+ tags: data.tags,
249
520
  });
250
521
  await this.syncContacts(tx, id, incomingContacts);
251
522
  await this.syncAddresses(tx, id, incomingAddresses, locale);
@@ -385,6 +656,66 @@ let PersonService = class PersonService {
385
656
  employerCompanyIdByPersonId.set(meta.person_id, employerId);
386
657
  }
387
658
  }
659
+ const ownerUserIdByPersonId = new Map();
660
+ const sourceByPersonId = new Map();
661
+ const lifecycleStageByPersonId = new Map();
662
+ const nextActionAtByPersonId = new Map();
663
+ const scoreByPersonId = new Map();
664
+ const dealValueByPersonId = new Map();
665
+ const tagsByPersonId = new Map();
666
+ const lastInteractionAtByPersonId = new Map();
667
+ const interactionCountByPersonId = new Map();
668
+ for (const person of people) {
669
+ const metadata = this.metadataArrayToMap(person.person_metadata);
670
+ const ownerUserId = this.metadataToNumber(metadata.get(OWNER_USER_METADATA_KEY));
671
+ if (ownerUserId > 0) {
672
+ ownerUserIdByPersonId.set(person.id, ownerUserId);
673
+ }
674
+ const source = this.metadataToString(metadata.get(SOURCE_METADATA_KEY));
675
+ if (source) {
676
+ sourceByPersonId.set(person.id, source);
677
+ }
678
+ const lifecycleStage = this.metadataToString(metadata.get(LIFECYCLE_STAGE_METADATA_KEY));
679
+ if (lifecycleStage) {
680
+ lifecycleStageByPersonId.set(person.id, lifecycleStage);
681
+ }
682
+ const nextActionAt = this.metadataToIsoString(metadata.get(NEXT_ACTION_AT_METADATA_KEY));
683
+ if (nextActionAt) {
684
+ nextActionAtByPersonId.set(person.id, nextActionAt);
685
+ }
686
+ const score = this.metadataToNumber(metadata.get(SCORE_METADATA_KEY));
687
+ if (score != null) {
688
+ scoreByPersonId.set(person.id, score);
689
+ }
690
+ const dealValue = this.metadataToNumber(metadata.get(DEAL_VALUE_METADATA_KEY));
691
+ if (dealValue != null) {
692
+ dealValueByPersonId.set(person.id, dealValue);
693
+ }
694
+ const tags = this.metadataToStringArray(metadata.get(TAGS_METADATA_KEY));
695
+ if (tags.length > 0) {
696
+ tagsByPersonId.set(person.id, tags);
697
+ }
698
+ const lastInteractionAt = this.metadataToIsoString(metadata.get(LAST_INTERACTION_AT_METADATA_KEY));
699
+ if (lastInteractionAt) {
700
+ lastInteractionAtByPersonId.set(person.id, lastInteractionAt);
701
+ }
702
+ interactionCountByPersonId.set(person.id, this.metadataToInteractions(metadata.get(INTERACTIONS_METADATA_KEY)).length);
703
+ }
704
+ const ownerUserIds = Array.from(new Set(ownerUserIdByPersonId.values()));
705
+ const ownerUsers = ownerUserIds.length > 0
706
+ ? await this.prismaService.user.findMany({
707
+ where: {
708
+ id: {
709
+ in: ownerUserIds,
710
+ },
711
+ },
712
+ select: {
713
+ id: true,
714
+ name: true,
715
+ },
716
+ })
717
+ : [];
718
+ const ownerUserById = new Map(ownerUsers.map((item) => [item.id, { id: item.id, name: item.name || `#${item.id}` }]));
388
719
  const employerCompanyIds = Array.from(new Set(Array.from(employerCompanyIdByPersonId.values())));
389
720
  const employerCompanies = employerCompanyIds.length > 0
390
721
  ? await this.prismaService.person.findMany({
@@ -410,7 +741,7 @@ let PersonService = class PersonService {
410
741
  ];
411
742
  }));
412
743
  return people.map((person) => {
413
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
744
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
414
745
  const metadata = this.metadataArrayToMap(person.person_metadata);
415
746
  const companyData = companyById.get(person.id);
416
747
  const individualData = individualById.get(person.id);
@@ -420,23 +751,34 @@ let PersonService = class PersonService {
420
751
  const employerCompany = allowCompanyRegistration && employerCompanyId != null
421
752
  ? this.normalizeRelationPersonSummary(employerCompanyById.get(employerCompanyId))
422
753
  : null;
754
+ const ownerUserId = (_b = ownerUserIdByPersonId.get(person.id)) !== null && _b !== void 0 ? _b : null;
423
755
  return {
424
756
  id: person.id,
425
757
  name: person.name,
426
758
  type: person.type,
427
759
  status: person.status,
428
- avatar_id: (_b = person.avatar_id) !== null && _b !== void 0 ? _b : null,
760
+ avatar_id: (_c = person.avatar_id) !== null && _c !== void 0 ? _c : null,
429
761
  created_at: person.created_at,
430
762
  updated_at: person.updated_at,
431
- birth_date: (_c = individualData === null || individualData === void 0 ? void 0 : individualData.birth_date) !== null && _c !== void 0 ? _c : null,
432
- gender: (_d = individualData === null || individualData === void 0 ? void 0 : individualData.gender) !== null && _d !== void 0 ? _d : null,
433
- job_title: (_e = individualData === null || individualData === void 0 ? void 0 : individualData.job_title) !== null && _e !== void 0 ? _e : null,
434
- trade_name: (_f = companyData === null || companyData === void 0 ? void 0 : companyData.trade_name) !== null && _f !== void 0 ? _f : null,
435
- foundation_date: (_g = companyData === null || companyData === void 0 ? void 0 : companyData.foundation_date) !== null && _g !== void 0 ? _g : null,
436
- legal_nature: (_h = companyData === null || companyData === void 0 ? void 0 : companyData.legal_nature) !== null && _h !== void 0 ? _h : null,
437
- headquarter_id: (_j = companyData === null || companyData === void 0 ? void 0 : companyData.headquarter_id) !== null && _j !== void 0 ? _j : null,
763
+ birth_date: (_d = individualData === null || individualData === void 0 ? void 0 : individualData.birth_date) !== null && _d !== void 0 ? _d : null,
764
+ gender: (_e = individualData === null || individualData === void 0 ? void 0 : individualData.gender) !== null && _e !== void 0 ? _e : null,
765
+ job_title: (_f = individualData === null || individualData === void 0 ? void 0 : individualData.job_title) !== null && _f !== void 0 ? _f : null,
766
+ trade_name: (_g = companyData === null || companyData === void 0 ? void 0 : companyData.trade_name) !== null && _g !== void 0 ? _g : null,
767
+ foundation_date: (_h = companyData === null || companyData === void 0 ? void 0 : companyData.foundation_date) !== null && _h !== void 0 ? _h : null,
768
+ legal_nature: (_j = companyData === null || companyData === void 0 ? void 0 : companyData.legal_nature) !== null && _j !== void 0 ? _j : null,
769
+ headquarter_id: (_k = companyData === null || companyData === void 0 ? void 0 : companyData.headquarter_id) !== null && _k !== void 0 ? _k : null,
438
770
  branch_ids: branchesByHeadquarterId.get(person.id) || [],
439
771
  notes: this.metadataToString(metadata.get(NOTES_METADATA_KEY)),
772
+ owner_user_id: ownerUserId,
773
+ owner_user: ownerUserId ? (_l = ownerUserById.get(ownerUserId)) !== null && _l !== void 0 ? _l : null : null,
774
+ source: (_m = sourceByPersonId.get(person.id)) !== null && _m !== void 0 ? _m : null,
775
+ lifecycle_stage: (_o = lifecycleStageByPersonId.get(person.id)) !== null && _o !== void 0 ? _o : 'new',
776
+ next_action_at: (_p = nextActionAtByPersonId.get(person.id)) !== null && _p !== void 0 ? _p : null,
777
+ score: (_q = scoreByPersonId.get(person.id)) !== null && _q !== void 0 ? _q : 0,
778
+ deal_value: (_r = dealValueByPersonId.get(person.id)) !== null && _r !== void 0 ? _r : 0,
779
+ tags: (_s = tagsByPersonId.get(person.id)) !== null && _s !== void 0 ? _s : [],
780
+ last_interaction_at: (_w = (_t = lastInteractionAtByPersonId.get(person.id)) !== null && _t !== void 0 ? _t : (_v = (_u = person.created_at) === null || _u === void 0 ? void 0 : _u.toISOString) === null || _v === void 0 ? void 0 : _v.call(_u)) !== null && _w !== void 0 ? _w : null,
781
+ interaction_count: (_x = interactionCountByPersonId.get(person.id)) !== null && _x !== void 0 ? _x : 0,
440
782
  employer_company_id: allowCompanyRegistration ? employerCompanyId : null,
441
783
  employer_company: allowCompanyRegistration ? employerCompany : null,
442
784
  contact: person.contact || [],
@@ -466,6 +808,61 @@ let PersonService = class PersonService {
466
808
  }
467
809
  return null;
468
810
  }
811
+ metadataToNumber(value) {
812
+ if (typeof value === 'number' && Number.isFinite(value)) {
813
+ return value;
814
+ }
815
+ if (typeof value === 'string') {
816
+ const parsed = Number(value);
817
+ return Number.isFinite(parsed) ? parsed : null;
818
+ }
819
+ return null;
820
+ }
821
+ metadataToIsoString(value) {
822
+ if (!value)
823
+ return null;
824
+ const parsed = new Date(String(value));
825
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
826
+ }
827
+ metadataToStringArray(value) {
828
+ if (Array.isArray(value)) {
829
+ return value
830
+ .map((item) => this.normalizeTextOrNull(item))
831
+ .filter((item) => Boolean(item));
832
+ }
833
+ if (typeof value === 'string') {
834
+ return value
835
+ .split(',')
836
+ .map((item) => item.trim())
837
+ .filter(Boolean);
838
+ }
839
+ return [];
840
+ }
841
+ metadataToInteractions(value) {
842
+ if (!Array.isArray(value)) {
843
+ return [];
844
+ }
845
+ return this.sortInteractions(value
846
+ .map((item) => {
847
+ if (!item || typeof item !== 'object')
848
+ return null;
849
+ const interaction = item;
850
+ const createdAt = this.metadataToIsoString(interaction.created_at);
851
+ const type = this.normalizeTextOrNull(interaction.type);
852
+ if (!createdAt || !type) {
853
+ return null;
854
+ }
855
+ return {
856
+ id: this.coerceNumber(interaction.id) || Date.now(),
857
+ type: type,
858
+ notes: this.normalizeTextOrNull(interaction.notes),
859
+ created_at: createdAt,
860
+ user_id: this.coerceNumber(interaction.user_id) || null,
861
+ user_name: this.normalizeTextOrNull(interaction.user_name),
862
+ };
863
+ })
864
+ .filter((item) => item != null));
865
+ }
469
866
  async syncPersonSubtypeData(tx, personId, currentType, data, locale) {
470
867
  var _a, _b, _c;
471
868
  const targetType = (_a = data.type) !== null && _a !== void 0 ? _a : currentType;
@@ -586,6 +983,38 @@ let PersonService = class PersonService {
586
983
  if (data.employer_company_id !== undefined) {
587
984
  await this.upsertMetadataValue(tx, personId, EMPLOYER_COMPANY_METADATA_KEY, data.employer_company_id);
588
985
  }
986
+ if (data.owner_user_id !== undefined) {
987
+ await this.upsertMetadataValue(tx, personId, OWNER_USER_METADATA_KEY, data.owner_user_id);
988
+ }
989
+ if (data.source !== undefined) {
990
+ await this.upsertMetadataValue(tx, personId, SOURCE_METADATA_KEY, data.source);
991
+ }
992
+ if (data.lifecycle_stage !== undefined) {
993
+ await this.upsertMetadataValue(tx, personId, LIFECYCLE_STAGE_METADATA_KEY, data.lifecycle_stage);
994
+ }
995
+ if (data.next_action_at !== undefined) {
996
+ await this.upsertMetadataValue(tx, personId, NEXT_ACTION_AT_METADATA_KEY, this.normalizeDateTimeOrNull(data.next_action_at));
997
+ }
998
+ if (data.score !== undefined) {
999
+ await this.upsertMetadataValue(tx, personId, SCORE_METADATA_KEY, data.score);
1000
+ }
1001
+ if (data.deal_value !== undefined) {
1002
+ await this.upsertMetadataValue(tx, personId, DEAL_VALUE_METADATA_KEY, data.deal_value);
1003
+ }
1004
+ if (data.tags !== undefined) {
1005
+ const normalizedTags = Array.isArray(data.tags)
1006
+ ? data.tags
1007
+ .map((item) => this.normalizeTextOrNull(item))
1008
+ .filter((item) => Boolean(item))
1009
+ : null;
1010
+ await this.upsertMetadataValue(tx, personId, TAGS_METADATA_KEY, normalizedTags);
1011
+ }
1012
+ if (data.last_interaction_at !== undefined) {
1013
+ await this.upsertMetadataValue(tx, personId, LAST_INTERACTION_AT_METADATA_KEY, this.normalizeDateTimeOrNull(data.last_interaction_at));
1014
+ }
1015
+ if (data.interactions !== undefined) {
1016
+ await this.upsertMetadataValue(tx, personId, INTERACTIONS_METADATA_KEY, data.interactions);
1017
+ }
589
1018
  }
590
1019
  async upsertMetadataValue(tx, personId, key, value) {
591
1020
  const existing = await tx.person_metadata.findFirst({
@@ -630,6 +1059,14 @@ let PersonService = class PersonService {
630
1059
  if (typeof value === 'boolean') {
631
1060
  return value;
632
1061
  }
1062
+ if (Array.isArray(value) || typeof value === 'object') {
1063
+ try {
1064
+ return JSON.parse(JSON.stringify(value));
1065
+ }
1066
+ catch (_a) {
1067
+ return null;
1068
+ }
1069
+ }
633
1070
  return null;
634
1071
  }
635
1072
  async syncContacts(tx, personId, incomingContacts) {
@@ -709,6 +1146,234 @@ let PersonService = class PersonService {
709
1146
  }
710
1147
  }
711
1148
  }
1149
+ addDuplicateReason(reasonsByPersonId, personId, reason) {
1150
+ var _a;
1151
+ if (!reasonsByPersonId.has(personId)) {
1152
+ reasonsByPersonId.set(personId, new Set());
1153
+ }
1154
+ (_a = reasonsByPersonId.get(personId)) === null || _a === void 0 ? void 0 : _a.add(reason);
1155
+ }
1156
+ async mergeContacts(tx, sourcePersonId, targetPersonId) {
1157
+ const [sourceContacts, targetContacts] = await Promise.all([
1158
+ tx.contact.findMany({ where: { person_id: sourcePersonId } }),
1159
+ tx.contact.findMany({ where: { person_id: targetPersonId } }),
1160
+ ]);
1161
+ const existingByKey = new Set(targetContacts.map((contact) => this.buildContactDedupKey(contact.contact_type_id, contact.value)));
1162
+ const hasPrimaryType = new Set(targetContacts
1163
+ .filter((contact) => contact.is_primary)
1164
+ .map((contact) => Number(contact.contact_type_id)));
1165
+ for (const sourceContact of sourceContacts) {
1166
+ const dedupKey = this.buildContactDedupKey(sourceContact.contact_type_id, sourceContact.value);
1167
+ if (existingByKey.has(dedupKey)) {
1168
+ continue;
1169
+ }
1170
+ const keepPrimary = sourceContact.is_primary &&
1171
+ !hasPrimaryType.has(Number(sourceContact.contact_type_id));
1172
+ await tx.contact.create({
1173
+ data: {
1174
+ person_id: targetPersonId,
1175
+ contact_type_id: sourceContact.contact_type_id,
1176
+ value: sourceContact.value,
1177
+ is_primary: keepPrimary,
1178
+ },
1179
+ });
1180
+ existingByKey.add(dedupKey);
1181
+ if (keepPrimary) {
1182
+ hasPrimaryType.add(Number(sourceContact.contact_type_id));
1183
+ }
1184
+ }
1185
+ await tx.contact.deleteMany({
1186
+ where: {
1187
+ person_id: sourcePersonId,
1188
+ },
1189
+ });
1190
+ }
1191
+ async mergeDocuments(tx, sourcePersonId, targetPersonId) {
1192
+ const [sourceDocuments, targetDocuments] = await Promise.all([
1193
+ tx.document.findMany({ where: { person_id: sourcePersonId } }),
1194
+ tx.document.findMany({ where: { person_id: targetPersonId } }),
1195
+ ]);
1196
+ const existingByKey = new Set(targetDocuments.map((document) => this.buildDocumentDedupKey(document.document_type_id, document.value)));
1197
+ for (const sourceDocument of sourceDocuments) {
1198
+ const dedupKey = this.buildDocumentDedupKey(sourceDocument.document_type_id, sourceDocument.value);
1199
+ if (existingByKey.has(dedupKey)) {
1200
+ continue;
1201
+ }
1202
+ await tx.document.create({
1203
+ data: {
1204
+ person_id: targetPersonId,
1205
+ document_type_id: sourceDocument.document_type_id,
1206
+ value: sourceDocument.value,
1207
+ },
1208
+ });
1209
+ existingByKey.add(dedupKey);
1210
+ }
1211
+ await tx.document.deleteMany({
1212
+ where: {
1213
+ person_id: sourcePersonId,
1214
+ },
1215
+ });
1216
+ }
1217
+ async mergeAddresses(tx, sourcePersonId, targetPersonId) {
1218
+ const [sourceLinks, targetLinks] = await Promise.all([
1219
+ tx.person_address.findMany({
1220
+ where: { person_id: sourcePersonId },
1221
+ include: { address: true },
1222
+ }),
1223
+ tx.person_address.findMany({
1224
+ where: { person_id: targetPersonId },
1225
+ include: { address: true },
1226
+ }),
1227
+ ]);
1228
+ const targetByKey = new Map();
1229
+ const hasPrimaryType = new Set();
1230
+ for (const link of targetLinks) {
1231
+ if (!link.address)
1232
+ continue;
1233
+ targetByKey.set(this.buildAddressDedupKey(link.address), link.address);
1234
+ if (link.address.is_primary && link.address.address_type) {
1235
+ hasPrimaryType.add(String(link.address.address_type));
1236
+ }
1237
+ }
1238
+ for (const sourceLink of sourceLinks) {
1239
+ if (!sourceLink.address) {
1240
+ await tx.person_address.delete({ where: { id: sourceLink.id } });
1241
+ continue;
1242
+ }
1243
+ const sourceAddress = sourceLink.address;
1244
+ const dedupKey = this.buildAddressDedupKey(sourceAddress);
1245
+ const existingTargetAddress = targetByKey.get(dedupKey);
1246
+ if (existingTargetAddress) {
1247
+ if (sourceAddress.is_primary &&
1248
+ sourceAddress.address_type &&
1249
+ !hasPrimaryType.has(String(sourceAddress.address_type))) {
1250
+ await tx.address.update({
1251
+ where: { id: existingTargetAddress.id },
1252
+ data: {
1253
+ is_primary: true,
1254
+ },
1255
+ });
1256
+ hasPrimaryType.add(String(sourceAddress.address_type));
1257
+ }
1258
+ await tx.person_address.delete({ where: { id: sourceLink.id } });
1259
+ await tx.address.delete({ where: { id: sourceAddress.id } });
1260
+ continue;
1261
+ }
1262
+ const keepPrimary = sourceAddress.is_primary &&
1263
+ sourceAddress.address_type &&
1264
+ !hasPrimaryType.has(String(sourceAddress.address_type));
1265
+ await tx.person_address.update({
1266
+ where: { id: sourceLink.id },
1267
+ data: {
1268
+ person_id: targetPersonId,
1269
+ },
1270
+ });
1271
+ if (Boolean(sourceAddress.is_primary) !== Boolean(keepPrimary)) {
1272
+ await tx.address.update({
1273
+ where: { id: sourceAddress.id },
1274
+ data: {
1275
+ is_primary: Boolean(keepPrimary),
1276
+ },
1277
+ });
1278
+ }
1279
+ targetByKey.set(dedupKey, sourceAddress);
1280
+ if (keepPrimary && sourceAddress.address_type) {
1281
+ hasPrimaryType.add(String(sourceAddress.address_type));
1282
+ }
1283
+ }
1284
+ }
1285
+ async mergeMetadata(tx, sourcePersonId, targetPersonId) {
1286
+ const [sourceMetadata, targetMetadata] = await Promise.all([
1287
+ tx.person_metadata.findMany({ where: { person_id: sourcePersonId } }),
1288
+ tx.person_metadata.findMany({ where: { person_id: targetPersonId } }),
1289
+ ]);
1290
+ const targetByKey = new Map();
1291
+ for (const metadata of targetMetadata) {
1292
+ if (!targetByKey.has(metadata.key)) {
1293
+ targetByKey.set(metadata.key, metadata);
1294
+ }
1295
+ }
1296
+ for (const sourceItem of sourceMetadata) {
1297
+ if (sourceItem.key === MERGED_INTO_PERSON_METADATA_KEY) {
1298
+ await tx.person_metadata.delete({ where: { id: sourceItem.id } });
1299
+ continue;
1300
+ }
1301
+ const targetItem = targetByKey.get(sourceItem.key);
1302
+ if (!targetItem) {
1303
+ await tx.person_metadata.update({
1304
+ where: { id: sourceItem.id },
1305
+ data: {
1306
+ person_id: targetPersonId,
1307
+ },
1308
+ });
1309
+ targetByKey.set(sourceItem.key, sourceItem);
1310
+ continue;
1311
+ }
1312
+ if (sourceItem.key === NOTES_METADATA_KEY) {
1313
+ const targetNotes = this.metadataToString(targetItem.value);
1314
+ const sourceNotes = this.metadataToString(sourceItem.value);
1315
+ if (sourceNotes && sourceNotes !== targetNotes) {
1316
+ const nextNotes = [targetNotes, sourceNotes].filter(Boolean).join('\n\n');
1317
+ await tx.person_metadata.update({
1318
+ where: { id: targetItem.id },
1319
+ data: {
1320
+ value: nextNotes,
1321
+ },
1322
+ });
1323
+ }
1324
+ }
1325
+ else if (sourceItem.key === TAGS_METADATA_KEY) {
1326
+ const mergedTags = Array.from(new Set([
1327
+ ...this.metadataToStringArray(targetItem.value),
1328
+ ...this.metadataToStringArray(sourceItem.value),
1329
+ ]));
1330
+ await tx.person_metadata.update({
1331
+ where: { id: targetItem.id },
1332
+ data: {
1333
+ value: mergedTags,
1334
+ },
1335
+ });
1336
+ }
1337
+ else if (sourceItem.key === INTERACTIONS_METADATA_KEY) {
1338
+ const mergedInteractions = this.sortInteractions([
1339
+ ...this.metadataToInteractions(targetItem.value),
1340
+ ...this.metadataToInteractions(sourceItem.value),
1341
+ ]);
1342
+ await tx.person_metadata.update({
1343
+ where: { id: targetItem.id },
1344
+ data: {
1345
+ value: mergedInteractions,
1346
+ },
1347
+ });
1348
+ }
1349
+ await tx.person_metadata.delete({ where: { id: sourceItem.id } });
1350
+ }
1351
+ }
1352
+ buildContactDedupKey(contactTypeId, value) {
1353
+ return `${contactTypeId}::${this.normalizeText(value).toLowerCase()}`;
1354
+ }
1355
+ buildDocumentDedupKey(documentTypeId, value) {
1356
+ const normalizedDigits = this.normalizeDigits(value || '');
1357
+ const normalizedValue = normalizedDigits || this.normalizeText(value).toLowerCase();
1358
+ return `${documentTypeId}::${normalizedValue}`;
1359
+ }
1360
+ buildAddressDedupKey(address) {
1361
+ const parts = [
1362
+ this.normalizeText(address.line1 || ''),
1363
+ this.normalizeText(address.line2 || ''),
1364
+ this.normalizeText(address.city || ''),
1365
+ this.normalizeText(address.state || ''),
1366
+ this.normalizeText(address.country_code || ''),
1367
+ this.normalizeDigits(address.postal_code || ''),
1368
+ ];
1369
+ return parts.join('::').toLowerCase();
1370
+ }
1371
+ normalizeEmail(value) {
1372
+ return this.normalizeText(value || '').toLowerCase();
1373
+ }
1374
+ normalizeText(value) {
1375
+ return String(value || '').trim();
1376
+ }
712
1377
  validateSinglePrimaryPerType(items, groupKey, locale, localeKey, fallbackMessage) {
713
1378
  const map = new Map();
714
1379
  for (const item of items) {
@@ -736,10 +1401,66 @@ let PersonService = class PersonService {
736
1401
  const trimmed = value.trim();
737
1402
  return trimmed.length > 0 ? trimmed : null;
738
1403
  }
1404
+ normalizeDateTimeOrNull(value) {
1405
+ if (!value)
1406
+ return null;
1407
+ const date = value instanceof Date ? value : new Date(String(value));
1408
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
1409
+ }
739
1410
  coerceNumber(value) {
740
1411
  const parsed = Number(value);
741
1412
  return Number.isFinite(parsed) ? parsed : 0;
742
1413
  }
1414
+ resolveRequestedOwnerUserId(ownerUserId, mine, currentUserId) {
1415
+ if (mine === true ||
1416
+ mine === 'true' ||
1417
+ mine === '1') {
1418
+ return Number(currentUserId) > 0 ? Number(currentUserId) : 0;
1419
+ }
1420
+ return this.coerceNumber(ownerUserId);
1421
+ }
1422
+ async ensurePersonAccessible(id, locale) {
1423
+ const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
1424
+ const person = await this.prismaService.person.findUnique({
1425
+ where: { id },
1426
+ select: {
1427
+ id: true,
1428
+ type: true,
1429
+ },
1430
+ });
1431
+ if (!person) {
1432
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person with ID ${id} not found`));
1433
+ }
1434
+ if (!allowCompanyRegistration && person.type === 'company') {
1435
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person with ID ${id} not found`));
1436
+ }
1437
+ return person;
1438
+ }
1439
+ async loadInteractionsFromTx(tx, personId) {
1440
+ const metadata = await tx.person_metadata.findFirst({
1441
+ where: {
1442
+ person_id: personId,
1443
+ key: INTERACTIONS_METADATA_KEY,
1444
+ },
1445
+ select: {
1446
+ value: true,
1447
+ },
1448
+ });
1449
+ return this.metadataToInteractions(metadata === null || metadata === void 0 ? void 0 : metadata.value);
1450
+ }
1451
+ buildInteractionRecord(data, user) {
1452
+ return {
1453
+ id: Date.now() + Math.floor(Math.random() * 1000),
1454
+ type: data.type,
1455
+ notes: this.normalizeTextOrNull(data.notes),
1456
+ created_at: new Date().toISOString(),
1457
+ user_id: Number(user === null || user === void 0 ? void 0 : user.id) > 0 ? Number(user.id) : null,
1458
+ user_name: this.normalizeTextOrNull(user === null || user === void 0 ? void 0 : user.name) || null,
1459
+ };
1460
+ }
1461
+ sortInteractions(interactions) {
1462
+ return [...interactions].sort((left, right) => new Date(right.created_at).getTime() - new Date(left.created_at).getTime());
1463
+ }
743
1464
  async buildSearchFilters(search) {
744
1465
  const normalizedDigits = this.normalizeDigits(search);
745
1466
  const filters = [];