@hed-hog/contact 0.0.266 → 0.0.274

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 (83) hide show
  1. package/README.md +470 -0
  2. package/dist/address-type.enum.d.ts +10 -0
  3. package/dist/address-type.enum.d.ts.map +1 -0
  4. package/dist/address-type.enum.js +14 -0
  5. package/dist/address-type.enum.js.map +1 -0
  6. package/dist/contact.module.d.ts.map +1 -1
  7. package/dist/contact.module.js +0 -2
  8. package/dist/contact.module.js.map +1 -1
  9. package/dist/contact.service.d.ts +19 -22
  10. package/dist/contact.service.d.ts.map +1 -1
  11. package/dist/contact.service.js +22 -2
  12. package/dist/contact.service.js.map +1 -1
  13. package/dist/index.d.ts +5 -8
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -4
  16. package/dist/index.js.map +1 -1
  17. package/dist/person/dto/create.dto.d.ts +14 -0
  18. package/dist/person/dto/create.dto.d.ts.map +1 -1
  19. package/dist/person/dto/create.dto.js +52 -1
  20. package/dist/person/dto/create.dto.js.map +1 -1
  21. package/dist/person/dto/interaction-create.dto.d.ts +16 -0
  22. package/dist/person/dto/interaction-create.dto.d.ts.map +1 -0
  23. package/dist/person/dto/interaction-create.dto.js +57 -0
  24. package/dist/person/dto/interaction-create.dto.js.map +1 -0
  25. package/dist/person/dto/update.dto.d.ts +17 -1
  26. package/dist/person/dto/update.dto.d.ts.map +1 -1
  27. package/dist/person/dto/update.dto.js +79 -3
  28. package/dist/person/dto/update.dto.js.map +1 -1
  29. package/dist/person/person.controller.d.ts +37 -8
  30. package/dist/person/person.controller.d.ts.map +1 -1
  31. package/dist/person/person.controller.js +29 -3
  32. package/dist/person/person.controller.js.map +1 -1
  33. package/dist/person/person.service.d.ts +71 -13
  34. package/dist/person/person.service.d.ts.map +1 -1
  35. package/dist/person/person.service.js +762 -108
  36. package/dist/person/person.service.js.map +1 -1
  37. package/dist/person-relation-type/person-relation-type.controller.d.ts +13 -9
  38. package/dist/person-relation-type/person-relation-type.controller.d.ts.map +1 -1
  39. package/dist/person-relation-type/person-relation-type.service.d.ts +16 -20
  40. package/dist/person-relation-type/person-relation-type.service.d.ts.map +1 -1
  41. package/dist/person-relation-type/person-relation-type.service.js +48 -41
  42. package/dist/person-relation-type/person-relation-type.service.js.map +1 -1
  43. package/hedhog/data/menu.yaml +2 -16
  44. package/hedhog/data/role.yaml +9 -1
  45. package/hedhog/data/route.yaml +10 -21
  46. package/hedhog/data/setting_group.yaml +21 -0
  47. package/hedhog/frontend/app/person/_components/delete-person-dialog.tsx.ejs +59 -0
  48. package/hedhog/frontend/app/person/_components/person-field-with-create.tsx.ejs +831 -0
  49. package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +2277 -0
  50. package/hedhog/frontend/app/person/_components/person-types.ts.ejs +157 -0
  51. package/hedhog/frontend/app/person/page.tsx.ejs +1158 -1335
  52. package/hedhog/frontend/messages/en.json +114 -4
  53. package/hedhog/frontend/messages/pt.json +155 -4
  54. package/hedhog/table/person.yaml +7 -0
  55. package/hedhog/table/person_address.yaml +18 -0
  56. package/hedhog/table/person_company.yaml +26 -11
  57. package/hedhog/table/person_individual.yaml +4 -0
  58. package/hedhog/table/person_individual_relation.yaml +39 -0
  59. package/package.json +6 -5
  60. package/src/address-type.enum.ts +9 -0
  61. package/src/contact.module.ts +46 -48
  62. package/src/contact.service.ts +28 -13
  63. package/src/index.ts +6 -13
  64. package/src/language/en.json +8 -1
  65. package/src/language/pt.json +9 -1
  66. package/src/person/dto/create.dto.ts +49 -1
  67. package/src/person/dto/update.dto.ts +75 -3
  68. package/src/person/person.controller.ts +31 -14
  69. package/src/person/person.service.ts +1019 -121
  70. package/src/person-relation-type/person-relation-type.service.ts +84 -76
  71. package/hedhog/data/address_type.yaml +0 -28
  72. package/hedhog/frontend/app/address-type/page.tsx.ejs +0 -480
  73. package/hedhog/query/add-unique-address-type-locale.sql +0 -3
  74. package/hedhog/table/address.yaml +0 -28
  75. package/hedhog/table/address_type.yaml +0 -11
  76. package/hedhog/table/person_relation.yaml +0 -20
  77. package/hedhog/table/person_relation_type.yaml +0 -6
  78. package/src/address-type/address-type.controller.ts +0 -55
  79. package/src/address-type/address-type.enum.ts +0 -9
  80. package/src/address-type/address-type.module.ts +0 -18
  81. package/src/address-type/address-type.service.ts +0 -121
  82. package/src/address-type/dto/create.dto.ts +0 -19
  83. package/src/address-type/dto/update.dto.ts +0 -9
@@ -16,13 +16,44 @@ exports.PersonService = void 0;
16
16
  const api_locale_1 = require("@hed-hog/api-locale");
17
17
  const api_pagination_1 = require("@hed-hog/api-pagination");
18
18
  const api_prisma_1 = require("@hed-hog/api-prisma");
19
+ const core_1 = require("@hed-hog/core");
19
20
  const common_1 = require("@nestjs/common");
21
+ const CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING = 'contact-allow-company-registration';
22
+ const CONTACT_OWNER_ROLE_SLUG = 'owner-contact';
23
+ const CONTACT_OWNER_ALLOWED_ROLE_SLUGS = [
24
+ 'admin',
25
+ 'admin-contact',
26
+ CONTACT_OWNER_ROLE_SLUG,
27
+ ];
28
+ const EMPLOYER_COMPANY_METADATA_KEY = 'employer_company_id';
29
+ const NOTES_METADATA_KEY = 'notes';
20
30
  let PersonService = class PersonService {
21
- constructor(prismaService, paginationService) {
31
+ constructor(prismaService, paginationService, fileService, settingService) {
22
32
  this.prismaService = prismaService;
23
33
  this.paginationService = paginationService;
34
+ this.fileService = fileService;
35
+ this.settingService = settingService;
24
36
  }
25
37
  async getStats() {
38
+ const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
39
+ if (!allowCompanyRegistration) {
40
+ const [individual, active, inactive] = await Promise.all([
41
+ this.prismaService.person.count({ where: { type: 'individual' } }),
42
+ this.prismaService.person.count({
43
+ where: { type: 'individual', status: 'active' },
44
+ }),
45
+ this.prismaService.person.count({
46
+ where: { type: 'individual', status: 'inactive' },
47
+ }),
48
+ ]);
49
+ return {
50
+ total: individual,
51
+ individual,
52
+ company: 0,
53
+ active,
54
+ inactive,
55
+ };
56
+ }
26
57
  const [total, individual, company, active, inactive] = await Promise.all([
27
58
  this.prismaService.person.count(),
28
59
  this.prismaService.person.count({ where: { type: 'individual' } }),
@@ -38,143 +69,192 @@ let PersonService = class PersonService {
38
69
  inactive,
39
70
  };
40
71
  }
41
- async list(paginationParams) {
72
+ async getOwnerOptions(currentUserId) {
73
+ const where = {
74
+ OR: [
75
+ {
76
+ role_user: {
77
+ some: {
78
+ role: {
79
+ slug: {
80
+ in: CONTACT_OWNER_ALLOWED_ROLE_SLUGS,
81
+ },
82
+ },
83
+ },
84
+ },
85
+ },
86
+ ...(Number(currentUserId) > 0
87
+ ? [
88
+ {
89
+ id: Number(currentUserId),
90
+ },
91
+ ]
92
+ : []),
93
+ ],
94
+ };
95
+ const users = await this.prismaService.user.findMany({
96
+ where,
97
+ select: {
98
+ id: true,
99
+ name: true,
100
+ },
101
+ orderBy: {
102
+ name: 'asc',
103
+ },
104
+ take: 500,
105
+ });
106
+ const byId = new Map();
107
+ for (const user of users) {
108
+ if (!(user === null || user === void 0 ? void 0 : user.id))
109
+ continue;
110
+ byId.set(user.id, {
111
+ id: user.id,
112
+ name: user.name || `#${user.id}`,
113
+ });
114
+ }
115
+ return Array.from(byId.values());
116
+ }
117
+ async list(paginationParams, currentUserId) {
42
118
  var _a;
43
- const OR = [];
119
+ const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
120
+ const where = {};
44
121
  const search = (_a = paginationParams.search) === null || _a === void 0 ? void 0 : _a.trim();
45
- if (search) {
46
- if (!isNaN(+search)) {
47
- OR.push({ id: { equals: +search } });
48
- }
49
- OR.push({ name: { contains: search, mode: 'insensitive' } });
122
+ const requestedType = paginationParams.type;
123
+ if (!allowCompanyRegistration && requestedType === 'company') {
124
+ return this.paginationService.paginate(this.prismaService.person, paginationParams, {
125
+ where: {
126
+ id: -1,
127
+ },
128
+ });
50
129
  }
51
- const where = {};
52
- if (OR.length > 0) {
53
- where.OR = OR;
130
+ if (!allowCompanyRegistration) {
131
+ where.type = 'individual';
54
132
  }
55
- if (paginationParams.type && paginationParams.type !== 'all') {
56
- where.type = paginationParams.type;
133
+ else if (requestedType && requestedType !== 'all') {
134
+ where.type = requestedType;
57
135
  }
58
136
  if (paginationParams.status && paginationParams.status !== 'all') {
59
137
  where.status = paginationParams.status;
60
138
  }
61
- return this.paginationService.paginate(this.prismaService.person, paginationParams, {
139
+ if (search) {
140
+ where.OR = await this.buildSearchFilters(search);
141
+ }
142
+ const result = await this.paginationService.paginate(this.prismaService.person, paginationParams, {
62
143
  where,
63
144
  include: {
64
- address: true,
145
+ person_address: {
146
+ include: {
147
+ address: true,
148
+ },
149
+ },
65
150
  contact: true,
66
151
  document: true,
152
+ person_metadata: true,
67
153
  },
68
154
  });
155
+ const enriched = await this.enrichPeople(result.data, allowCompanyRegistration);
156
+ return Object.assign(Object.assign({}, result), { data: enriched });
69
157
  }
70
158
  async get(locale, id) {
71
- const person = await this.prismaService.person.findUnique({ where: { id } });
159
+ const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
160
+ const person = await this.prismaService.person.findUnique({
161
+ where: { id },
162
+ include: {
163
+ person_address: {
164
+ include: {
165
+ address: true,
166
+ },
167
+ },
168
+ contact: true,
169
+ document: true,
170
+ person_metadata: true,
171
+ },
172
+ });
72
173
  if (!person) {
73
174
  throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person with ID ${id} not found`));
74
175
  }
75
- return person;
176
+ if (!allowCompanyRegistration && person.type === 'company') {
177
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person with ID ${id} not found`));
178
+ }
179
+ const [normalized] = await this.enrichPeople([person], allowCompanyRegistration);
180
+ return normalized;
76
181
  }
77
- async create(data) {
78
- return this.prismaService.person.create({
79
- data,
182
+ async create(data, locale) {
183
+ const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
184
+ await this.ensureCompanyRegistrationAllowed({
185
+ nextType: data.type,
186
+ locale,
187
+ });
188
+ return this.prismaService.$transaction(async (tx) => {
189
+ var _a, _b;
190
+ const person = await tx.person.create({
191
+ data: {
192
+ name: data.name,
193
+ type: data.type,
194
+ status: data.status,
195
+ avatar_id: (_a = data.avatar_id) !== null && _a !== void 0 ? _a : null,
196
+ },
197
+ });
198
+ await this.syncPersonSubtypeData(tx, person.id, null, data, locale);
199
+ await this.syncPersonMetadata(tx, person.id, {
200
+ notes: data.notes,
201
+ employer_company_id: allowCompanyRegistration
202
+ ? (_b = data.employer_company_id) !== null && _b !== void 0 ? _b : null
203
+ : null,
204
+ });
205
+ return person;
80
206
  });
81
207
  }
82
208
  async update(id, data, locale) {
209
+ const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
83
210
  const person = await this.prismaService.person.findUnique({ where: { id } });
84
211
  if (!person) {
85
212
  throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person with ID ${id} not found`));
86
213
  }
87
- const incomingContacts = data.contacts || [];
88
- const incomingAddresses = data.addresses || [];
89
- const incomingDocuments = data.documents || [];
90
- const contactPrimaryMap = new Map();
91
- for (const c of incomingContacts) {
92
- if (c.is_primary) {
93
- const key = String(c.contact_type_id);
94
- contactPrimaryMap.set(key, (contactPrimaryMap.get(key) || 0) + 1);
95
- }
96
- }
97
- for (const [_, count] of contactPrimaryMap.entries()) {
98
- if (count > 1) {
99
- throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('moreThanOnePrimaryContact', locale, 'More than one contact of the same type cannot be marked as primary.'));
100
- }
101
- }
102
- const addressPrimaryMap = new Map();
103
- for (const a of incomingAddresses) {
104
- if (a.is_primary) {
105
- const key = String(a.address_type_id);
106
- addressPrimaryMap.set(key, (addressPrimaryMap.get(key) || 0) + 1);
107
- }
108
- }
109
- for (const [_, count] of addressPrimaryMap.entries()) {
110
- if (count > 1) {
111
- throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('moreThanOnePrimaryAddress', locale, 'More than one address of the same type cannot be marked as primary.'));
112
- }
214
+ if (!allowCompanyRegistration && person.type === 'company') {
215
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person with ID ${id} not found`));
113
216
  }
114
- const documentPrimaryMap = new Map();
115
- for (const d of incomingDocuments) {
116
- if (d.is_primary) {
117
- const key = String(d.document_type_id);
118
- documentPrimaryMap.set(key, (documentPrimaryMap.get(key) || 0) + 1);
119
- }
120
- }
121
- for (const [_, count] of documentPrimaryMap.entries()) {
122
- if (count > 1) {
123
- throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('moreThanOnePrimaryDocument', locale, 'More than one document of the same type cannot be marked as primary.'));
124
- }
125
- }
126
- return this.prismaService.$transaction(async (tx) => {
217
+ await this.ensureCompanyRegistrationAllowed({
218
+ currentType: person.type,
219
+ nextType: data.type,
220
+ locale,
221
+ });
222
+ const incomingContacts = Array.isArray(data.contacts) ? data.contacts : [];
223
+ const incomingAddresses = Array.isArray(data.addresses) ? data.addresses : [];
224
+ const incomingDocuments = Array.isArray(data.documents) ? data.documents : [];
225
+ this.validateSinglePrimaryPerType(incomingContacts, 'contact_type_id', locale, 'moreThanOnePrimaryContact', 'More than one contact of the same type cannot be marked as primary.');
226
+ this.validateSinglePrimaryPerType(incomingAddresses, 'address_type', locale, 'moreThanOnePrimaryAddress', 'More than one address of the same type cannot be marked as primary.');
227
+ this.validateSinglePrimaryPerType(incomingDocuments, 'document_type_id', locale, 'moreThanOnePrimaryDocument', 'More than one document of the same type cannot be marked as primary.');
228
+ return this.prismaService
229
+ .$transaction(async (tx) => {
230
+ var _a, _b, _c;
231
+ const nextType = (_a = data.type) !== null && _a !== void 0 ? _a : person.type;
127
232
  await tx.person.update({
128
233
  where: { id },
129
234
  data: {
130
- name: data.name,
131
- type: data.type,
132
- status: data.status,
235
+ name: (_b = data.name) !== null && _b !== void 0 ? _b : person.name,
236
+ type: nextType,
237
+ status: (_c = data.status) !== null && _c !== void 0 ? _c : person.status,
238
+ avatar_id: data.avatar_id === undefined ? person.avatar_id : data.avatar_id,
133
239
  },
134
240
  });
135
- const existingContacts = await tx.contact.findMany({ where: { person_id: id } });
136
- for (const c of incomingContacts) {
137
- if (c.id) {
138
- await tx.contact.update({ where: { id: c.id }, data: c });
139
- }
140
- else {
141
- await tx.contact.create({ data: Object.assign(Object.assign({}, c), { person_id: id }) });
142
- }
143
- }
144
- for (const old of existingContacts) {
145
- if (!incomingContacts.find((c) => c.id === old.id)) {
146
- await tx.contact.delete({ where: { id: old.id } });
147
- }
148
- }
149
- const existingAddresses = await tx.address.findMany({ where: { person_id: id } });
150
- for (const a of incomingAddresses) {
151
- if (a.id) {
152
- await tx.address.update({ where: { id: a.id }, data: a });
153
- }
154
- else {
155
- await tx.address.create({ data: Object.assign(Object.assign({}, a), { person_id: id }) });
156
- }
157
- }
158
- for (const old of existingAddresses) {
159
- if (!incomingAddresses.find((a) => a.id === old.id)) {
160
- await tx.address.delete({ where: { id: old.id } });
161
- }
162
- }
163
- const existingDocuments = await tx.document.findMany({ where: { person_id: id } });
164
- for (const d of incomingDocuments) {
165
- if (d.id) {
166
- await tx.document.update({ where: { id: d.id }, data: d });
167
- }
168
- else {
169
- await tx.document.create({ data: Object.assign(Object.assign({}, d), { person_id: id }) });
170
- }
171
- }
172
- for (const old of existingDocuments) {
173
- if (!incomingDocuments.find((d) => d.id === old.id)) {
174
- await tx.document.delete({ where: { id: old.id } });
175
- }
176
- }
241
+ await this.syncPersonSubtypeData(tx, id, person.type, data, locale);
242
+ await this.syncPersonMetadata(tx, id, {
243
+ notes: data.notes,
244
+ employer_company_id: allowCompanyRegistration
245
+ ? data.employer_company_id === undefined
246
+ ? undefined
247
+ : data.employer_company_id
248
+ : null,
249
+ });
250
+ await this.syncContacts(tx, id, incomingContacts);
251
+ await this.syncAddresses(tx, id, incomingAddresses, locale);
252
+ await this.syncDocuments(tx, id, incomingDocuments);
177
253
  return { success: true };
254
+ })
255
+ .then(async (result) => {
256
+ await this.cleanupReplacedAvatar(locale, person.avatar_id, data.avatar_id);
257
+ return result;
178
258
  });
179
259
  }
180
260
  async delete({ ids }, locale) {
@@ -185,15 +265,53 @@ let PersonService = class PersonService {
185
265
  where: { id: { in: ids } },
186
266
  select: { id: true },
187
267
  });
188
- const existingIds = existing.map(p => p.id);
189
- const missingIds = ids.filter(id => !existingIds.includes(id));
268
+ const existingIds = existing.map((p) => p.id);
269
+ const missingIds = ids.filter((itemId) => !existingIds.includes(itemId));
190
270
  if (missingIds.length > 0) {
191
271
  throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Person(s) with ID(s) ${missingIds.join(', ')} not found.`));
192
272
  }
273
+ const personAddresses = await this.prismaService.person_address.findMany({
274
+ where: {
275
+ person_id: { in: ids },
276
+ },
277
+ select: {
278
+ id: true,
279
+ address_id: true,
280
+ },
281
+ });
282
+ const addressIds = personAddresses.map((item) => item.address_id);
193
283
  return this.prismaService.$transaction([
194
284
  this.prismaService.contact.deleteMany({ where: { person_id: { in: ids } } }),
195
- this.prismaService.address.deleteMany({ where: { person_id: { in: ids } } }),
196
285
  this.prismaService.document.deleteMany({ where: { person_id: { in: ids } } }),
286
+ this.prismaService.person_individual_relation.deleteMany({
287
+ where: {
288
+ OR: [
289
+ { person_individual_id: { in: ids } },
290
+ { related_person_individual_id: { in: ids } },
291
+ ],
292
+ },
293
+ }),
294
+ this.prismaService.person_company.deleteMany({
295
+ where: {
296
+ OR: [{ id: { in: ids } }, { headquarter_id: { in: ids } }],
297
+ },
298
+ }),
299
+ this.prismaService.person_individual.deleteMany({
300
+ where: { id: { in: ids } },
301
+ }),
302
+ this.prismaService.person_metadata.deleteMany({
303
+ where: { person_id: { in: ids } },
304
+ }),
305
+ this.prismaService.person_address.deleteMany({
306
+ where: { person_id: { in: ids } },
307
+ }),
308
+ this.prismaService.address.deleteMany({
309
+ where: {
310
+ id: {
311
+ in: addressIds,
312
+ },
313
+ },
314
+ }),
197
315
  this.prismaService.person.deleteMany({
198
316
  where: {
199
317
  id: {
@@ -203,13 +321,549 @@ let PersonService = class PersonService {
203
321
  }),
204
322
  ]);
205
323
  }
324
+ async openPublicAvatar(locale, fileId, res) {
325
+ const personWithAvatar = await this.prismaService.person.findFirst({
326
+ where: {
327
+ avatar_id: fileId,
328
+ },
329
+ select: {
330
+ id: true,
331
+ },
332
+ });
333
+ if (!personWithAvatar) {
334
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('personNotFound', locale, 'Person not found'));
335
+ }
336
+ const { file, buffer } = await this.fileService.getBuffer(fileId);
337
+ res.set({
338
+ 'Content-Type': file.file_mimetype.name,
339
+ 'Content-Length': buffer.length,
340
+ 'Cache-Control': 'public, max-age=3600',
341
+ });
342
+ res.send(buffer);
343
+ }
344
+ async enrichPeople(people, allowCompanyRegistration = true) {
345
+ if (people.length === 0) {
346
+ return [];
347
+ }
348
+ const personIds = people.map((person) => person.id);
349
+ const [companies, individuals, companyBranches, employerMetadata] = await Promise.all([
350
+ this.prismaService.person_company.findMany({
351
+ where: { id: { in: personIds } },
352
+ }),
353
+ this.prismaService.person_individual.findMany({
354
+ where: { id: { in: personIds } },
355
+ }),
356
+ this.prismaService.person_company.findMany({
357
+ where: { headquarter_id: { in: personIds } },
358
+ select: { id: true, headquarter_id: true },
359
+ }),
360
+ this.prismaService.person_metadata.findMany({
361
+ where: {
362
+ person_id: { in: personIds },
363
+ key: EMPLOYER_COMPANY_METADATA_KEY,
364
+ },
365
+ select: {
366
+ person_id: true,
367
+ value: true,
368
+ },
369
+ }).then((metadata) => (allowCompanyRegistration ? metadata : [])),
370
+ ]);
371
+ const companyById = new Map(companies.map((item) => [item.id, item]));
372
+ const individualById = new Map(individuals.map((item) => [item.id, item]));
373
+ const branchesByHeadquarterId = new Map();
374
+ for (const branch of companyBranches) {
375
+ if (!branch.headquarter_id)
376
+ continue;
377
+ const current = branchesByHeadquarterId.get(branch.headquarter_id) || [];
378
+ current.push(branch.id);
379
+ branchesByHeadquarterId.set(branch.headquarter_id, current);
380
+ }
381
+ const employerCompanyIdByPersonId = new Map();
382
+ for (const meta of employerMetadata) {
383
+ const employerId = this.coerceNumber(meta.value);
384
+ if (employerId > 0) {
385
+ employerCompanyIdByPersonId.set(meta.person_id, employerId);
386
+ }
387
+ }
388
+ const employerCompanyIds = Array.from(new Set(Array.from(employerCompanyIdByPersonId.values())));
389
+ const employerCompanies = employerCompanyIds.length > 0
390
+ ? await this.prismaService.person.findMany({
391
+ where: {
392
+ id: { in: employerCompanyIds },
393
+ type: 'company',
394
+ },
395
+ })
396
+ : [];
397
+ const employerCompanyRows = employerCompanyIds.length > 0
398
+ ? await this.prismaService.person_company.findMany({
399
+ where: {
400
+ id: { in: employerCompanyIds },
401
+ },
402
+ })
403
+ : [];
404
+ const employerCompanyRowById = new Map(employerCompanyRows.map((item) => [item.id, item]));
405
+ const employerCompanyById = new Map(employerCompanies.map((item) => {
406
+ var _a;
407
+ return [
408
+ item.id,
409
+ Object.assign(Object.assign({}, item), { person_company: (_a = employerCompanyRowById.get(item.id)) !== null && _a !== void 0 ? _a : null }),
410
+ ];
411
+ }));
412
+ return people.map((person) => {
413
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
414
+ const metadata = this.metadataArrayToMap(person.person_metadata);
415
+ const companyData = companyById.get(person.id);
416
+ const individualData = individualById.get(person.id);
417
+ const employerCompanyId = allowCompanyRegistration
418
+ ? (_a = employerCompanyIdByPersonId.get(person.id)) !== null && _a !== void 0 ? _a : null
419
+ : null;
420
+ const employerCompany = allowCompanyRegistration && employerCompanyId != null
421
+ ? this.normalizeRelationPersonSummary(employerCompanyById.get(employerCompanyId))
422
+ : null;
423
+ return {
424
+ id: person.id,
425
+ name: person.name,
426
+ type: person.type,
427
+ status: person.status,
428
+ avatar_id: (_b = person.avatar_id) !== null && _b !== void 0 ? _b : null,
429
+ created_at: person.created_at,
430
+ 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,
438
+ branch_ids: branchesByHeadquarterId.get(person.id) || [],
439
+ notes: this.metadataToString(metadata.get(NOTES_METADATA_KEY)),
440
+ employer_company_id: allowCompanyRegistration ? employerCompanyId : null,
441
+ employer_company: allowCompanyRegistration ? employerCompany : null,
442
+ contact: person.contact || [],
443
+ document: person.document || [],
444
+ address: Array.isArray(person.person_address)
445
+ ? person.person_address
446
+ .map((item) => item.address)
447
+ .filter((address) => address != null)
448
+ : [],
449
+ };
450
+ });
451
+ }
452
+ metadataArrayToMap(metadata) {
453
+ const map = new Map();
454
+ for (const item of Array.isArray(metadata) ? metadata : []) {
455
+ map.set(item.key, item.value);
456
+ }
457
+ return map;
458
+ }
459
+ metadataToString(value) {
460
+ if (value == null)
461
+ return null;
462
+ if (typeof value === 'string')
463
+ return value;
464
+ if (typeof value === 'number' || typeof value === 'boolean') {
465
+ return String(value);
466
+ }
467
+ return null;
468
+ }
469
+ async syncPersonSubtypeData(tx, personId, currentType, data, locale) {
470
+ var _a, _b, _c;
471
+ const targetType = (_a = data.type) !== null && _a !== void 0 ? _a : currentType;
472
+ if (!targetType)
473
+ return;
474
+ if (targetType === 'individual') {
475
+ await tx.person_company.deleteMany({ where: { id: personId } });
476
+ await tx.person_individual.upsert({
477
+ where: { id: personId },
478
+ create: {
479
+ id: personId,
480
+ birth_date: this.parseDateOrNull(data.birth_date),
481
+ gender: (_b = data.gender) !== null && _b !== void 0 ? _b : null,
482
+ job_title: this.normalizeTextOrNull(data.job_title),
483
+ },
484
+ update: {
485
+ birth_date: data.birth_date === undefined
486
+ ? undefined
487
+ : this.parseDateOrNull(data.birth_date),
488
+ gender: data.gender === undefined ? undefined : (_c = data.gender) !== null && _c !== void 0 ? _c : null,
489
+ job_title: data.job_title === undefined
490
+ ? undefined
491
+ : this.normalizeTextOrNull(data.job_title),
492
+ },
493
+ });
494
+ return;
495
+ }
496
+ await tx.person_individual_relation.deleteMany({
497
+ where: {
498
+ OR: [
499
+ { person_individual_id: personId },
500
+ { related_person_individual_id: personId },
501
+ ],
502
+ },
503
+ });
504
+ await tx.person_individual.deleteMany({ where: { id: personId } });
505
+ const normalizedHeadquarterId = this.coerceNumber(data.headquarter_id);
506
+ if (normalizedHeadquarterId > 0) {
507
+ if (normalizedHeadquarterId === personId) {
508
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('companyRelationSelfReference', locale, 'A company cannot be its own headquarter.'));
509
+ }
510
+ const headquarter = await tx.person.findFirst({
511
+ where: { id: normalizedHeadquarterId, type: 'company' },
512
+ select: { id: true },
513
+ });
514
+ if (!headquarter) {
515
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('companyRelationInvalidTarget', locale, 'Only company records can be linked as headquarters or branches.'));
516
+ }
517
+ }
518
+ await tx.person_company.upsert({
519
+ where: { id: personId },
520
+ create: {
521
+ id: personId,
522
+ trade_name: this.normalizeTextOrNull(data.trade_name),
523
+ foundation_date: this.parseDateOrNull(data.foundation_date),
524
+ legal_nature: this.normalizeTextOrNull(data.legal_nature),
525
+ headquarter_id: normalizedHeadquarterId > 0 ? normalizedHeadquarterId : null,
526
+ },
527
+ update: {
528
+ trade_name: data.trade_name === undefined
529
+ ? undefined
530
+ : this.normalizeTextOrNull(data.trade_name),
531
+ foundation_date: data.foundation_date === undefined
532
+ ? undefined
533
+ : this.parseDateOrNull(data.foundation_date),
534
+ legal_nature: data.legal_nature === undefined
535
+ ? undefined
536
+ : this.normalizeTextOrNull(data.legal_nature),
537
+ headquarter_id: data.headquarter_id === undefined
538
+ ? undefined
539
+ : normalizedHeadquarterId > 0
540
+ ? normalizedHeadquarterId
541
+ : null,
542
+ },
543
+ });
544
+ if (Object.prototype.hasOwnProperty.call(data, 'branch_ids')) {
545
+ await this.syncCompanyBranches(tx, personId, data.branch_ids, locale);
546
+ }
547
+ }
548
+ async syncCompanyBranches(tx, personId, branchIds, locale) {
549
+ const normalizedBranchIds = Array.from(new Set((Array.isArray(branchIds) ? branchIds : [])
550
+ .map((value) => Number(value))
551
+ .filter((value) => Number.isInteger(value) && value > 0 && value !== personId)));
552
+ if (normalizedBranchIds.length > 0) {
553
+ const companies = await tx.person.findMany({
554
+ where: {
555
+ id: { in: normalizedBranchIds },
556
+ type: 'company',
557
+ },
558
+ select: { id: true },
559
+ });
560
+ if (companies.length !== normalizedBranchIds.length) {
561
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('companyRelationInvalidTarget', locale, 'Only company records can be linked as headquarters or branches.'));
562
+ }
563
+ await tx.person_company.updateMany({
564
+ where: {
565
+ id: { in: normalizedBranchIds },
566
+ },
567
+ data: {
568
+ headquarter_id: personId,
569
+ },
570
+ });
571
+ }
572
+ await tx.person_company.updateMany({
573
+ where: {
574
+ headquarter_id: personId,
575
+ id: { notIn: normalizedBranchIds.length > 0 ? normalizedBranchIds : [-1] },
576
+ },
577
+ data: {
578
+ headquarter_id: null,
579
+ },
580
+ });
581
+ }
582
+ async syncPersonMetadata(tx, personId, data) {
583
+ if (data.notes !== undefined) {
584
+ await this.upsertMetadataValue(tx, personId, NOTES_METADATA_KEY, data.notes);
585
+ }
586
+ if (data.employer_company_id !== undefined) {
587
+ await this.upsertMetadataValue(tx, personId, EMPLOYER_COMPANY_METADATA_KEY, data.employer_company_id);
588
+ }
589
+ }
590
+ async upsertMetadataValue(tx, personId, key, value) {
591
+ const existing = await tx.person_metadata.findFirst({
592
+ where: {
593
+ person_id: personId,
594
+ key,
595
+ },
596
+ select: { id: true },
597
+ });
598
+ const normalizedValue = this.normalizeMetadataValue(value);
599
+ if (normalizedValue == null) {
600
+ if (existing) {
601
+ await tx.person_metadata.delete({ where: { id: existing.id } });
602
+ }
603
+ return;
604
+ }
605
+ if (existing) {
606
+ await tx.person_metadata.update({
607
+ where: { id: existing.id },
608
+ data: { value: normalizedValue },
609
+ });
610
+ return;
611
+ }
612
+ await tx.person_metadata.create({
613
+ data: {
614
+ person_id: personId,
615
+ key,
616
+ value: normalizedValue,
617
+ },
618
+ });
619
+ }
620
+ normalizeMetadataValue(value) {
621
+ if (value == null)
622
+ return null;
623
+ if (typeof value === 'string') {
624
+ const trimmed = value.trim();
625
+ return trimmed.length > 0 ? trimmed : null;
626
+ }
627
+ if (typeof value === 'number') {
628
+ return Number.isFinite(value) ? value : null;
629
+ }
630
+ if (typeof value === 'boolean') {
631
+ return value;
632
+ }
633
+ return null;
634
+ }
635
+ async syncContacts(tx, personId, incomingContacts) {
636
+ const existingContacts = await tx.contact.findMany({ where: { person_id: personId } });
637
+ for (const contact of incomingContacts) {
638
+ if (contact.id) {
639
+ await tx.contact.update({ where: { id: contact.id }, data: contact });
640
+ }
641
+ else {
642
+ await tx.contact.create({ data: Object.assign(Object.assign({}, contact), { person_id: personId }) });
643
+ }
644
+ }
645
+ for (const old of existingContacts) {
646
+ if (!incomingContacts.find((item) => item.id === old.id)) {
647
+ await tx.contact.delete({ where: { id: old.id } });
648
+ }
649
+ }
650
+ }
651
+ async syncAddresses(tx, personId, incomingAddresses, locale) {
652
+ const existingAddresses = await tx.person_address.findMany({
653
+ where: { person_id: personId },
654
+ include: { address: true },
655
+ });
656
+ for (const address of incomingAddresses) {
657
+ const addressData = {
658
+ line1: address.line1,
659
+ line2: address.line2 || '',
660
+ city: address.city,
661
+ state: address.state,
662
+ country_code: address.country_code || 'BRA',
663
+ postal_code: address.postal_code || '',
664
+ is_primary: address.is_primary,
665
+ address_type: address.address_type,
666
+ };
667
+ if (address.id) {
668
+ const existingLink = existingAddresses.find((item) => item.address_id === address.id);
669
+ if (!existingLink) {
670
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, `Address with ID ${address.id} not found for person ${personId}.`));
671
+ }
672
+ await tx.address.update({
673
+ where: { id: address.id },
674
+ data: addressData,
675
+ });
676
+ }
677
+ else {
678
+ const createdAddress = await tx.address.create({
679
+ data: addressData,
680
+ });
681
+ await tx.person_address.create({
682
+ data: {
683
+ person_id: personId,
684
+ address_id: createdAddress.id,
685
+ },
686
+ });
687
+ }
688
+ }
689
+ for (const old of existingAddresses) {
690
+ if (!incomingAddresses.find((item) => item.id === old.address_id)) {
691
+ await tx.person_address.delete({ where: { id: old.id } });
692
+ await tx.address.delete({ where: { id: old.address_id } });
693
+ }
694
+ }
695
+ }
696
+ async syncDocuments(tx, personId, incomingDocuments) {
697
+ const existingDocuments = await tx.document.findMany({ where: { person_id: personId } });
698
+ for (const document of incomingDocuments) {
699
+ if (document.id) {
700
+ await tx.document.update({ where: { id: document.id }, data: document });
701
+ }
702
+ else {
703
+ await tx.document.create({ data: Object.assign(Object.assign({}, document), { person_id: personId }) });
704
+ }
705
+ }
706
+ for (const old of existingDocuments) {
707
+ if (!incomingDocuments.find((item) => item.id === old.id)) {
708
+ await tx.document.delete({ where: { id: old.id } });
709
+ }
710
+ }
711
+ }
712
+ validateSinglePrimaryPerType(items, groupKey, locale, localeKey, fallbackMessage) {
713
+ const map = new Map();
714
+ for (const item of items) {
715
+ if (!item.is_primary)
716
+ continue;
717
+ const key = String(item[groupKey]);
718
+ map.set(key, (map.get(key) || 0) + 1);
719
+ }
720
+ for (const count of map.values()) {
721
+ if (count > 1) {
722
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)(localeKey, locale, fallbackMessage));
723
+ }
724
+ }
725
+ }
726
+ parseDateOrNull(value) {
727
+ if (!value)
728
+ return null;
729
+ const date = value instanceof Date ? value : new Date(String(value));
730
+ return Number.isNaN(date.getTime()) ? null : date;
731
+ }
732
+ normalizeTextOrNull(value) {
733
+ if (typeof value !== 'string') {
734
+ return value == null ? null : String(value);
735
+ }
736
+ const trimmed = value.trim();
737
+ return trimmed.length > 0 ? trimmed : null;
738
+ }
739
+ coerceNumber(value) {
740
+ const parsed = Number(value);
741
+ return Number.isFinite(parsed) ? parsed : 0;
742
+ }
743
+ async buildSearchFilters(search) {
744
+ const normalizedDigits = this.normalizeDigits(search);
745
+ const filters = [];
746
+ if (!Number.isNaN(+search)) {
747
+ filters.push({ id: { equals: +search } });
748
+ }
749
+ filters.push({ name: { contains: search, mode: 'insensitive' } }, {
750
+ person_metadata: {
751
+ some: {
752
+ value: { path: [], equals: search },
753
+ },
754
+ },
755
+ }, {
756
+ contact: {
757
+ some: {
758
+ value: { contains: search, mode: 'insensitive' },
759
+ },
760
+ },
761
+ }, {
762
+ document: {
763
+ some: {
764
+ value: { contains: search, mode: 'insensitive' },
765
+ },
766
+ },
767
+ }, {
768
+ person_address: {
769
+ some: {
770
+ address: {
771
+ OR: [
772
+ { line1: { contains: search, mode: 'insensitive' } },
773
+ { line2: { contains: search, mode: 'insensitive' } },
774
+ { city: { contains: search, mode: 'insensitive' } },
775
+ { state: { contains: search, mode: 'insensitive' } },
776
+ { postal_code: { contains: search, mode: 'insensitive' } },
777
+ { country_code: { contains: search, mode: 'insensitive' } },
778
+ ],
779
+ },
780
+ },
781
+ },
782
+ });
783
+ if (normalizedDigits.length > 0) {
784
+ const normalizedMatches = await this.findPersonIdsByNormalizedDigits(normalizedDigits);
785
+ if (normalizedMatches.length > 0) {
786
+ filters.push({
787
+ id: {
788
+ in: normalizedMatches,
789
+ },
790
+ });
791
+ }
792
+ }
793
+ return filters;
794
+ }
795
+ normalizeDigits(value) {
796
+ return value.replace(/\D/g, '');
797
+ }
798
+ async findPersonIdsByNormalizedDigits(normalizedDigits) {
799
+ const likeValue = `%${normalizedDigits}%`;
800
+ const rows = await this.prismaService.$queryRaw(api_prisma_1.Prisma.sql `
801
+ SELECT DISTINCT p.id
802
+ FROM person p
803
+ LEFT JOIN contact c ON c.person_id = p.id
804
+ LEFT JOIN document d ON d.person_id = p.id
805
+ LEFT JOIN person_address pa ON pa.person_id = p.id
806
+ LEFT JOIN address a ON a.id = pa.address_id
807
+ WHERE regexp_replace(COALESCE(c.value, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
808
+ OR regexp_replace(COALESCE(d.value, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
809
+ OR regexp_replace(COALESCE(a.postal_code, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
810
+ `);
811
+ return rows.map((row) => row.id);
812
+ }
813
+ async ensureCompanyRegistrationAllowed({ currentType, nextType, locale, }) {
814
+ const targetType = nextType !== null && nextType !== void 0 ? nextType : currentType;
815
+ const isNewCompanyRegistration = targetType === 'company' && currentType !== 'company';
816
+ if (!isNewCompanyRegistration) {
817
+ return;
818
+ }
819
+ const settings = await this.settingService.getSettingValues(CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING);
820
+ if (settings[CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING] !== false) {
821
+ return;
822
+ }
823
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('companyRegistrationDisabled', locale, 'Company registration is disabled. Only individual records can be created.'));
824
+ }
825
+ async isCompanyRegistrationAllowed() {
826
+ const settings = await this.settingService.getSettingValues(CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING);
827
+ return settings[CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING] !== false;
828
+ }
829
+ async cleanupReplacedAvatar(locale, previousAvatarId, nextAvatarId) {
830
+ if (!previousAvatarId || previousAvatarId === nextAvatarId) {
831
+ return;
832
+ }
833
+ try {
834
+ await this.fileService.delete(locale, { ids: [previousAvatarId] });
835
+ }
836
+ catch (_a) {
837
+ // Keep the person update successful even if avatar cleanup fails.
838
+ }
839
+ }
840
+ normalizeRelationPersonSummary(person) {
841
+ var _a, _b;
842
+ if (!person)
843
+ return null;
844
+ const tradeName = ((_a = person.person_company) === null || _a === void 0 ? void 0 : _a.trade_name) != null
845
+ ? String(person.person_company.trade_name)
846
+ : null;
847
+ return {
848
+ id: person.id,
849
+ name: person.name,
850
+ type: person.type,
851
+ status: person.status,
852
+ avatar_id: (_b = person.avatar_id) !== null && _b !== void 0 ? _b : null,
853
+ trade_name: tradeName,
854
+ };
855
+ }
206
856
  };
207
857
  exports.PersonService = PersonService;
208
858
  exports.PersonService = PersonService = __decorate([
209
859
  (0, common_1.Injectable)(),
210
860
  __param(0, (0, common_1.Inject)((0, common_1.forwardRef)(() => api_prisma_1.PrismaService))),
211
861
  __param(1, (0, common_1.Inject)((0, common_1.forwardRef)(() => api_pagination_1.PaginationService))),
862
+ __param(2, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.FileService))),
863
+ __param(3, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.SettingService))),
212
864
  __metadata("design:paramtypes", [api_prisma_1.PrismaService,
213
- api_pagination_1.PaginationService])
865
+ api_pagination_1.PaginationService,
866
+ core_1.FileService,
867
+ core_1.SettingService])
214
868
  ], PersonService);
215
869
  //# sourceMappingURL=person.service.js.map