@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.
- package/README.md +470 -0
- package/dist/address-type.enum.d.ts +10 -0
- package/dist/address-type.enum.d.ts.map +1 -0
- package/dist/address-type.enum.js +14 -0
- package/dist/address-type.enum.js.map +1 -0
- package/dist/contact.module.d.ts.map +1 -1
- package/dist/contact.module.js +0 -2
- package/dist/contact.module.js.map +1 -1
- package/dist/contact.service.d.ts +19 -22
- package/dist/contact.service.d.ts.map +1 -1
- package/dist/contact.service.js +22 -2
- package/dist/contact.service.js.map +1 -1
- package/dist/index.d.ts +5 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/person/dto/create.dto.d.ts +14 -0
- package/dist/person/dto/create.dto.d.ts.map +1 -1
- package/dist/person/dto/create.dto.js +52 -1
- package/dist/person/dto/create.dto.js.map +1 -1
- package/dist/person/dto/interaction-create.dto.d.ts +16 -0
- package/dist/person/dto/interaction-create.dto.d.ts.map +1 -0
- package/dist/person/dto/interaction-create.dto.js +57 -0
- package/dist/person/dto/interaction-create.dto.js.map +1 -0
- package/dist/person/dto/update.dto.d.ts +17 -1
- package/dist/person/dto/update.dto.d.ts.map +1 -1
- package/dist/person/dto/update.dto.js +79 -3
- package/dist/person/dto/update.dto.js.map +1 -1
- package/dist/person/person.controller.d.ts +37 -8
- package/dist/person/person.controller.d.ts.map +1 -1
- package/dist/person/person.controller.js +29 -3
- package/dist/person/person.controller.js.map +1 -1
- package/dist/person/person.service.d.ts +71 -13
- package/dist/person/person.service.d.ts.map +1 -1
- package/dist/person/person.service.js +762 -108
- package/dist/person/person.service.js.map +1 -1
- package/dist/person-relation-type/person-relation-type.controller.d.ts +13 -9
- package/dist/person-relation-type/person-relation-type.controller.d.ts.map +1 -1
- package/dist/person-relation-type/person-relation-type.service.d.ts +16 -20
- package/dist/person-relation-type/person-relation-type.service.d.ts.map +1 -1
- package/dist/person-relation-type/person-relation-type.service.js +48 -41
- package/dist/person-relation-type/person-relation-type.service.js.map +1 -1
- package/hedhog/data/menu.yaml +2 -16
- package/hedhog/data/role.yaml +9 -1
- package/hedhog/data/route.yaml +10 -21
- package/hedhog/data/setting_group.yaml +21 -0
- package/hedhog/frontend/app/person/_components/delete-person-dialog.tsx.ejs +59 -0
- package/hedhog/frontend/app/person/_components/person-field-with-create.tsx.ejs +831 -0
- package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +2277 -0
- package/hedhog/frontend/app/person/_components/person-types.ts.ejs +157 -0
- package/hedhog/frontend/app/person/page.tsx.ejs +1158 -1335
- package/hedhog/frontend/messages/en.json +114 -4
- package/hedhog/frontend/messages/pt.json +155 -4
- package/hedhog/table/person.yaml +7 -0
- package/hedhog/table/person_address.yaml +18 -0
- package/hedhog/table/person_company.yaml +26 -11
- package/hedhog/table/person_individual.yaml +4 -0
- package/hedhog/table/person_individual_relation.yaml +39 -0
- package/package.json +6 -5
- package/src/address-type.enum.ts +9 -0
- package/src/contact.module.ts +46 -48
- package/src/contact.service.ts +28 -13
- package/src/index.ts +6 -13
- package/src/language/en.json +8 -1
- package/src/language/pt.json +9 -1
- package/src/person/dto/create.dto.ts +49 -1
- package/src/person/dto/update.dto.ts +75 -3
- package/src/person/person.controller.ts +31 -14
- package/src/person/person.service.ts +1019 -121
- package/src/person-relation-type/person-relation-type.service.ts +84 -76
- package/hedhog/data/address_type.yaml +0 -28
- package/hedhog/frontend/app/address-type/page.tsx.ejs +0 -480
- package/hedhog/query/add-unique-address-type-locale.sql +0 -3
- package/hedhog/table/address.yaml +0 -28
- package/hedhog/table/address_type.yaml +0 -11
- package/hedhog/table/person_relation.yaml +0 -20
- package/hedhog/table/person_relation_type.yaml +0 -6
- package/src/address-type/address-type.controller.ts +0 -55
- package/src/address-type/address-type.enum.ts +0 -9
- package/src/address-type/address-type.module.ts +0 -18
- package/src/address-type/address-type.service.ts +0 -121
- package/src/address-type/dto/create.dto.ts +0 -19
- package/src/address-type/dto/update.dto.ts +0 -9
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { DeleteDTO } from '@hed-hog/api';
|
|
2
2
|
import { getLocaleText } from '@hed-hog/api-locale';
|
|
3
3
|
import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
|
|
4
|
-
import { PrismaService } from '@hed-hog/api-prisma';
|
|
4
|
+
import { Prisma, PrismaService } from '@hed-hog/api-prisma';
|
|
5
|
+
import { FileService, SettingService } from '@hed-hog/core';
|
|
5
6
|
import {
|
|
6
7
|
BadRequestException,
|
|
7
8
|
Inject,
|
|
8
9
|
Injectable,
|
|
10
|
+
NotFoundException,
|
|
9
11
|
forwardRef,
|
|
10
12
|
} from '@nestjs/common';
|
|
11
13
|
import { CreateDTO } from './dto/create.dto';
|
|
12
14
|
|
|
15
|
+
const CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING =
|
|
16
|
+
'contact-allow-company-registration';
|
|
17
|
+
const CONTACT_OWNER_ROLE_SLUG = 'owner-contact';
|
|
18
|
+
const CONTACT_OWNER_ALLOWED_ROLE_SLUGS = [
|
|
19
|
+
'admin',
|
|
20
|
+
'admin-contact',
|
|
21
|
+
CONTACT_OWNER_ROLE_SLUG,
|
|
22
|
+
];
|
|
23
|
+
type PersonActivityAction = 'created' | 'updated' | 'interaction_created';
|
|
24
|
+
const EMPLOYER_COMPANY_METADATA_KEY = 'employer_company_id';
|
|
25
|
+
const NOTES_METADATA_KEY = 'notes';
|
|
26
|
+
|
|
13
27
|
@Injectable()
|
|
14
28
|
export class PersonService {
|
|
15
29
|
constructor(
|
|
@@ -17,9 +31,35 @@ export class PersonService {
|
|
|
17
31
|
private readonly prismaService: PrismaService,
|
|
18
32
|
@Inject(forwardRef(() => PaginationService))
|
|
19
33
|
private readonly paginationService: PaginationService,
|
|
34
|
+
@Inject(forwardRef(() => FileService))
|
|
35
|
+
private readonly fileService: FileService,
|
|
36
|
+
@Inject(forwardRef(() => SettingService))
|
|
37
|
+
private readonly settingService: SettingService,
|
|
20
38
|
) {}
|
|
21
39
|
|
|
22
40
|
async getStats() {
|
|
41
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
42
|
+
|
|
43
|
+
if (!allowCompanyRegistration) {
|
|
44
|
+
const [individual, active, inactive] = await Promise.all([
|
|
45
|
+
this.prismaService.person.count({ where: { type: 'individual' } }),
|
|
46
|
+
this.prismaService.person.count({
|
|
47
|
+
where: { type: 'individual', status: 'active' },
|
|
48
|
+
}),
|
|
49
|
+
this.prismaService.person.count({
|
|
50
|
+
where: { type: 'individual', status: 'inactive' },
|
|
51
|
+
}),
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
total: individual,
|
|
56
|
+
individual,
|
|
57
|
+
company: 0,
|
|
58
|
+
active,
|
|
59
|
+
inactive,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
23
63
|
const [total, individual, company, active, inactive] = await Promise.all([
|
|
24
64
|
this.prismaService.person.count(),
|
|
25
65
|
this.prismaService.person.count({ where: { type: 'individual' } }),
|
|
@@ -37,173 +77,266 @@ export class PersonService {
|
|
|
37
77
|
};
|
|
38
78
|
}
|
|
39
79
|
|
|
40
|
-
async
|
|
41
|
-
const
|
|
42
|
-
|
|
80
|
+
async getOwnerOptions(currentUserId?: number) {
|
|
81
|
+
const where: Prisma.userWhereInput = {
|
|
82
|
+
OR: [
|
|
83
|
+
{
|
|
84
|
+
role_user: {
|
|
85
|
+
some: {
|
|
86
|
+
role: {
|
|
87
|
+
slug: {
|
|
88
|
+
in: CONTACT_OWNER_ALLOWED_ROLE_SLUGS,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
...(Number(currentUserId) > 0
|
|
95
|
+
? [
|
|
96
|
+
{
|
|
97
|
+
id: Number(currentUserId),
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
: []),
|
|
101
|
+
],
|
|
102
|
+
};
|
|
43
103
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
104
|
+
const users = await this.prismaService.user.findMany({
|
|
105
|
+
where,
|
|
106
|
+
select: {
|
|
107
|
+
id: true,
|
|
108
|
+
name: true,
|
|
109
|
+
},
|
|
110
|
+
orderBy: {
|
|
111
|
+
name: 'asc',
|
|
112
|
+
},
|
|
113
|
+
take: 500,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const byId = new Map<number, { id: number; name: string }>();
|
|
117
|
+
for (const user of users) {
|
|
118
|
+
if (!user?.id) continue;
|
|
119
|
+
byId.set(user.id, {
|
|
120
|
+
id: user.id,
|
|
121
|
+
name: user.name || `#${user.id}`,
|
|
122
|
+
});
|
|
49
123
|
}
|
|
50
124
|
|
|
125
|
+
return Array.from(byId.values());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async list(
|
|
129
|
+
paginationParams: PaginationDTO & {
|
|
130
|
+
type?: string;
|
|
131
|
+
status?: string;
|
|
132
|
+
owner_user_id?: string | number;
|
|
133
|
+
source?: string;
|
|
134
|
+
lifecycle_stage?: string;
|
|
135
|
+
follow_up_status?: string;
|
|
136
|
+
mine?: string | boolean;
|
|
137
|
+
},
|
|
138
|
+
currentUserId?: number,
|
|
139
|
+
) {
|
|
140
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
51
141
|
const where: any = {};
|
|
52
|
-
|
|
53
|
-
|
|
142
|
+
const search = paginationParams.search?.trim();
|
|
143
|
+
const requestedType = paginationParams.type;
|
|
144
|
+
|
|
145
|
+
if (!allowCompanyRegistration && requestedType === 'company') {
|
|
146
|
+
return this.paginationService.paginate(
|
|
147
|
+
this.prismaService.person,
|
|
148
|
+
paginationParams,
|
|
149
|
+
{
|
|
150
|
+
where: {
|
|
151
|
+
id: -1,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
);
|
|
54
155
|
}
|
|
55
156
|
|
|
56
|
-
if (
|
|
57
|
-
where.type =
|
|
157
|
+
if (!allowCompanyRegistration) {
|
|
158
|
+
where.type = 'individual';
|
|
159
|
+
} else if (requestedType && requestedType !== 'all') {
|
|
160
|
+
where.type = requestedType;
|
|
58
161
|
}
|
|
162
|
+
|
|
59
163
|
if (paginationParams.status && paginationParams.status !== 'all') {
|
|
60
164
|
where.status = paginationParams.status;
|
|
61
165
|
}
|
|
62
166
|
|
|
63
|
-
|
|
167
|
+
if (search) {
|
|
168
|
+
where.OR = await this.buildSearchFilters(search);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result = await this.paginationService.paginate(
|
|
64
172
|
this.prismaService.person,
|
|
65
173
|
paginationParams,
|
|
66
174
|
{
|
|
67
175
|
where,
|
|
68
176
|
include: {
|
|
69
|
-
|
|
177
|
+
person_address: {
|
|
178
|
+
include: {
|
|
179
|
+
address: true,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
70
182
|
contact: true,
|
|
71
183
|
document: true,
|
|
184
|
+
person_metadata: true,
|
|
72
185
|
},
|
|
73
186
|
},
|
|
74
187
|
);
|
|
75
|
-
}
|
|
76
188
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
189
|
+
const enriched = await this.enrichPeople(
|
|
190
|
+
result.data as any[],
|
|
191
|
+
allowCompanyRegistration,
|
|
192
|
+
);
|
|
82
193
|
|
|
83
|
-
return
|
|
194
|
+
return {
|
|
195
|
+
...result,
|
|
196
|
+
data: enriched,
|
|
197
|
+
};
|
|
84
198
|
}
|
|
85
199
|
|
|
86
|
-
async
|
|
87
|
-
|
|
88
|
-
|
|
200
|
+
async get(locale: string, id: number) {
|
|
201
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
202
|
+
|
|
203
|
+
const person = await this.prismaService.person.findUnique({
|
|
204
|
+
where: { id },
|
|
205
|
+
include: {
|
|
206
|
+
person_address: {
|
|
207
|
+
include: {
|
|
208
|
+
address: true,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
contact: true,
|
|
212
|
+
document: true,
|
|
213
|
+
person_metadata: true,
|
|
214
|
+
},
|
|
89
215
|
});
|
|
90
|
-
}
|
|
91
216
|
|
|
92
|
-
async update(id: number, data: any, locale: string) {
|
|
93
|
-
const person = await this.prismaService.person.findUnique({ where: { id } });
|
|
94
217
|
if (!person) {
|
|
95
|
-
throw new BadRequestException(
|
|
218
|
+
throw new BadRequestException(
|
|
219
|
+
getLocaleText('personNotFound', locale, `Person with ID ${id} not found`),
|
|
220
|
+
);
|
|
96
221
|
}
|
|
97
222
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
for (const c of incomingContacts) {
|
|
103
|
-
if (c.is_primary) {
|
|
104
|
-
const key = String(c.contact_type_id);
|
|
105
|
-
contactPrimaryMap.set(key, (contactPrimaryMap.get(key) || 0) + 1);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
for (const [_, count] of contactPrimaryMap.entries()) {
|
|
109
|
-
if (count > 1) {
|
|
110
|
-
throw new BadRequestException(
|
|
111
|
-
getLocaleText('moreThanOnePrimaryContact', locale, 'More than one contact of the same type cannot be marked as primary.')
|
|
112
|
-
);
|
|
113
|
-
}
|
|
223
|
+
if (!allowCompanyRegistration && person.type === 'company') {
|
|
224
|
+
throw new NotFoundException(
|
|
225
|
+
getLocaleText('personNotFound', locale, `Person with ID ${id} not found`),
|
|
226
|
+
);
|
|
114
227
|
}
|
|
115
228
|
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
for (const [_, count] of addressPrimaryMap.entries()) {
|
|
124
|
-
if (count > 1) {
|
|
125
|
-
throw new BadRequestException(
|
|
126
|
-
getLocaleText('moreThanOnePrimaryAddress', locale, 'More than one address of the same type cannot be marked as primary.')
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
229
|
+
const [normalized] = await this.enrichPeople(
|
|
230
|
+
[person as any],
|
|
231
|
+
allowCompanyRegistration,
|
|
232
|
+
);
|
|
233
|
+
return normalized;
|
|
234
|
+
}
|
|
130
235
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
for (const [_, count] of documentPrimaryMap.entries()) {
|
|
139
|
-
if (count > 1) {
|
|
140
|
-
throw new BadRequestException(
|
|
141
|
-
getLocaleText('moreThanOnePrimaryDocument', locale, 'More than one document of the same type cannot be marked as primary.')
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
236
|
+
async create(data: CreateDTO, locale: string) {
|
|
237
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
238
|
+
|
|
239
|
+
await this.ensureCompanyRegistrationAllowed({
|
|
240
|
+
nextType: data.type,
|
|
241
|
+
locale,
|
|
242
|
+
});
|
|
145
243
|
|
|
146
244
|
return this.prismaService.$transaction(async (tx) => {
|
|
147
|
-
await tx.person.
|
|
148
|
-
where: { id },
|
|
245
|
+
const person = await tx.person.create({
|
|
149
246
|
data: {
|
|
150
247
|
name: data.name,
|
|
151
248
|
type: data.type,
|
|
152
249
|
status: data.status,
|
|
250
|
+
avatar_id: data.avatar_id ?? null,
|
|
153
251
|
},
|
|
154
252
|
});
|
|
155
253
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
254
|
+
await this.syncPersonSubtypeData(tx, person.id, null, data, locale);
|
|
255
|
+
await this.syncPersonMetadata(tx, person.id, {
|
|
256
|
+
notes: data.notes,
|
|
257
|
+
employer_company_id: allowCompanyRegistration
|
|
258
|
+
? data.employer_company_id ?? null
|
|
259
|
+
: null,
|
|
260
|
+
});
|
|
164
261
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
}
|
|
262
|
+
return person;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
170
265
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
for (const old of existingAddresses) {
|
|
180
|
-
if (!incomingAddresses.find((a: any) => a.id === old.id)) {
|
|
181
|
-
await tx.address.delete({ where: { id: old.id } });
|
|
182
|
-
}
|
|
183
|
-
}
|
|
266
|
+
async update(id: number, data: any, locale: string) {
|
|
267
|
+
const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
|
|
268
|
+
const person = await this.prismaService.person.findUnique({ where: { id } });
|
|
269
|
+
if (!person) {
|
|
270
|
+
throw new BadRequestException(
|
|
271
|
+
getLocaleText('personNotFound', locale, `Person with ID ${id} not found`),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
184
274
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
await tx.document.create({ data: { ...d, person_id: id } });
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
for (const old of existingDocuments) {
|
|
194
|
-
if (!incomingDocuments.find((d: any) => d.id === old.id)) {
|
|
195
|
-
await tx.document.delete({ where: { id: old.id } });
|
|
196
|
-
}
|
|
197
|
-
}
|
|
275
|
+
if (!allowCompanyRegistration && person.type === 'company') {
|
|
276
|
+
throw new NotFoundException(
|
|
277
|
+
getLocaleText('personNotFound', locale, `Person with ID ${id} not found`),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
198
280
|
|
|
199
|
-
|
|
281
|
+
await this.ensureCompanyRegistrationAllowed({
|
|
282
|
+
currentType: person.type,
|
|
283
|
+
nextType: data.type,
|
|
284
|
+
locale,
|
|
200
285
|
});
|
|
286
|
+
|
|
287
|
+
const incomingContacts = Array.isArray(data.contacts) ? data.contacts : [];
|
|
288
|
+
const incomingAddresses = Array.isArray(data.addresses) ? data.addresses : [];
|
|
289
|
+
const incomingDocuments = Array.isArray(data.documents) ? data.documents : [];
|
|
290
|
+
|
|
291
|
+
this.validateSinglePrimaryPerType(incomingContacts, 'contact_type_id', locale, 'moreThanOnePrimaryContact', 'More than one contact of the same type cannot be marked as primary.');
|
|
292
|
+
this.validateSinglePrimaryPerType(incomingAddresses, 'address_type', locale, 'moreThanOnePrimaryAddress', 'More than one address of the same type cannot be marked as primary.');
|
|
293
|
+
this.validateSinglePrimaryPerType(incomingDocuments, 'document_type_id', locale, 'moreThanOnePrimaryDocument', 'More than one document of the same type cannot be marked as primary.');
|
|
294
|
+
|
|
295
|
+
return this.prismaService
|
|
296
|
+
.$transaction(async (tx) => {
|
|
297
|
+
const nextType = data.type ?? person.type;
|
|
298
|
+
|
|
299
|
+
await tx.person.update({
|
|
300
|
+
where: { id },
|
|
301
|
+
data: {
|
|
302
|
+
name: data.name ?? person.name,
|
|
303
|
+
type: nextType,
|
|
304
|
+
status: data.status ?? person.status,
|
|
305
|
+
avatar_id:
|
|
306
|
+
data.avatar_id === undefined ? person.avatar_id : data.avatar_id,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await this.syncPersonSubtypeData(tx, id, person.type, data, locale);
|
|
311
|
+
await this.syncPersonMetadata(tx, id, {
|
|
312
|
+
notes: data.notes,
|
|
313
|
+
employer_company_id: allowCompanyRegistration
|
|
314
|
+
? data.employer_company_id === undefined
|
|
315
|
+
? undefined
|
|
316
|
+
: data.employer_company_id
|
|
317
|
+
: null,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
await this.syncContacts(tx, id, incomingContacts);
|
|
321
|
+
await this.syncAddresses(tx, id, incomingAddresses, locale);
|
|
322
|
+
await this.syncDocuments(tx, id, incomingDocuments);
|
|
323
|
+
|
|
324
|
+
return { success: true };
|
|
325
|
+
})
|
|
326
|
+
.then(async (result) => {
|
|
327
|
+
await this.cleanupReplacedAvatar(locale, person.avatar_id, data.avatar_id);
|
|
328
|
+
return result;
|
|
329
|
+
});
|
|
201
330
|
}
|
|
202
331
|
|
|
203
332
|
async delete({ ids }: DeleteDTO, locale: string) {
|
|
204
333
|
if (ids == undefined || ids == null) {
|
|
205
334
|
throw new BadRequestException(
|
|
206
|
-
getLocaleText(
|
|
335
|
+
getLocaleText(
|
|
336
|
+
'deleteItemsRequired',
|
|
337
|
+
locale,
|
|
338
|
+
'You must select at least one item to delete.',
|
|
339
|
+
),
|
|
207
340
|
);
|
|
208
341
|
}
|
|
209
342
|
|
|
@@ -211,22 +344,62 @@ export class PersonService {
|
|
|
211
344
|
where: { id: { in: ids } },
|
|
212
345
|
select: { id: true },
|
|
213
346
|
});
|
|
214
|
-
const existingIds = existing.map(p => p.id);
|
|
215
|
-
const missingIds = ids.filter(
|
|
347
|
+
const existingIds = existing.map((p) => p.id);
|
|
348
|
+
const missingIds = ids.filter((itemId) => !existingIds.includes(itemId));
|
|
216
349
|
if (missingIds.length > 0) {
|
|
217
350
|
throw new BadRequestException(
|
|
218
351
|
getLocaleText(
|
|
219
352
|
'personNotFound',
|
|
220
353
|
locale,
|
|
221
|
-
`Person(s) with ID(s) ${missingIds.join(', ')} not found
|
|
222
|
-
)
|
|
354
|
+
`Person(s) with ID(s) ${missingIds.join(', ')} not found.`,
|
|
355
|
+
),
|
|
223
356
|
);
|
|
224
357
|
}
|
|
225
358
|
|
|
359
|
+
const personAddresses = await this.prismaService.person_address.findMany({
|
|
360
|
+
where: {
|
|
361
|
+
person_id: { in: ids },
|
|
362
|
+
},
|
|
363
|
+
select: {
|
|
364
|
+
id: true,
|
|
365
|
+
address_id: true,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const addressIds = personAddresses.map((item) => item.address_id);
|
|
370
|
+
|
|
226
371
|
return this.prismaService.$transaction([
|
|
227
372
|
this.prismaService.contact.deleteMany({ where: { person_id: { in: ids } } }),
|
|
228
|
-
this.prismaService.address.deleteMany({ where: { person_id: { in: ids } } }),
|
|
229
373
|
this.prismaService.document.deleteMany({ where: { person_id: { in: ids } } }),
|
|
374
|
+
this.prismaService.person_individual_relation.deleteMany({
|
|
375
|
+
where: {
|
|
376
|
+
OR: [
|
|
377
|
+
{ person_individual_id: { in: ids } },
|
|
378
|
+
{ related_person_individual_id: { in: ids } },
|
|
379
|
+
],
|
|
380
|
+
},
|
|
381
|
+
}),
|
|
382
|
+
this.prismaService.person_company.deleteMany({
|
|
383
|
+
where: {
|
|
384
|
+
OR: [{ id: { in: ids } }, { headquarter_id: { in: ids } }],
|
|
385
|
+
},
|
|
386
|
+
}),
|
|
387
|
+
this.prismaService.person_individual.deleteMany({
|
|
388
|
+
where: { id: { in: ids } },
|
|
389
|
+
}),
|
|
390
|
+
this.prismaService.person_metadata.deleteMany({
|
|
391
|
+
where: { person_id: { in: ids } },
|
|
392
|
+
}),
|
|
393
|
+
this.prismaService.person_address.deleteMany({
|
|
394
|
+
where: { person_id: { in: ids } },
|
|
395
|
+
}),
|
|
396
|
+
this.prismaService.address.deleteMany({
|
|
397
|
+
where: {
|
|
398
|
+
id: {
|
|
399
|
+
in: addressIds,
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
}),
|
|
230
403
|
this.prismaService.person.deleteMany({
|
|
231
404
|
where: {
|
|
232
405
|
id: {
|
|
@@ -236,4 +409,729 @@ export class PersonService {
|
|
|
236
409
|
}),
|
|
237
410
|
]);
|
|
238
411
|
}
|
|
412
|
+
|
|
413
|
+
async openPublicAvatar(locale: string, fileId: number, res: any) {
|
|
414
|
+
const personWithAvatar = await this.prismaService.person.findFirst({
|
|
415
|
+
where: {
|
|
416
|
+
avatar_id: fileId,
|
|
417
|
+
},
|
|
418
|
+
select: {
|
|
419
|
+
id: true,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
if (!personWithAvatar) {
|
|
424
|
+
throw new NotFoundException(
|
|
425
|
+
getLocaleText('personNotFound', locale, 'Person not found'),
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const { file, buffer } = await this.fileService.getBuffer(fileId);
|
|
430
|
+
|
|
431
|
+
res.set({
|
|
432
|
+
'Content-Type': file.file_mimetype.name,
|
|
433
|
+
'Content-Length': buffer.length,
|
|
434
|
+
'Cache-Control': 'public, max-age=3600',
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
res.send(buffer);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private async enrichPeople(people: any[], allowCompanyRegistration = true) {
|
|
441
|
+
if (people.length === 0) {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const personIds = people.map((person) => person.id);
|
|
446
|
+
|
|
447
|
+
const [companies, individuals, companyBranches, employerMetadata] =
|
|
448
|
+
await Promise.all([
|
|
449
|
+
this.prismaService.person_company.findMany({
|
|
450
|
+
where: { id: { in: personIds } },
|
|
451
|
+
}),
|
|
452
|
+
this.prismaService.person_individual.findMany({
|
|
453
|
+
where: { id: { in: personIds } },
|
|
454
|
+
}),
|
|
455
|
+
this.prismaService.person_company.findMany({
|
|
456
|
+
where: { headquarter_id: { in: personIds } },
|
|
457
|
+
select: { id: true, headquarter_id: true },
|
|
458
|
+
}),
|
|
459
|
+
this.prismaService.person_metadata.findMany({
|
|
460
|
+
where: {
|
|
461
|
+
person_id: { in: personIds },
|
|
462
|
+
key: EMPLOYER_COMPANY_METADATA_KEY,
|
|
463
|
+
},
|
|
464
|
+
select: {
|
|
465
|
+
person_id: true,
|
|
466
|
+
value: true,
|
|
467
|
+
},
|
|
468
|
+
}).then((metadata) => (allowCompanyRegistration ? metadata : [])),
|
|
469
|
+
]);
|
|
470
|
+
|
|
471
|
+
const companyById = new Map(companies.map((item) => [item.id, item]));
|
|
472
|
+
const individualById = new Map(individuals.map((item) => [item.id, item]));
|
|
473
|
+
|
|
474
|
+
const branchesByHeadquarterId = new Map<number, number[]>();
|
|
475
|
+
for (const branch of companyBranches) {
|
|
476
|
+
if (!branch.headquarter_id) continue;
|
|
477
|
+
const current = branchesByHeadquarterId.get(branch.headquarter_id) || [];
|
|
478
|
+
current.push(branch.id);
|
|
479
|
+
branchesByHeadquarterId.set(branch.headquarter_id, current);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const employerCompanyIdByPersonId = new Map<number, number>();
|
|
483
|
+
for (const meta of employerMetadata) {
|
|
484
|
+
const employerId = this.coerceNumber(meta.value);
|
|
485
|
+
if (employerId > 0) {
|
|
486
|
+
employerCompanyIdByPersonId.set(meta.person_id, employerId);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const employerCompanyIds = Array.from(
|
|
491
|
+
new Set(Array.from(employerCompanyIdByPersonId.values())),
|
|
492
|
+
);
|
|
493
|
+
const employerCompanies =
|
|
494
|
+
employerCompanyIds.length > 0
|
|
495
|
+
? await this.prismaService.person.findMany({
|
|
496
|
+
where: {
|
|
497
|
+
id: { in: employerCompanyIds },
|
|
498
|
+
type: 'company',
|
|
499
|
+
},
|
|
500
|
+
})
|
|
501
|
+
: [];
|
|
502
|
+
const employerCompanyRows =
|
|
503
|
+
employerCompanyIds.length > 0
|
|
504
|
+
? await this.prismaService.person_company.findMany({
|
|
505
|
+
where: {
|
|
506
|
+
id: { in: employerCompanyIds },
|
|
507
|
+
},
|
|
508
|
+
})
|
|
509
|
+
: [];
|
|
510
|
+
const employerCompanyRowById = new Map(
|
|
511
|
+
employerCompanyRows.map((item) => [item.id, item]),
|
|
512
|
+
);
|
|
513
|
+
const employerCompanyById = new Map(
|
|
514
|
+
employerCompanies.map((item: any) => [
|
|
515
|
+
item.id,
|
|
516
|
+
{
|
|
517
|
+
...item,
|
|
518
|
+
person_company: employerCompanyRowById.get(item.id) ?? null,
|
|
519
|
+
},
|
|
520
|
+
]),
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
return people.map((person) => {
|
|
524
|
+
const metadata = this.metadataArrayToMap(person.person_metadata);
|
|
525
|
+
const companyData = companyById.get(person.id);
|
|
526
|
+
const individualData = individualById.get(person.id);
|
|
527
|
+
const employerCompanyId = allowCompanyRegistration
|
|
528
|
+
? employerCompanyIdByPersonId.get(person.id) ?? null
|
|
529
|
+
: null;
|
|
530
|
+
const employerCompany =
|
|
531
|
+
allowCompanyRegistration && employerCompanyId != null
|
|
532
|
+
? this.normalizeRelationPersonSummary(
|
|
533
|
+
employerCompanyById.get(employerCompanyId),
|
|
534
|
+
)
|
|
535
|
+
: null;
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
id: person.id,
|
|
539
|
+
name: person.name,
|
|
540
|
+
type: person.type,
|
|
541
|
+
status: person.status,
|
|
542
|
+
avatar_id: person.avatar_id ?? null,
|
|
543
|
+
created_at: person.created_at,
|
|
544
|
+
updated_at: person.updated_at,
|
|
545
|
+
birth_date: individualData?.birth_date ?? null,
|
|
546
|
+
gender: individualData?.gender ?? null,
|
|
547
|
+
job_title: individualData?.job_title ?? null,
|
|
548
|
+
trade_name: companyData?.trade_name ?? null,
|
|
549
|
+
foundation_date: companyData?.foundation_date ?? null,
|
|
550
|
+
legal_nature: companyData?.legal_nature ?? null,
|
|
551
|
+
headquarter_id: companyData?.headquarter_id ?? null,
|
|
552
|
+
branch_ids: branchesByHeadquarterId.get(person.id) || [],
|
|
553
|
+
notes: this.metadataToString(metadata.get(NOTES_METADATA_KEY)),
|
|
554
|
+
employer_company_id: allowCompanyRegistration ? employerCompanyId : null,
|
|
555
|
+
employer_company: allowCompanyRegistration ? employerCompany : null,
|
|
556
|
+
contact: person.contact || [],
|
|
557
|
+
document: person.document || [],
|
|
558
|
+
address: Array.isArray(person.person_address)
|
|
559
|
+
? person.person_address
|
|
560
|
+
.map((item: any) => item.address)
|
|
561
|
+
.filter((address: any) => address != null)
|
|
562
|
+
: [],
|
|
563
|
+
};
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private metadataArrayToMap(metadata: any[]) {
|
|
568
|
+
const map = new Map<string, unknown>();
|
|
569
|
+
|
|
570
|
+
for (const item of Array.isArray(metadata) ? metadata : []) {
|
|
571
|
+
map.set(item.key, item.value);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return map;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private metadataToString(value: unknown): string | null {
|
|
578
|
+
if (value == null) return null;
|
|
579
|
+
if (typeof value === 'string') return value;
|
|
580
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
581
|
+
return String(value);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private async syncPersonSubtypeData(
|
|
588
|
+
tx: any,
|
|
589
|
+
personId: number,
|
|
590
|
+
currentType: string | null,
|
|
591
|
+
data: any,
|
|
592
|
+
locale: string,
|
|
593
|
+
) {
|
|
594
|
+
const targetType = data.type ?? currentType;
|
|
595
|
+
if (!targetType) return;
|
|
596
|
+
|
|
597
|
+
if (targetType === 'individual') {
|
|
598
|
+
await tx.person_company.deleteMany({ where: { id: personId } });
|
|
599
|
+
|
|
600
|
+
await tx.person_individual.upsert({
|
|
601
|
+
where: { id: personId },
|
|
602
|
+
create: {
|
|
603
|
+
id: personId,
|
|
604
|
+
birth_date: this.parseDateOrNull(data.birth_date),
|
|
605
|
+
gender: data.gender ?? null,
|
|
606
|
+
job_title: this.normalizeTextOrNull(data.job_title),
|
|
607
|
+
},
|
|
608
|
+
update: {
|
|
609
|
+
birth_date:
|
|
610
|
+
data.birth_date === undefined
|
|
611
|
+
? undefined
|
|
612
|
+
: this.parseDateOrNull(data.birth_date),
|
|
613
|
+
gender: data.gender === undefined ? undefined : data.gender ?? null,
|
|
614
|
+
job_title:
|
|
615
|
+
data.job_title === undefined
|
|
616
|
+
? undefined
|
|
617
|
+
: this.normalizeTextOrNull(data.job_title),
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
await tx.person_individual_relation.deleteMany({
|
|
624
|
+
where: {
|
|
625
|
+
OR: [
|
|
626
|
+
{ person_individual_id: personId },
|
|
627
|
+
{ related_person_individual_id: personId },
|
|
628
|
+
],
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
await tx.person_individual.deleteMany({ where: { id: personId } });
|
|
632
|
+
|
|
633
|
+
const normalizedHeadquarterId = this.coerceNumber(data.headquarter_id);
|
|
634
|
+
|
|
635
|
+
if (normalizedHeadquarterId > 0) {
|
|
636
|
+
if (normalizedHeadquarterId === personId) {
|
|
637
|
+
throw new BadRequestException(
|
|
638
|
+
getLocaleText(
|
|
639
|
+
'companyRelationSelfReference',
|
|
640
|
+
locale,
|
|
641
|
+
'A company cannot be its own headquarter.',
|
|
642
|
+
),
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const headquarter = await tx.person.findFirst({
|
|
647
|
+
where: { id: normalizedHeadquarterId, type: 'company' },
|
|
648
|
+
select: { id: true },
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
if (!headquarter) {
|
|
652
|
+
throw new BadRequestException(
|
|
653
|
+
getLocaleText(
|
|
654
|
+
'companyRelationInvalidTarget',
|
|
655
|
+
locale,
|
|
656
|
+
'Only company records can be linked as headquarters or branches.',
|
|
657
|
+
),
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
await tx.person_company.upsert({
|
|
663
|
+
where: { id: personId },
|
|
664
|
+
create: {
|
|
665
|
+
id: personId,
|
|
666
|
+
trade_name: this.normalizeTextOrNull(data.trade_name),
|
|
667
|
+
foundation_date: this.parseDateOrNull(data.foundation_date),
|
|
668
|
+
legal_nature: this.normalizeTextOrNull(data.legal_nature),
|
|
669
|
+
headquarter_id: normalizedHeadquarterId > 0 ? normalizedHeadquarterId : null,
|
|
670
|
+
},
|
|
671
|
+
update: {
|
|
672
|
+
trade_name:
|
|
673
|
+
data.trade_name === undefined
|
|
674
|
+
? undefined
|
|
675
|
+
: this.normalizeTextOrNull(data.trade_name),
|
|
676
|
+
foundation_date:
|
|
677
|
+
data.foundation_date === undefined
|
|
678
|
+
? undefined
|
|
679
|
+
: this.parseDateOrNull(data.foundation_date),
|
|
680
|
+
legal_nature:
|
|
681
|
+
data.legal_nature === undefined
|
|
682
|
+
? undefined
|
|
683
|
+
: this.normalizeTextOrNull(data.legal_nature),
|
|
684
|
+
headquarter_id:
|
|
685
|
+
data.headquarter_id === undefined
|
|
686
|
+
? undefined
|
|
687
|
+
: normalizedHeadquarterId > 0
|
|
688
|
+
? normalizedHeadquarterId
|
|
689
|
+
: null,
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
if (Object.prototype.hasOwnProperty.call(data, 'branch_ids')) {
|
|
694
|
+
await this.syncCompanyBranches(tx, personId, data.branch_ids, locale);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private async syncCompanyBranches(
|
|
699
|
+
tx: any,
|
|
700
|
+
personId: number,
|
|
701
|
+
branchIds: number[] | undefined,
|
|
702
|
+
locale: string,
|
|
703
|
+
) {
|
|
704
|
+
const normalizedBranchIds = Array.from(
|
|
705
|
+
new Set(
|
|
706
|
+
(Array.isArray(branchIds) ? branchIds : [])
|
|
707
|
+
.map((value) => Number(value))
|
|
708
|
+
.filter((value) => Number.isInteger(value) && value > 0 && value !== personId),
|
|
709
|
+
),
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
if (normalizedBranchIds.length > 0) {
|
|
713
|
+
const companies = await tx.person.findMany({
|
|
714
|
+
where: {
|
|
715
|
+
id: { in: normalizedBranchIds },
|
|
716
|
+
type: 'company',
|
|
717
|
+
},
|
|
718
|
+
select: { id: true },
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
if (companies.length !== normalizedBranchIds.length) {
|
|
722
|
+
throw new BadRequestException(
|
|
723
|
+
getLocaleText(
|
|
724
|
+
'companyRelationInvalidTarget',
|
|
725
|
+
locale,
|
|
726
|
+
'Only company records can be linked as headquarters or branches.',
|
|
727
|
+
),
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
await tx.person_company.updateMany({
|
|
732
|
+
where: {
|
|
733
|
+
id: { in: normalizedBranchIds },
|
|
734
|
+
},
|
|
735
|
+
data: {
|
|
736
|
+
headquarter_id: personId,
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
await tx.person_company.updateMany({
|
|
742
|
+
where: {
|
|
743
|
+
headquarter_id: personId,
|
|
744
|
+
id: { notIn: normalizedBranchIds.length > 0 ? normalizedBranchIds : [-1] },
|
|
745
|
+
},
|
|
746
|
+
data: {
|
|
747
|
+
headquarter_id: null,
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
private async syncPersonMetadata(
|
|
753
|
+
tx: any,
|
|
754
|
+
personId: number,
|
|
755
|
+
data: { notes?: unknown; employer_company_id?: unknown },
|
|
756
|
+
) {
|
|
757
|
+
if (data.notes !== undefined) {
|
|
758
|
+
await this.upsertMetadataValue(tx, personId, NOTES_METADATA_KEY, data.notes);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (data.employer_company_id !== undefined) {
|
|
762
|
+
await this.upsertMetadataValue(
|
|
763
|
+
tx,
|
|
764
|
+
personId,
|
|
765
|
+
EMPLOYER_COMPANY_METADATA_KEY,
|
|
766
|
+
data.employer_company_id,
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private async upsertMetadataValue(
|
|
772
|
+
tx: any,
|
|
773
|
+
personId: number,
|
|
774
|
+
key: string,
|
|
775
|
+
value: unknown,
|
|
776
|
+
) {
|
|
777
|
+
const existing = await tx.person_metadata.findFirst({
|
|
778
|
+
where: {
|
|
779
|
+
person_id: personId,
|
|
780
|
+
key,
|
|
781
|
+
},
|
|
782
|
+
select: { id: true },
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
const normalizedValue = this.normalizeMetadataValue(value);
|
|
786
|
+
|
|
787
|
+
if (normalizedValue == null) {
|
|
788
|
+
if (existing) {
|
|
789
|
+
await tx.person_metadata.delete({ where: { id: existing.id } });
|
|
790
|
+
}
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (existing) {
|
|
795
|
+
await tx.person_metadata.update({
|
|
796
|
+
where: { id: existing.id },
|
|
797
|
+
data: { value: normalizedValue },
|
|
798
|
+
});
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
await tx.person_metadata.create({
|
|
803
|
+
data: {
|
|
804
|
+
person_id: personId,
|
|
805
|
+
key,
|
|
806
|
+
value: normalizedValue,
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private normalizeMetadataValue(value: unknown): string | number | boolean | null {
|
|
812
|
+
if (value == null) return null;
|
|
813
|
+
|
|
814
|
+
if (typeof value === 'string') {
|
|
815
|
+
const trimmed = value.trim();
|
|
816
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (typeof value === 'number') {
|
|
820
|
+
return Number.isFinite(value) ? value : null;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (typeof value === 'boolean') {
|
|
824
|
+
return value;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private async syncContacts(tx: any, personId: number, incomingContacts: any[]) {
|
|
831
|
+
const existingContacts = await tx.contact.findMany({ where: { person_id: personId } });
|
|
832
|
+
|
|
833
|
+
for (const contact of incomingContacts) {
|
|
834
|
+
if (contact.id) {
|
|
835
|
+
await tx.contact.update({ where: { id: contact.id }, data: contact });
|
|
836
|
+
} else {
|
|
837
|
+
await tx.contact.create({ data: { ...contact, person_id: personId } });
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
for (const old of existingContacts) {
|
|
842
|
+
if (!incomingContacts.find((item: any) => item.id === old.id)) {
|
|
843
|
+
await tx.contact.delete({ where: { id: old.id } });
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
private async syncAddresses(
|
|
849
|
+
tx: any,
|
|
850
|
+
personId: number,
|
|
851
|
+
incomingAddresses: any[],
|
|
852
|
+
locale: string,
|
|
853
|
+
) {
|
|
854
|
+
const existingAddresses = await tx.person_address.findMany({
|
|
855
|
+
where: { person_id: personId },
|
|
856
|
+
include: { address: true },
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
for (const address of incomingAddresses) {
|
|
860
|
+
const addressData = {
|
|
861
|
+
line1: address.line1,
|
|
862
|
+
line2: address.line2 || '',
|
|
863
|
+
city: address.city,
|
|
864
|
+
state: address.state,
|
|
865
|
+
country_code: address.country_code || 'BRA',
|
|
866
|
+
postal_code: address.postal_code || '',
|
|
867
|
+
is_primary: address.is_primary,
|
|
868
|
+
address_type: address.address_type,
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
if (address.id) {
|
|
872
|
+
const existingLink = existingAddresses.find(
|
|
873
|
+
(item: any) => item.address_id === address.id,
|
|
874
|
+
);
|
|
875
|
+
|
|
876
|
+
if (!existingLink) {
|
|
877
|
+
throw new BadRequestException(
|
|
878
|
+
getLocaleText(
|
|
879
|
+
'personNotFound',
|
|
880
|
+
locale,
|
|
881
|
+
`Address with ID ${address.id} not found for person ${personId}.`,
|
|
882
|
+
),
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
await tx.address.update({
|
|
887
|
+
where: { id: address.id },
|
|
888
|
+
data: addressData,
|
|
889
|
+
});
|
|
890
|
+
} else {
|
|
891
|
+
const createdAddress = await tx.address.create({
|
|
892
|
+
data: addressData,
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
await tx.person_address.create({
|
|
896
|
+
data: {
|
|
897
|
+
person_id: personId,
|
|
898
|
+
address_id: createdAddress.id,
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
for (const old of existingAddresses) {
|
|
905
|
+
if (!incomingAddresses.find((item: any) => item.id === old.address_id)) {
|
|
906
|
+
await tx.person_address.delete({ where: { id: old.id } });
|
|
907
|
+
await tx.address.delete({ where: { id: old.address_id } });
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
private async syncDocuments(tx: any, personId: number, incomingDocuments: any[]) {
|
|
913
|
+
const existingDocuments = await tx.document.findMany({ where: { person_id: personId } });
|
|
914
|
+
|
|
915
|
+
for (const document of incomingDocuments) {
|
|
916
|
+
if (document.id) {
|
|
917
|
+
await tx.document.update({ where: { id: document.id }, data: document });
|
|
918
|
+
} else {
|
|
919
|
+
await tx.document.create({ data: { ...document, person_id: personId } });
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
for (const old of existingDocuments) {
|
|
924
|
+
if (!incomingDocuments.find((item: any) => item.id === old.id)) {
|
|
925
|
+
await tx.document.delete({ where: { id: old.id } });
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
private validateSinglePrimaryPerType(
|
|
931
|
+
items: any[],
|
|
932
|
+
groupKey: string,
|
|
933
|
+
locale: string,
|
|
934
|
+
localeKey: string,
|
|
935
|
+
fallbackMessage: string,
|
|
936
|
+
) {
|
|
937
|
+
const map = new Map<string, number>();
|
|
938
|
+
|
|
939
|
+
for (const item of items) {
|
|
940
|
+
if (!item.is_primary) continue;
|
|
941
|
+
|
|
942
|
+
const key = String(item[groupKey]);
|
|
943
|
+
map.set(key, (map.get(key) || 0) + 1);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
for (const count of map.values()) {
|
|
947
|
+
if (count > 1) {
|
|
948
|
+
throw new BadRequestException(getLocaleText(localeKey, locale, fallbackMessage));
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private parseDateOrNull(value: unknown): Date | null {
|
|
954
|
+
if (!value) return null;
|
|
955
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
956
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
private normalizeTextOrNull(value: unknown): string | null {
|
|
960
|
+
if (typeof value !== 'string') {
|
|
961
|
+
return value == null ? null : String(value);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const trimmed = value.trim();
|
|
965
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
private coerceNumber(value: unknown): number {
|
|
969
|
+
const parsed = Number(value);
|
|
970
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
private async buildSearchFilters(search: string) {
|
|
974
|
+
const normalizedDigits = this.normalizeDigits(search);
|
|
975
|
+
const filters: any[] = [];
|
|
976
|
+
|
|
977
|
+
if (!Number.isNaN(+search)) {
|
|
978
|
+
filters.push({ id: { equals: +search } });
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
filters.push(
|
|
982
|
+
{ name: { contains: search, mode: 'insensitive' } },
|
|
983
|
+
{
|
|
984
|
+
person_metadata: {
|
|
985
|
+
some: {
|
|
986
|
+
value: { path: [], equals: search } as any,
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
contact: {
|
|
992
|
+
some: {
|
|
993
|
+
value: { contains: search, mode: 'insensitive' },
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
document: {
|
|
999
|
+
some: {
|
|
1000
|
+
value: { contains: search, mode: 'insensitive' },
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
person_address: {
|
|
1006
|
+
some: {
|
|
1007
|
+
address: {
|
|
1008
|
+
OR: [
|
|
1009
|
+
{ line1: { contains: search, mode: 'insensitive' } },
|
|
1010
|
+
{ line2: { contains: search, mode: 'insensitive' } },
|
|
1011
|
+
{ city: { contains: search, mode: 'insensitive' } },
|
|
1012
|
+
{ state: { contains: search, mode: 'insensitive' } },
|
|
1013
|
+
{ postal_code: { contains: search, mode: 'insensitive' } },
|
|
1014
|
+
{ country_code: { contains: search, mode: 'insensitive' } },
|
|
1015
|
+
],
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
if (normalizedDigits.length > 0) {
|
|
1023
|
+
const normalizedMatches =
|
|
1024
|
+
await this.findPersonIdsByNormalizedDigits(normalizedDigits);
|
|
1025
|
+
|
|
1026
|
+
if (normalizedMatches.length > 0) {
|
|
1027
|
+
filters.push({
|
|
1028
|
+
id: {
|
|
1029
|
+
in: normalizedMatches,
|
|
1030
|
+
},
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return filters;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private normalizeDigits(value: string) {
|
|
1039
|
+
return value.replace(/\D/g, '');
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
private async findPersonIdsByNormalizedDigits(normalizedDigits: string) {
|
|
1043
|
+
const likeValue = `%${normalizedDigits}%`;
|
|
1044
|
+
|
|
1045
|
+
const rows = await this.prismaService.$queryRaw<Array<{ id: number }>>(
|
|
1046
|
+
Prisma.sql`
|
|
1047
|
+
SELECT DISTINCT p.id
|
|
1048
|
+
FROM person p
|
|
1049
|
+
LEFT JOIN contact c ON c.person_id = p.id
|
|
1050
|
+
LEFT JOIN document d ON d.person_id = p.id
|
|
1051
|
+
LEFT JOIN person_address pa ON pa.person_id = p.id
|
|
1052
|
+
LEFT JOIN address a ON a.id = pa.address_id
|
|
1053
|
+
WHERE regexp_replace(COALESCE(c.value, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
|
|
1054
|
+
OR regexp_replace(COALESCE(d.value, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
|
|
1055
|
+
OR regexp_replace(COALESCE(a.postal_code, ''), '[^0-9]+', '', 'g') ILIKE ${likeValue}
|
|
1056
|
+
`,
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
return rows.map((row) => row.id);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
private async ensureCompanyRegistrationAllowed({
|
|
1063
|
+
currentType,
|
|
1064
|
+
nextType,
|
|
1065
|
+
locale,
|
|
1066
|
+
}: {
|
|
1067
|
+
currentType?: string;
|
|
1068
|
+
nextType?: string;
|
|
1069
|
+
locale: string;
|
|
1070
|
+
}) {
|
|
1071
|
+
const targetType = nextType ?? currentType;
|
|
1072
|
+
const isNewCompanyRegistration =
|
|
1073
|
+
targetType === 'company' && currentType !== 'company';
|
|
1074
|
+
|
|
1075
|
+
if (!isNewCompanyRegistration) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const settings = await this.settingService.getSettingValues(
|
|
1080
|
+
CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING,
|
|
1081
|
+
);
|
|
1082
|
+
|
|
1083
|
+
if (settings[CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING] !== false) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
throw new BadRequestException(
|
|
1088
|
+
getLocaleText(
|
|
1089
|
+
'companyRegistrationDisabled',
|
|
1090
|
+
locale,
|
|
1091
|
+
'Company registration is disabled. Only individual records can be created.',
|
|
1092
|
+
),
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
private async isCompanyRegistrationAllowed() {
|
|
1097
|
+
const settings = await this.settingService.getSettingValues(
|
|
1098
|
+
CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING,
|
|
1099
|
+
);
|
|
1100
|
+
|
|
1101
|
+
return settings[CONTACT_ALLOW_COMPANY_REGISTRATION_SETTING] !== false;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
private async cleanupReplacedAvatar(
|
|
1105
|
+
locale: string,
|
|
1106
|
+
previousAvatarId?: number | null,
|
|
1107
|
+
nextAvatarId?: number | null,
|
|
1108
|
+
) {
|
|
1109
|
+
if (!previousAvatarId || previousAvatarId === nextAvatarId) {
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
try {
|
|
1114
|
+
await this.fileService.delete(locale, { ids: [previousAvatarId] });
|
|
1115
|
+
} catch {
|
|
1116
|
+
// Keep the person update successful even if avatar cleanup fails.
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
private normalizeRelationPersonSummary(person: any) {
|
|
1121
|
+
if (!person) return null;
|
|
1122
|
+
|
|
1123
|
+
const tradeName =
|
|
1124
|
+
person.person_company?.trade_name != null
|
|
1125
|
+
? String(person.person_company.trade_name)
|
|
1126
|
+
: null;
|
|
1127
|
+
|
|
1128
|
+
return {
|
|
1129
|
+
id: person.id,
|
|
1130
|
+
name: person.name,
|
|
1131
|
+
type: person.type,
|
|
1132
|
+
status: person.status,
|
|
1133
|
+
avatar_id: person.avatar_id ?? null,
|
|
1134
|
+
trade_name: tradeName,
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
239
1137
|
}
|