@hed-hog/contact 0.0.301 → 0.0.302

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 (36) hide show
  1. package/dist/person/person.service.d.ts +2 -0
  2. package/dist/person/person.service.d.ts.map +1 -1
  3. package/dist/person/person.service.js +111 -127
  4. package/dist/person/person.service.js.map +1 -1
  5. package/dist/person/person.service.spec.d.ts +2 -0
  6. package/dist/person/person.service.spec.d.ts.map +1 -0
  7. package/dist/person/person.service.spec.js +106 -0
  8. package/dist/person/person.service.spec.js.map +1 -0
  9. package/dist/proposal/proposal.service.d.ts +5 -0
  10. package/dist/proposal/proposal.service.d.ts.map +1 -1
  11. package/dist/proposal/proposal.service.js +242 -19
  12. package/dist/proposal/proposal.service.js.map +1 -1
  13. package/dist/proposal/proposal.service.spec.js +153 -165
  14. package/dist/proposal/proposal.service.spec.js.map +1 -1
  15. package/hedhog/data/menu.yaml +35 -18
  16. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +517 -346
  17. package/hedhog/frontend/app/activities/_components/activity-detail-sheet.tsx.ejs +42 -17
  18. package/hedhog/frontend/app/activities/_components/activity-types.ts.ejs +1 -1
  19. package/hedhog/frontend/app/activities/page.tsx.ejs +315 -101
  20. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +172 -22
  21. package/hedhog/frontend/app/page.tsx.ejs +1 -1
  22. package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +1 -1
  23. package/hedhog/frontend/app/pipeline/_components/lead-detail-sheet.tsx.ejs +1 -1
  24. package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +505 -428
  25. package/hedhog/frontend/app/pipeline/page.tsx.ejs +30 -4
  26. package/hedhog/frontend/app/proposals/_components/proposals-management-page.tsx.ejs +773 -0
  27. package/hedhog/frontend/app/proposals/approvals/page.tsx.ejs +5 -0
  28. package/hedhog/frontend/app/proposals/page.tsx.ejs +5 -0
  29. package/hedhog/frontend/app/reports/page.tsx.ejs +431 -375
  30. package/hedhog/frontend/messages/en.json +100 -1
  31. package/hedhog/frontend/messages/pt.json +100 -1
  32. package/package.json +6 -6
  33. package/src/person/person.service.spec.ts +143 -0
  34. package/src/person/person.service.ts +147 -158
  35. package/src/proposal/proposal.service.spec.ts +196 -0
  36. package/src/proposal/proposal.service.ts +348 -18
@@ -11,6 +11,7 @@ import {
11
11
  BadRequestException,
12
12
  Inject,
13
13
  Injectable,
14
+ InternalServerErrorException,
14
15
  Logger,
15
16
  NotFoundException,
16
17
  forwardRef,
@@ -69,11 +70,32 @@ export class ProposalService {
69
70
  }
70
71
 
71
72
  if (search) {
73
+ const matchingCompanyRows = await (this.prisma as any).person_company.findMany({
74
+ where: {
75
+ trade_name: { contains: search, mode: 'insensitive' },
76
+ },
77
+ select: {
78
+ id: true,
79
+ },
80
+ });
81
+
82
+ const matchingCompanyPersonIds = matchingCompanyRows
83
+ .map((item: { id: number }) => Number(item.id))
84
+ .filter((id: number) => id > 0);
85
+
72
86
  where.OR = [
73
87
  { code: { contains: search, mode: 'insensitive' } },
74
88
  { title: { contains: search, mode: 'insensitive' } },
75
- { person: { name: { contains: search, mode: 'insensitive' } } },
76
- { person: { trade_name: { contains: search, mode: 'insensitive' } } },
89
+ {
90
+ person: {
91
+ is: {
92
+ name: { contains: search, mode: 'insensitive' },
93
+ },
94
+ },
95
+ },
96
+ ...(matchingCompanyPersonIds.length > 0
97
+ ? [{ person_id: { in: matchingCompanyPersonIds } }]
98
+ : []),
77
99
  ];
78
100
  }
79
101
 
@@ -85,9 +107,6 @@ export class ProposalService {
85
107
  select: {
86
108
  id: true,
87
109
  name: true,
88
- trade_name: true,
89
- email: true,
90
- phone: true,
91
110
  },
92
111
  },
93
112
  proposal_revision: {
@@ -116,11 +135,12 @@ export class ProposalService {
116
135
  (this.prisma as any).proposal.count({ where }),
117
136
  ]);
118
137
 
138
+ const normalizedData = await this.attachPersonTradeNames(this.prisma, data);
119
139
  const page = Math.floor(skip / take) + 1;
120
140
  const lastPage = Math.max(1, Math.ceil(total / take));
121
141
 
122
142
  return {
123
- data,
143
+ data: normalizedData,
124
144
  total,
125
145
  page,
126
146
  pageSize: take,
@@ -279,6 +299,11 @@ export class ProposalService {
279
299
  return proposal;
280
300
  });
281
301
 
302
+ await this.syncPersonDealValueFromApprovedProposals(
303
+ this.prisma,
304
+ createdProposal.person_id,
305
+ );
306
+
282
307
  return this.getById(createdProposal.id, locale);
283
308
  }
284
309
 
@@ -507,6 +532,11 @@ export class ProposalService {
507
532
  }
508
533
  });
509
534
 
535
+ await this.syncPersonDealValueFromApprovedProposals(
536
+ this.prisma,
537
+ current.person_id,
538
+ );
539
+
510
540
  return this.getById(id, locale);
511
541
  }
512
542
 
@@ -783,6 +813,11 @@ export class ProposalService {
783
813
  );
784
814
  });
785
815
 
816
+ await this.syncPersonDealValueFromApprovedProposals(
817
+ this.prisma,
818
+ current.person_id,
819
+ );
820
+
786
821
  return this.getById(id, locale);
787
822
  }
788
823
 
@@ -905,6 +940,11 @@ export class ProposalService {
905
940
  );
906
941
  });
907
942
 
943
+ await this.syncPersonDealValueFromApprovedProposals(
944
+ this.prisma,
945
+ current.person_id,
946
+ );
947
+
908
948
  return this.getById(id, locale);
909
949
  }
910
950
 
@@ -990,6 +1030,11 @@ export class ProposalService {
990
1030
  );
991
1031
  });
992
1032
 
1033
+ await this.syncPersonDealValueFromApprovedProposals(
1034
+ this.prisma,
1035
+ current.person_id,
1036
+ );
1037
+
993
1038
  return this.getById(id, locale);
994
1039
  }
995
1040
 
@@ -1180,6 +1225,11 @@ export class ProposalService {
1180
1225
  );
1181
1226
  });
1182
1227
 
1228
+ await this.syncPersonDealValueFromApprovedProposals(
1229
+ this.prisma,
1230
+ current.person_id,
1231
+ );
1232
+
1183
1233
  return this.getById(proposalId, locale);
1184
1234
  }
1185
1235
 
@@ -1319,6 +1369,26 @@ export class ProposalService {
1319
1369
  );
1320
1370
  }
1321
1371
 
1372
+ const impactedProposals = await (this.prisma as any).proposal.findMany({
1373
+ where: {
1374
+ id: { in: ids },
1375
+ deleted_at: null,
1376
+ },
1377
+ select: {
1378
+ id: true,
1379
+ person_id: true,
1380
+ },
1381
+ });
1382
+ const impactedPersonIds: number[] = Array.from(
1383
+ new Set<number>(
1384
+ impactedProposals
1385
+ .map((proposal: { person_id?: number | null }) =>
1386
+ Number(proposal?.person_id || 0),
1387
+ )
1388
+ .filter((personId): personId is number => personId > 0),
1389
+ ),
1390
+ );
1391
+
1322
1392
  await this.prisma.$transaction(async (tx) => {
1323
1393
  const now = new Date();
1324
1394
  await (tx as any).proposal.updateMany({
@@ -1376,6 +1446,12 @@ export class ProposalService {
1376
1446
  });
1377
1447
  });
1378
1448
 
1449
+ await Promise.all(
1450
+ impactedPersonIds.map((personId) =>
1451
+ this.syncPersonDealValueFromApprovedProposals(this.prisma, personId),
1452
+ ),
1453
+ );
1454
+
1379
1455
  return {
1380
1456
  deleted: ids.length,
1381
1457
  ids,
@@ -1636,6 +1712,114 @@ export class ProposalService {
1636
1712
  }
1637
1713
  }
1638
1714
 
1715
+ private async syncPersonDealValueFromApprovedProposals(
1716
+ client: any,
1717
+ personId: number | null | undefined,
1718
+ ) {
1719
+ const normalizedPersonId = Number(personId || 0);
1720
+
1721
+ if (!Number.isInteger(normalizedPersonId) || normalizedPersonId <= 0) {
1722
+ return;
1723
+ }
1724
+
1725
+ const aggregation = await (client as any).proposal.aggregate({
1726
+ where: {
1727
+ deleted_at: null,
1728
+ person_id: normalizedPersonId,
1729
+ status: {
1730
+ in: [ProposalStatus.APPROVED, ProposalStatus.CONTRACT_GENERATED],
1731
+ },
1732
+ },
1733
+ _sum: {
1734
+ total_amount_cents: true,
1735
+ },
1736
+ });
1737
+
1738
+ const totalAmountCents = Number(aggregation?._sum?.total_amount_cents || 0);
1739
+ const dealValue =
1740
+ totalAmountCents > 0 ? (totalAmountCents / 100).toFixed(2) : null;
1741
+
1742
+ await this.upsertPersonMetadataValue(
1743
+ client,
1744
+ normalizedPersonId,
1745
+ 'deal_value',
1746
+ dealValue,
1747
+ );
1748
+ }
1749
+
1750
+ private async upsertPersonMetadataValue(
1751
+ client: any,
1752
+ personId: number,
1753
+ key: string,
1754
+ value: unknown,
1755
+ ) {
1756
+ if (!(client as any).person_metadata) {
1757
+ return;
1758
+ }
1759
+
1760
+ const existing = await (client as any).person_metadata.findFirst({
1761
+ where: {
1762
+ person_id: personId,
1763
+ key,
1764
+ },
1765
+ select: { id: true },
1766
+ });
1767
+
1768
+ const normalizedValue = this.normalizePersonMetadataValue(value);
1769
+
1770
+ if (normalizedValue == null) {
1771
+ if (existing) {
1772
+ await (client as any).person_metadata.delete({
1773
+ where: { id: existing.id },
1774
+ });
1775
+ }
1776
+ return;
1777
+ }
1778
+
1779
+ if (existing) {
1780
+ await (client as any).person_metadata.update({
1781
+ where: { id: existing.id },
1782
+ data: { value: normalizedValue },
1783
+ });
1784
+ return;
1785
+ }
1786
+
1787
+ await (client as any).person_metadata.create({
1788
+ data: {
1789
+ person_id: personId,
1790
+ key,
1791
+ value: normalizedValue,
1792
+ },
1793
+ });
1794
+ }
1795
+
1796
+ private normalizePersonMetadataValue(value: unknown) {
1797
+ if (value == null) return null;
1798
+
1799
+ if (typeof value === 'string') {
1800
+ const trimmed = value.trim();
1801
+ return trimmed.length > 0 ? trimmed : null;
1802
+ }
1803
+
1804
+ if (typeof value === 'number') {
1805
+ return Number.isFinite(value) ? value : null;
1806
+ }
1807
+
1808
+ if (typeof value === 'boolean') {
1809
+ return value;
1810
+ }
1811
+
1812
+ if (Array.isArray(value) || typeof value === 'object') {
1813
+ try {
1814
+ return JSON.parse(JSON.stringify(value));
1815
+ } catch {
1816
+ return null;
1817
+ }
1818
+ }
1819
+
1820
+ return null;
1821
+ }
1822
+
1639
1823
  private async resolveProposalCode(code?: string | null) {
1640
1824
  const normalized = this.normalizeOptionalText(code)?.toUpperCase();
1641
1825
 
@@ -1872,8 +2056,24 @@ export class ProposalService {
1872
2056
  }),
1873
2057
  );
1874
2058
  } catch (error) {
1875
- throw new BadRequestException(
1876
- 'PDF generation requires Playwright to be installed on the server.',
2059
+ const errorMessage =
2060
+ error instanceof Error ? error.message : String(error);
2061
+ const errorStack =
2062
+ error instanceof Error ? (error.stack ?? error.message) : String(error);
2063
+ const missingPlaywrightRuntime =
2064
+ /Cannot find package ['"]playwright['"]|Cannot find module ['"]playwright['"]|Executable doesn't exist|browserType\.launch|Failed to launch|Please run the following command to download new browsers/i.test(
2065
+ errorMessage,
2066
+ );
2067
+
2068
+ this.logger.error(
2069
+ `Failed to generate proposal PDF for proposal ${proposal?.id ?? 'unknown'} (locale=${locale}). ${errorMessage}`,
2070
+ errorStack,
2071
+ );
2072
+
2073
+ throw new InternalServerErrorException(
2074
+ missingPlaywrightRuntime
2075
+ ? 'PDF generation is unavailable because Playwright/Chromium is not installed on the server. Run `pnpm --filter api run playwright:install` in the API environment.'
2076
+ : 'Failed to generate the PDF document. Check server logs for details.',
1877
2077
  );
1878
2078
  } finally {
1879
2079
  await browser?.close?.();
@@ -2419,8 +2619,144 @@ export class ProposalService {
2419
2619
  return normalized.length > 0 ? normalized : null;
2420
2620
  }
2421
2621
 
2622
+ private getPrimaryPersonContactValue(
2623
+ contacts: any[] | undefined,
2624
+ codes: string[],
2625
+ ): string | null {
2626
+ const normalizedCodes = new Set(codes.map((code) => code.toUpperCase()));
2627
+ const items = Array.isArray(contacts)
2628
+ ? contacts.filter((contact) =>
2629
+ normalizedCodes.has(
2630
+ String(contact?.contact_type?.code || '').toUpperCase(),
2631
+ ),
2632
+ )
2633
+ : [];
2634
+
2635
+ const primary = items.find((contact) => contact?.is_primary);
2636
+ const fallback = items[0];
2637
+ const value = primary?.value ?? fallback?.value ?? null;
2638
+ return value != null && String(value).trim().length > 0
2639
+ ? String(value).trim()
2640
+ : null;
2641
+ }
2642
+
2643
+ private async attachPersonTradeNames(client: any, input: any) {
2644
+ if (!input) {
2645
+ return input;
2646
+ }
2647
+
2648
+ const proposals = Array.isArray(input) ? input : [input];
2649
+ const personIds = Array.from(
2650
+ new Set(
2651
+ proposals
2652
+ .map((proposal) =>
2653
+ Number(proposal?.person?.id ?? proposal?.person_id ?? 0),
2654
+ )
2655
+ .filter((id) => id > 0),
2656
+ ),
2657
+ );
2658
+
2659
+ if (personIds.length === 0) {
2660
+ return input;
2661
+ }
2662
+
2663
+ const [companyRows, contactRows, documentRows] = await Promise.all([
2664
+ client.person_company.findMany({
2665
+ where: {
2666
+ id: {
2667
+ in: personIds,
2668
+ },
2669
+ },
2670
+ select: {
2671
+ id: true,
2672
+ trade_name: true,
2673
+ },
2674
+ }),
2675
+ client.contact.findMany({
2676
+ where: {
2677
+ person_id: {
2678
+ in: personIds,
2679
+ },
2680
+ },
2681
+ include: {
2682
+ contact_type: {
2683
+ select: {
2684
+ code: true,
2685
+ },
2686
+ },
2687
+ },
2688
+ orderBy: [{ is_primary: 'desc' }, { id: 'asc' }],
2689
+ }),
2690
+ client.document.findMany({
2691
+ where: {
2692
+ person_id: {
2693
+ in: personIds,
2694
+ },
2695
+ },
2696
+ select: {
2697
+ id: true,
2698
+ person_id: true,
2699
+ value: true,
2700
+ },
2701
+ orderBy: [{ id: 'asc' }],
2702
+ }),
2703
+ ]);
2704
+
2705
+ const tradeNameByPersonId = new Map<number, string | null>(
2706
+ companyRows.map((row: { id: number; trade_name?: string | null }) => [
2707
+ row.id,
2708
+ row.trade_name ?? null,
2709
+ ]),
2710
+ );
2711
+
2712
+ const contactsByPersonId = new Map<number, any[]>();
2713
+ for (const contact of contactRows) {
2714
+ const current = contactsByPersonId.get(Number(contact.person_id)) ?? [];
2715
+ current.push(contact);
2716
+ contactsByPersonId.set(Number(contact.person_id), current);
2717
+ }
2718
+
2719
+ const documentsByPersonId = new Map<number, any[]>();
2720
+ for (const document of documentRows) {
2721
+ const current = documentsByPersonId.get(Number(document.person_id)) ?? [];
2722
+ current.push(document);
2723
+ documentsByPersonId.set(Number(document.person_id), current);
2724
+ }
2725
+
2726
+ const normalized = proposals.map((proposal) => {
2727
+ if (!proposal?.person) {
2728
+ return proposal;
2729
+ }
2730
+
2731
+ const personId = Number(proposal.person.id);
2732
+ const contacts = contactsByPersonId.get(personId) ?? [];
2733
+ const documents = documentsByPersonId.get(personId) ?? [];
2734
+
2735
+ return {
2736
+ ...proposal,
2737
+ person: {
2738
+ ...proposal.person,
2739
+ trade_name: tradeNameByPersonId.get(personId) ?? null,
2740
+ email: this.getPrimaryPersonContactValue(contacts, ['EMAIL']),
2741
+ phone: this.getPrimaryPersonContactValue(contacts, [
2742
+ 'PHONE',
2743
+ 'MOBILE',
2744
+ 'WHATSAPP',
2745
+ ]),
2746
+ document:
2747
+ documents[0]?.value != null &&
2748
+ String(documents[0].value).trim().length > 0
2749
+ ? String(documents[0].value).trim()
2750
+ : null,
2751
+ },
2752
+ };
2753
+ });
2754
+
2755
+ return Array.isArray(input) ? normalized : normalized[0];
2756
+ }
2757
+
2422
2758
  private async loadProposalDetail(client: any, proposalId: number) {
2423
- return client.proposal.findFirst({
2759
+ const proposal = await client.proposal.findFirst({
2424
2760
  where: {
2425
2761
  id: proposalId,
2426
2762
  deleted_at: null,
@@ -2430,10 +2766,6 @@ export class ProposalService {
2430
2766
  select: {
2431
2767
  id: true,
2432
2768
  name: true,
2433
- trade_name: true,
2434
- email: true,
2435
- phone: true,
2436
- document: true,
2437
2769
  },
2438
2770
  },
2439
2771
  proposal_revision: {
@@ -2474,6 +2806,8 @@ export class ProposalService {
2474
2806
  },
2475
2807
  },
2476
2808
  });
2809
+
2810
+ return this.attachPersonTradeNames(client, proposal);
2477
2811
  }
2478
2812
 
2479
2813
  private async loadProposalIntegrationSnapshot(client: any, proposalId: number) {
@@ -2487,10 +2821,6 @@ export class ProposalService {
2487
2821
  select: {
2488
2822
  id: true,
2489
2823
  name: true,
2490
- trade_name: true,
2491
- email: true,
2492
- phone: true,
2493
- document: true,
2494
2824
  type: true,
2495
2825
  },
2496
2826
  },
@@ -2519,7 +2849,7 @@ export class ProposalService {
2519
2849
  throw new NotFoundException('Proposal snapshot not found.');
2520
2850
  }
2521
2851
 
2522
- return proposal;
2852
+ return this.attachPersonTradeNames(client, proposal);
2523
2853
  }
2524
2854
 
2525
2855
  }