@hed-hog/contact 0.0.304 → 0.0.305

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 (44) hide show
  1. package/README.md +225 -17
  2. package/dist/person/dto/account.dto.d.ts +5 -0
  3. package/dist/person/dto/account.dto.d.ts.map +1 -1
  4. package/dist/person/dto/account.dto.js +29 -0
  5. package/dist/person/dto/account.dto.js.map +1 -1
  6. package/dist/person/dto/import-preview.dto.d.ts +7 -0
  7. package/dist/person/dto/import-preview.dto.d.ts.map +1 -0
  8. package/dist/person/dto/import-preview.dto.js +7 -0
  9. package/dist/person/dto/import-preview.dto.js.map +1 -0
  10. package/dist/person/dto/import.dto.d.ts +15 -0
  11. package/dist/person/dto/import.dto.d.ts.map +1 -0
  12. package/dist/person/dto/import.dto.js +51 -0
  13. package/dist/person/dto/import.dto.js.map +1 -0
  14. package/dist/person/person.controller.d.ts +14 -0
  15. package/dist/person/person.controller.d.ts.map +1 -1
  16. package/dist/person/person.controller.js +53 -0
  17. package/dist/person/person.controller.js.map +1 -1
  18. package/dist/person/person.service.d.ts +19 -0
  19. package/dist/person/person.service.d.ts.map +1 -1
  20. package/dist/person/person.service.js +481 -67
  21. package/dist/person/person.service.js.map +1 -1
  22. package/dist/person-relation-type/person-relation-type.controller.d.ts +2 -2
  23. package/dist/person-relation-type/person-relation-type.service.d.ts +2 -2
  24. package/hedhog/data/route.yaml +6 -0
  25. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +2242 -484
  26. package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +51 -0
  27. package/hedhog/frontend/app/accounts/page.tsx.ejs +181 -16
  28. package/hedhog/frontend/app/contact-type/page.tsx.ejs +223 -29
  29. package/hedhog/frontend/app/document-type/page.tsx.ejs +248 -37
  30. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +129 -19
  31. package/hedhog/frontend/app/person/_components/person-field-with-create.tsx.ejs +78 -212
  32. package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +760 -178
  33. package/hedhog/frontend/app/person/_components/person-import-sheet.tsx.ejs +1120 -0
  34. package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +171 -4
  35. package/hedhog/frontend/app/person/page.tsx.ejs +17 -0
  36. package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +160 -35
  37. package/hedhog/frontend/messages/en.json +104 -2
  38. package/hedhog/frontend/messages/pt.json +111 -9
  39. package/package.json +5 -5
  40. package/src/person/dto/account.dto.ts +31 -0
  41. package/src/person/dto/import-preview.dto.ts +6 -0
  42. package/src/person/dto/import.dto.ts +61 -0
  43. package/src/person/person.controller.ts +74 -12
  44. package/src/person/person.service.ts +615 -68
@@ -21,6 +21,7 @@ const core_1 = require("@hed-hog/core");
21
21
  const common_1 = require("@nestjs/common");
22
22
  const account_dto_1 = require("./dto/account.dto");
23
23
  const create_interaction_dto_1 = require("./dto/create-interaction.dto");
24
+ const import_dto_1 = require("./dto/import.dto");
24
25
  const CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING = 'contact-allow-company-registration';
25
26
  const CONTACT_OWNER_ROLE_SLUG = 'owner-contact';
26
27
  const CONTACT_OWNER_ALLOWED_ROLE_SLUGS = [
@@ -655,7 +656,22 @@ let PersonService = PersonService_1 = class PersonService {
655
656
  person_metadata: {
656
657
  some: {
657
658
  key: OWNER_USER_METADATA_KEY,
658
- value: ownerUserId,
659
+ value: {
660
+ equals: ownerUserId,
661
+ },
662
+ },
663
+ },
664
+ });
665
+ }
666
+ const employerCompanyId = this.coerceNumber(paginationParams.employer_company_id);
667
+ if (employerCompanyId > 0) {
668
+ metadataFilters.push({
669
+ person_metadata: {
670
+ some: {
671
+ key: EMPLOYER_COMPANY_METADATA_KEY,
672
+ value: {
673
+ equals: employerCompanyId,
674
+ },
659
675
  },
660
676
  },
661
677
  });
@@ -665,7 +681,9 @@ let PersonService = PersonService_1 = class PersonService {
665
681
  person_metadata: {
666
682
  some: {
667
683
  key: SOURCE_METADATA_KEY,
668
- value: paginationParams.source,
684
+ value: {
685
+ equals: paginationParams.source,
686
+ },
669
687
  },
670
688
  },
671
689
  });
@@ -676,7 +694,9 @@ let PersonService = PersonService_1 = class PersonService {
676
694
  person_metadata: {
677
695
  some: {
678
696
  key: LIFECYCLE_STAGE_METADATA_KEY,
679
- value: paginationParams.lifecycle_stage,
697
+ value: {
698
+ equals: paginationParams.lifecycle_stage,
699
+ },
680
700
  },
681
701
  },
682
702
  });
@@ -824,8 +844,7 @@ let PersonService = PersonService_1 = class PersonService {
824
844
  await this.syncPersonMetadata(tx, person.id, {
825
845
  owner_user_id: data.owner_user_id,
826
846
  });
827
- await this.upsertPrimaryAccountContact(tx, person.id, 'EMAIL', data.email);
828
- await this.upsertPrimaryAccountContact(tx, person.id, 'PHONE', data.phone);
847
+ await this.syncAccountArrays(tx, person.id, data, locale);
829
848
  return {
830
849
  success: true,
831
850
  id: person.id,
@@ -855,8 +874,7 @@ let PersonService = PersonService_1 = class PersonService {
855
874
  await this.syncPersonMetadata(tx, id, {
856
875
  owner_user_id: data.owner_user_id,
857
876
  });
858
- await this.upsertPrimaryAccountContact(tx, id, 'EMAIL', data.email);
859
- await this.upsertPrimaryAccountContact(tx, id, 'PHONE', data.phone);
877
+ await this.syncAccountArrays(tx, id, data, locale);
860
878
  return {
861
879
  success: true,
862
880
  id,
@@ -2071,15 +2089,29 @@ let PersonService = PersonService_1 = class PersonService {
2071
2089
  if (fromStage === toStage) {
2072
2090
  return;
2073
2091
  }
2074
- await client.crm_stage_history.create({
2075
- data: {
2076
- person_id: personId,
2077
- from_stage: fromStage !== null && fromStage !== void 0 ? fromStage : null,
2078
- to_stage: toStage,
2079
- changed_by_user_id: changedByUserId,
2080
- changed_at: new Date(),
2081
- },
2082
- });
2092
+ const fromStageSql = fromStage
2093
+ ? api_prisma_1.Prisma.sql `CAST(${fromStage} AS crm_stage_history_from_stage_f4181e9874_enum)`
2094
+ : api_prisma_1.Prisma.sql `NULL`;
2095
+ await client.$executeRaw(api_prisma_1.Prisma.sql `
2096
+ INSERT INTO crm_stage_history (
2097
+ person_id,
2098
+ from_stage,
2099
+ to_stage,
2100
+ changed_by_user_id,
2101
+ changed_at,
2102
+ created_at,
2103
+ updated_at
2104
+ )
2105
+ VALUES (
2106
+ ${personId},
2107
+ ${fromStageSql},
2108
+ CAST(${toStage} AS crm_stage_history_to_stage_ca5bd55f9f_enum),
2109
+ ${changedByUserId},
2110
+ NOW(),
2111
+ NOW(),
2112
+ NOW()
2113
+ )
2114
+ `);
2083
2115
  }
2084
2116
  async syncPersonSubtypeData(tx, personId, currentType, data, locale) {
2085
2117
  var _a, _b, _c;
@@ -2315,6 +2347,52 @@ let PersonService = PersonService_1 = class PersonService {
2315
2347
  }
2316
2348
  return null;
2317
2349
  }
2350
+ async syncAccountArrays(tx, personId, data, locale) {
2351
+ if (Object.prototype.hasOwnProperty.call(data, 'contacts')) {
2352
+ const incomingContacts = Array.isArray(data.contacts) ? data.contacts : [];
2353
+ this.validateSinglePrimaryPerType(incomingContacts, 'contact_type_id', locale, 'moreThanOnePrimaryContact', 'More than one contact of the same type cannot be marked as primary.');
2354
+ await this.syncContacts(tx, personId, incomingContacts);
2355
+ }
2356
+ if (Object.prototype.hasOwnProperty.call(data, 'addresses')) {
2357
+ const incomingAddresses = Array.isArray(data.addresses) ? data.addresses : [];
2358
+ this.validateSinglePrimaryPerType(incomingAddresses, 'address_type', locale, 'moreThanOnePrimaryAddress', 'More than one address of the same type cannot be marked as primary.');
2359
+ await this.syncAddresses(tx, personId, incomingAddresses, locale);
2360
+ }
2361
+ if (Object.prototype.hasOwnProperty.call(data, 'documents')) {
2362
+ const incomingDocuments = Array.isArray(data.documents) ? data.documents : [];
2363
+ this.validateSinglePrimaryPerType(incomingDocuments, 'document_type_id', locale, 'moreThanOnePrimaryDocument', 'More than one document of the same type cannot be marked as primary.');
2364
+ await this.syncDocuments(tx, personId, incomingDocuments);
2365
+ }
2366
+ if (Object.prototype.hasOwnProperty.call(data, 'email')) {
2367
+ await this.upsertPrimaryAccountContact(tx, personId, 'EMAIL', data.email);
2368
+ }
2369
+ if (Object.prototype.hasOwnProperty.call(data, 'phone')) {
2370
+ await this.upsertPrimaryAccountContact(tx, personId, 'PHONE', data.phone);
2371
+ }
2372
+ if (Object.prototype.hasOwnProperty.call(data, 'collaborator_person_ids')) {
2373
+ await this.attachCollaboratorsToCompany(tx, personId, data.collaborator_person_ids);
2374
+ }
2375
+ }
2376
+ async attachCollaboratorsToCompany(tx, companyId, collaboratorPersonIds) {
2377
+ const normalizedCollaboratorIds = Array.from(new Set((Array.isArray(collaboratorPersonIds) ? collaboratorPersonIds : [])
2378
+ .map((value) => Number(value))
2379
+ .filter((value) => Number.isInteger(value) && value > 0 && value !== companyId)));
2380
+ if (normalizedCollaboratorIds.length === 0) {
2381
+ return;
2382
+ }
2383
+ const collaborators = await tx.person.findMany({
2384
+ where: {
2385
+ id: { in: normalizedCollaboratorIds },
2386
+ type: 'individual',
2387
+ },
2388
+ select: {
2389
+ id: true,
2390
+ },
2391
+ });
2392
+ for (const collaborator of collaborators) {
2393
+ await this.upsertMetadataValue(tx, collaborator.id, EMPLOYER_COMPANY_METADATA_KEY, companyId);
2394
+ }
2395
+ }
2318
2396
  async syncContacts(tx, personId, incomingContacts) {
2319
2397
  const existingContacts = await tx.contact.findMany({ where: { person_id: personId } });
2320
2398
  for (const contact of incomingContacts) {
@@ -2740,62 +2818,86 @@ let PersonService = PersonService_1 = class PersonService {
2740
2818
  const priority = 'medium';
2741
2819
  const type = 'task';
2742
2820
  if (existing) {
2743
- await tx.crm_activity.update({
2744
- where: {
2745
- id: existing.id,
2746
- },
2747
- data: {
2748
- owner_user_id: ownerUserId,
2749
- type,
2750
- subject,
2751
- notes: normalizedNotes,
2752
- due_at: dueAtDate,
2753
- priority,
2754
- updated_at: new Date(),
2755
- },
2756
- });
2821
+ await tx.$executeRaw(api_prisma_1.Prisma.sql `
2822
+ UPDATE crm_activity
2823
+ SET
2824
+ owner_user_id = ${ownerUserId},
2825
+ type = CAST(${'task'} AS crm_activity_type_77c8508dad_enum),
2826
+ subject = ${this.getFollowupActivitySubject()},
2827
+ notes = ${normalizedNotes},
2828
+ due_at = CAST(${dueAt} AS TIMESTAMPTZ),
2829
+ priority = CAST(${'medium'} AS crm_activity_priority_b4e2dbbb4d_enum),
2830
+ updated_at = NOW()
2831
+ WHERE id = ${existing.id}
2832
+ `);
2757
2833
  return;
2758
2834
  }
2759
- const sourceKind = 'followup';
2760
- const now = new Date();
2761
- await tx.crm_activity.create({
2762
- data: {
2763
- person_id: personId,
2764
- owner_user_id: ownerUserId,
2765
- created_by_user_id: actorUserId,
2766
- type,
2767
- subject,
2768
- notes: normalizedNotes,
2769
- due_at: dueAtDate,
2770
- priority,
2771
- source_kind: sourceKind,
2772
- created_at: now,
2773
- updated_at: now,
2774
- },
2775
- });
2835
+ await tx.$executeRaw(api_prisma_1.Prisma.sql `
2836
+ INSERT INTO crm_activity (
2837
+ person_id,
2838
+ owner_user_id,
2839
+ created_by_user_id,
2840
+ type,
2841
+ subject,
2842
+ notes,
2843
+ due_at,
2844
+ priority,
2845
+ source_kind,
2846
+ created_at,
2847
+ updated_at
2848
+ )
2849
+ VALUES (
2850
+ ${personId},
2851
+ ${ownerUserId},
2852
+ ${actorUserId},
2853
+ CAST(${'task'} AS crm_activity_type_77c8508dad_enum),
2854
+ ${this.getFollowupActivitySubject()},
2855
+ ${normalizedNotes},
2856
+ CAST(${dueAt} AS TIMESTAMPTZ),
2857
+ CAST(${'medium'} AS crm_activity_priority_b4e2dbbb4d_enum),
2858
+ CAST(${'followup'} AS crm_activity_source_kind_a09c5de478_enum),
2859
+ NOW(),
2860
+ NOW()
2861
+ )
2862
+ `);
2776
2863
  }
2777
2864
  async createCompletedInteractionActivity(tx, { personId, ownerUserId, interaction, actorUserId, }) {
2778
2865
  const completedAt = new Date(interaction.created_at);
2779
2866
  const type = interaction.type;
2780
2867
  const priority = 'medium';
2781
2868
  const sourceKind = 'interaction';
2782
- await tx.crm_activity.create({
2783
- data: {
2784
- person_id: personId,
2785
- owner_user_id: ownerUserId,
2786
- created_by_user_id: actorUserId,
2787
- completed_by_user_id: actorUserId,
2788
- type,
2789
- subject: this.getInteractionActivitySubject(interaction.type),
2790
- notes: this.normalizeTextOrNull(interaction.notes),
2791
- due_at: completedAt,
2792
- completed_at: completedAt,
2793
- priority,
2794
- source_kind: sourceKind,
2795
- created_at: completedAt,
2796
- updated_at: new Date(),
2797
- },
2798
- });
2869
+ await tx.$executeRaw(api_prisma_1.Prisma.sql `
2870
+ INSERT INTO crm_activity (
2871
+ person_id,
2872
+ owner_user_id,
2873
+ created_by_user_id,
2874
+ completed_by_user_id,
2875
+ type,
2876
+ subject,
2877
+ notes,
2878
+ due_at,
2879
+ completed_at,
2880
+ priority,
2881
+ source_kind,
2882
+ created_at,
2883
+ updated_at
2884
+ )
2885
+ VALUES (
2886
+ ${personId},
2887
+ ${ownerUserId},
2888
+ ${actorUserId},
2889
+ ${actorUserId},
2890
+ CAST(${interaction.type} AS crm_activity_type_77c8508dad_enum),
2891
+ ${this.getInteractionActivitySubject(interaction.type)},
2892
+ ${this.normalizeTextOrNull(interaction.notes)},
2893
+ CAST(${interaction.created_at} AS TIMESTAMPTZ),
2894
+ CAST(${interaction.created_at} AS TIMESTAMPTZ),
2895
+ CAST(${'medium'} AS crm_activity_priority_b4e2dbbb4d_enum),
2896
+ CAST(${'interaction'} AS crm_activity_source_kind_a09c5de478_enum),
2897
+ CAST(${interaction.created_at} AS TIMESTAMPTZ),
2898
+ NOW()
2899
+ )
2900
+ `);
2799
2901
  }
2800
2902
  getFollowupActivitySubject() {
2801
2903
  return 'Follow-up';
@@ -3058,7 +3160,7 @@ let PersonService = PersonService_1 = class PersonService {
3058
3160
  filters.push(api_prisma_1.Prisma.sql `AND p.status = ${status}`);
3059
3161
  }
3060
3162
  if (lifecycleStage && lifecycleStage !== 'all') {
3061
- filters.push(api_prisma_1.Prisma.sql `AND pc.account_lifecycle_stage = ${lifecycleStage}`);
3163
+ filters.push(api_prisma_1.Prisma.sql `AND pc.account_lifecycle_stage = CAST(${lifecycleStage} AS person_company_account_lifecycle_stage_680de09d00_enum)`);
3062
3164
  }
3063
3165
  if (search) {
3064
3166
  const searchLike = `%${search}%`;
@@ -3133,10 +3235,10 @@ let PersonService = PersonService_1 = class PersonService {
3133
3235
  filters.push(api_prisma_1.Prisma.sql `AND a.completed_at IS NOT NULL`);
3134
3236
  }
3135
3237
  if (type && type !== 'all') {
3136
- filters.push(api_prisma_1.Prisma.sql `AND a.type = ${type}`);
3238
+ filters.push(api_prisma_1.Prisma.sql `AND a.type = CAST(${type} AS crm_activity_type_77c8508dad_enum)`);
3137
3239
  }
3138
3240
  if (priority && priority !== 'all') {
3139
- filters.push(api_prisma_1.Prisma.sql `AND a.priority = ${priority}`);
3241
+ filters.push(api_prisma_1.Prisma.sql `AND a.priority = CAST(${priority} AS crm_activity_priority_b4e2dbbb4d_enum)`);
3140
3242
  }
3141
3243
  if (search) {
3142
3244
  const searchLike = `%${search}%`;
@@ -3283,6 +3385,318 @@ let PersonService = PersonService_1 = class PersonService {
3283
3385
  trade_name: tradeName,
3284
3386
  };
3285
3387
  }
3388
+ // ─── CSV Import ──────────────────────────────────────────────────────────────
3389
+ getUploadedFileText(file) {
3390
+ let buffer = file.buffer;
3391
+ if (!buffer || buffer.length === 0) {
3392
+ throw new common_1.BadRequestException('Uploaded file is empty');
3393
+ }
3394
+ const utf8 = buffer.toString('utf8');
3395
+ // Replace UTF-8 BOM if present
3396
+ const withoutBom = utf8.replace(/^\uFEFF/, '');
3397
+ if (withoutBom.includes('\uFFFD')) {
3398
+ return buffer.toString('latin1').replace(/^\uFEFF/, '');
3399
+ }
3400
+ return withoutBom;
3401
+ }
3402
+ parseCsvRaw(content) {
3403
+ const text = content.trimEnd();
3404
+ if (!text)
3405
+ return { columns: [], rows: [] };
3406
+ // Detect delimiter from the first line before parsing the whole content
3407
+ const firstNewlineIdx = text.indexOf('\n');
3408
+ const firstLine = firstNewlineIdx === -1 ? text : text.slice(0, firstNewlineIdx);
3409
+ const delimiter = firstLine.includes(';') ? ';' : ',';
3410
+ // RFC 4180-compliant parser: character-by-character so quoted fields
3411
+ // can span multiple lines without being split into separate records.
3412
+ const records = [];
3413
+ let row = [];
3414
+ let field = '';
3415
+ let inQuotes = false;
3416
+ let i = 0;
3417
+ const pushField = () => {
3418
+ // For unquoted fields trim surrounding whitespace;
3419
+ // for quoted fields the value is already the inner content.
3420
+ row.push(inQuotes ? field : field.trim());
3421
+ field = '';
3422
+ };
3423
+ const pushRow = () => {
3424
+ pushField();
3425
+ // Skip rows where every cell is empty (blank lines between records)
3426
+ if (row.some((c) => c !== '')) {
3427
+ records.push(row);
3428
+ }
3429
+ row = [];
3430
+ inQuotes = false;
3431
+ };
3432
+ while (i < text.length) {
3433
+ const ch = text[i];
3434
+ if (inQuotes) {
3435
+ if (ch === '"') {
3436
+ if (i + 1 < text.length && text[i + 1] === '"') {
3437
+ // Escaped double-quote ("") → literal "
3438
+ field += '"';
3439
+ i += 2;
3440
+ }
3441
+ else {
3442
+ // Closing quote — exit quoted mode; consume any trailing
3443
+ // whitespace before the next delimiter so malformed exports
3444
+ // like `"value" ;` don't include a trailing space.
3445
+ inQuotes = false;
3446
+ i++;
3447
+ }
3448
+ }
3449
+ else {
3450
+ // Newlines inside quoted fields are literal content, not record separators
3451
+ field += ch;
3452
+ i++;
3453
+ }
3454
+ }
3455
+ else {
3456
+ if (ch === '"' && field.trim() === '') {
3457
+ // Opening quote — only valid at the start of a field
3458
+ inQuotes = true;
3459
+ field = '';
3460
+ i++;
3461
+ }
3462
+ else if (ch === delimiter) {
3463
+ pushField();
3464
+ field = '';
3465
+ i++;
3466
+ }
3467
+ else if (ch === '\r' && i + 1 < text.length && text[i + 1] === '\n') {
3468
+ pushRow();
3469
+ i += 2;
3470
+ }
3471
+ else if (ch === '\n') {
3472
+ pushRow();
3473
+ i++;
3474
+ }
3475
+ else {
3476
+ field += ch;
3477
+ i++;
3478
+ }
3479
+ }
3480
+ }
3481
+ // Flush the last record (file may not end with a newline)
3482
+ if (field || row.length > 0) {
3483
+ row.push(inQuotes ? field : field.trim());
3484
+ if (row.some((c) => c !== '')) {
3485
+ records.push(row);
3486
+ }
3487
+ }
3488
+ if (records.length === 0)
3489
+ return { columns: [], rows: [] };
3490
+ const columns = records[0].map((c) => c.trim());
3491
+ const rows = records
3492
+ .slice(1)
3493
+ .filter((r) => r.some((c) => c !== ''))
3494
+ .map((cells) => {
3495
+ const row = {};
3496
+ columns.forEach((col, idx) => {
3497
+ var _a;
3498
+ row[col] = ((_a = cells[idx]) !== null && _a !== void 0 ? _a : '').trim();
3499
+ });
3500
+ return row;
3501
+ });
3502
+ return { columns, rows };
3503
+ }
3504
+ async previewCsvImport(file) {
3505
+ const MAX_ROWS = 5000;
3506
+ const PREVIEW_ROWS = 20;
3507
+ const content = this.getUploadedFileText(file);
3508
+ const { columns, rows } = this.parseCsvRaw(content);
3509
+ if (rows.length > MAX_ROWS) {
3510
+ throw new common_1.BadRequestException(`File exceeds the maximum of ${MAX_ROWS} rows. Split the file and try again.`);
3511
+ }
3512
+ return {
3513
+ fileName: file.originalname,
3514
+ totalEstimated: rows.length,
3515
+ columns,
3516
+ preview: rows.slice(0, PREVIEW_ROWS),
3517
+ };
3518
+ }
3519
+ async importFromCsv(file, mapping, companyId, locale, userId) {
3520
+ var _a, _b, _c;
3521
+ // 1. Validate the linked company, if provided
3522
+ if (companyId) {
3523
+ const company = await this.prismaService.person.findFirst({
3524
+ where: { id: companyId, type: 'company' },
3525
+ select: { id: true },
3526
+ });
3527
+ if (!company) {
3528
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Company with ID ${companyId} not found.`));
3529
+ }
3530
+ }
3531
+ // 1b. Validate mapping field names against allowed CRM fields
3532
+ const validCrmFields = new Set(import_dto_1.CRM_IMPORT_FIELDS);
3533
+ for (const val of Object.values(mapping)) {
3534
+ if (!validCrmFields.has(val)) {
3535
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('validation.nameMustBeString', locale, `Invalid mapping field: "${val}"`));
3536
+ }
3537
+ }
3538
+ // 2. Pre-fetch contact type IDs once (avoid per-row roundtrips)
3539
+ const contactTypes = await this.prismaService.contact_type.findMany({
3540
+ where: { code: { in: ['EMAIL', 'PHONE', 'MOBILE'] } },
3541
+ select: { id: true, code: true },
3542
+ });
3543
+ const contactTypeMap = new Map(contactTypes.map((t) => [t.code.toUpperCase(), t.id]));
3544
+ // 3. Pre-fetch document type IDs once
3545
+ const documentTypes = await this.prismaService.document_type.findMany({
3546
+ where: { code: { in: ['CPF', 'CNPJ'] } },
3547
+ select: { id: true, code: true },
3548
+ });
3549
+ const documentTypeMap = new Map(documentTypes.map((t) => [t.code.toUpperCase(), t.id]));
3550
+ // 4. Parse CSV content
3551
+ const content = this.getUploadedFileText(file);
3552
+ const { rows } = this.parseCsvRaw(content);
3553
+ let imported = 0;
3554
+ let skipped = 0;
3555
+ const errors = [];
3556
+ const BATCH_SIZE = 50;
3557
+ for (let batchStart = 0; batchStart < rows.length; batchStart += BATCH_SIZE) {
3558
+ const batch = rows.slice(batchStart, batchStart + BATCH_SIZE);
3559
+ for (let i = 0; i < batch.length; i++) {
3560
+ const rowIndex = batchStart + i + 2; // +2: 1-indexed and skip header row
3561
+ const row = batch[i];
3562
+ try {
3563
+ // Build a flat object: CRM field → CSV cell value
3564
+ const mapped = {};
3565
+ for (const [csvCol, crmField] of Object.entries(mapping)) {
3566
+ if (crmField !== '_ignore' && row[csvCol] !== undefined && row[csvCol] !== '') {
3567
+ mapped[crmField] = row[csvCol];
3568
+ }
3569
+ }
3570
+ // 'name' is mandatory — skip silently when absent
3571
+ const name = this.normalizeTextOrNull(mapped['name']);
3572
+ if (!name) {
3573
+ skipped++;
3574
+ continue;
3575
+ }
3576
+ // Coerce type (default: individual)
3577
+ const rawType = ((_a = mapped['type']) !== null && _a !== void 0 ? _a : '').toLowerCase();
3578
+ const personType = rawType === 'company' ||
3579
+ rawType === 'empresa' ||
3580
+ rawType === 'jurídica' ||
3581
+ rawType === 'juridica'
3582
+ ? 'company'
3583
+ : 'individual';
3584
+ // Coerce status (default: active)
3585
+ const rawStatus = ((_b = mapped['status']) !== null && _b !== void 0 ? _b : '').toLowerCase();
3586
+ const personStatus = rawStatus === 'inactive' || rawStatus === 'inativo' ? 'inactive' : 'active';
3587
+ // Run each row inside its own transaction so a bad row never rolls
3588
+ // back successfully imported rows
3589
+ await this.prismaService.$transaction(async (tx) => {
3590
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
3591
+ // (a) Base person record
3592
+ const person = await tx.person.create({
3593
+ data: { name, type: personType, status: personStatus },
3594
+ });
3595
+ // (b) Subtype-specific table (person_individual / person_company)
3596
+ await this.syncPersonSubtypeData(tx, person.id, null, {
3597
+ type: personType,
3598
+ job_title: personType === 'individual' ? ((_a = mapped['job_title']) !== null && _a !== void 0 ? _a : null) : null,
3599
+ trade_name: personType === 'company' ? ((_b = mapped['trade_name']) !== null && _b !== void 0 ? _b : null) : null,
3600
+ website: personType === 'company' ? ((_c = mapped['website']) !== null && _c !== void 0 ? _c : null) : null,
3601
+ }, locale);
3602
+ // (c) Metadata (notes, source, employer company, owner)
3603
+ await this.syncPersonMetadata(tx, person.id, {
3604
+ notes: (_d = mapped['notes']) !== null && _d !== void 0 ? _d : undefined,
3605
+ source: (_e = mapped['source']) !== null && _e !== void 0 ? _e : undefined,
3606
+ employer_company_id: personType === 'individual' && companyId ? companyId : undefined,
3607
+ owner_user_id: userId > 0 ? userId : undefined,
3608
+ });
3609
+ // (d) Contacts — email takes priority as primary
3610
+ let needsPrimary = true;
3611
+ const emailTypeId = contactTypeMap.get('EMAIL');
3612
+ if (mapped['email'] && emailTypeId) {
3613
+ await tx.contact.create({
3614
+ data: {
3615
+ person_id: person.id,
3616
+ contact_type_id: emailTypeId,
3617
+ value: mapped['email'],
3618
+ is_primary: true,
3619
+ },
3620
+ });
3621
+ needsPrimary = false;
3622
+ }
3623
+ const phoneTypeId = contactTypeMap.get('PHONE');
3624
+ if (mapped['phone'] && phoneTypeId) {
3625
+ await tx.contact.create({
3626
+ data: {
3627
+ person_id: person.id,
3628
+ contact_type_id: phoneTypeId,
3629
+ value: mapped['phone'],
3630
+ is_primary: needsPrimary,
3631
+ },
3632
+ });
3633
+ needsPrimary = false;
3634
+ }
3635
+ const mobileTypeId = contactTypeMap.get('MOBILE');
3636
+ if (mapped['mobile'] && mobileTypeId) {
3637
+ await tx.contact.create({
3638
+ data: {
3639
+ person_id: person.id,
3640
+ contact_type_id: mobileTypeId,
3641
+ value: mapped['mobile'],
3642
+ is_primary: needsPrimary,
3643
+ },
3644
+ });
3645
+ }
3646
+ // (e) Address — only created when at least one address field is present
3647
+ const hasAddress = mapped['address_street'] ||
3648
+ mapped['address_city'] ||
3649
+ mapped['address_state'] ||
3650
+ mapped['address_zip'] ||
3651
+ mapped['address_country'];
3652
+ if (hasAddress) {
3653
+ const address = await tx.address.create({
3654
+ data: {
3655
+ line1: (_f = mapped['address_street']) !== null && _f !== void 0 ? _f : '',
3656
+ line2: '',
3657
+ city: (_g = mapped['address_city']) !== null && _g !== void 0 ? _g : '',
3658
+ state: (_h = mapped['address_state']) !== null && _h !== void 0 ? _h : '',
3659
+ country_code: mapped['address_country'] || 'BRA',
3660
+ postal_code: (_j = mapped['address_zip']) !== null && _j !== void 0 ? _j : '',
3661
+ is_primary: true,
3662
+ address_type: 'residential',
3663
+ },
3664
+ });
3665
+ await tx.person_address.create({
3666
+ data: { person_id: person.id, address_id: address.id },
3667
+ });
3668
+ }
3669
+ // (f) Documents — the document table has no is_primary column
3670
+ const cpfTypeId = documentTypeMap.get('CPF');
3671
+ if (mapped['cpf'] && cpfTypeId) {
3672
+ await tx.document.create({
3673
+ data: {
3674
+ person_id: person.id,
3675
+ document_type_id: cpfTypeId,
3676
+ value: mapped['cpf'],
3677
+ },
3678
+ });
3679
+ }
3680
+ const cnpjTypeId = documentTypeMap.get('CNPJ');
3681
+ if (mapped['cnpj'] && cnpjTypeId) {
3682
+ await tx.document.create({
3683
+ data: {
3684
+ person_id: person.id,
3685
+ document_type_id: cnpjTypeId,
3686
+ value: mapped['cnpj'],
3687
+ },
3688
+ });
3689
+ }
3690
+ });
3691
+ imported++;
3692
+ }
3693
+ catch (err) {
3694
+ errors.push({ row: rowIndex, message: (_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : 'Unknown error' });
3695
+ }
3696
+ }
3697
+ }
3698
+ return { imported, skipped, errors };
3699
+ }
3286
3700
  };
3287
3701
  exports.PersonService = PersonService;
3288
3702
  exports.PersonService = PersonService = PersonService_1 = __decorate([