@hed-hog/contact 0.0.333 → 0.0.347

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.
@@ -4,40 +4,40 @@ import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
4
4
  import { Prisma, PrismaService } from '@hed-hog/api-prisma';
5
5
  import { FileService, IntegrationDeveloperApiService, SettingService } from '@hed-hog/core';
6
6
  import {
7
- BadRequestException,
8
- Inject,
9
- Injectable,
10
- Logger,
11
- NotFoundException,
12
- forwardRef,
7
+ BadRequestException,
8
+ Inject,
9
+ Injectable,
10
+ Logger,
11
+ NotFoundException,
12
+ forwardRef,
13
13
  } from '@nestjs/common';
14
14
  import {
15
- ACCOUNT_LIFECYCLE_STAGES,
16
- type AccountLifecycleStage,
17
- type CreateAccountDTO,
18
- type UpdateAccountDTO,
15
+ ACCOUNT_LIFECYCLE_STAGES,
16
+ type AccountLifecycleStage,
17
+ type CreateAccountDTO,
18
+ type UpdateAccountDTO,
19
19
  } from './dto/account.dto';
20
20
  import {
21
- type ActivityListQueryDTO,
22
- type CrmActivityPriority,
23
- type CrmActivitySourceKind,
24
- type CrmActivityStatus,
25
- type CrmActivityType,
21
+ type ActivityListQueryDTO,
22
+ type CrmActivityPriority,
23
+ type CrmActivitySourceKind,
24
+ type CrmActivityStatus,
25
+ type CrmActivityType,
26
26
  } from './dto/activity.dto';
27
27
  import { CreateFollowupDTO } from './dto/create-followup.dto';
28
28
  import {
29
- CreateInteractionDTO,
30
- PersonInteractionTypeDTO,
29
+ CreateInteractionDTO,
30
+ PersonInteractionTypeDTO,
31
31
  } from './dto/create-interaction.dto';
32
32
  import { CreateDTO } from './dto/create.dto';
33
33
  import {
34
- type CrmDashboardPeriod,
35
- type DashboardQueryDTO,
34
+ type CrmDashboardPeriod,
35
+ type DashboardQueryDTO,
36
36
  } from './dto/dashboard-query.dto';
37
37
  import { CheckPersonDuplicatesQueryDTO } from './dto/duplicates-query.dto';
38
38
  import {
39
- FollowupListQueryDTO,
40
- FollowupStatsQueryDTO,
39
+ FollowupListQueryDTO,
40
+ FollowupStatsQueryDTO,
41
41
  } from './dto/followup-query.dto';
42
42
  import { CRM_IMPORT_FIELDS } from './dto/import.dto';
43
43
  import { MergePersonDTO } from './dto/merge.dto';
@@ -52,6 +52,11 @@ const CONTACT_OWNER_ALLOWED_ROLE_SLUGS = [
52
52
  'admin-contact',
53
53
  CONTACT_OWNER_ROLE_SLUG,
54
54
  ];
55
+ const PERSON_USER_LINKER_ALLOWED_ROLE_SLUGS = [
56
+ 'admin',
57
+ 'admin-contact',
58
+ 'person-user-linker',
59
+ ];
55
60
  type PersonActivityAction = 'created' | 'updated' | 'interaction_created';
56
61
  const EMPLOYER_COMPANY_METADATA_KEY = 'employer_company_id';
57
62
  const NOTES_METADATA_KEY = 'notes';
@@ -850,6 +855,32 @@ export class PersonService {
850
855
  return Array.from(byId.values());
851
856
  }
852
857
 
858
+ private async canLinkPersonUser(userId: number): Promise<boolean> {
859
+ if (!userId || userId <= 0) {
860
+ return false;
861
+ }
862
+
863
+ const userWithRole = await this.prismaService.user.findUnique({
864
+ where: { id: userId },
865
+ select: {
866
+ role_user: {
867
+ select: {
868
+ role: {
869
+ select: { slug: true },
870
+ },
871
+ },
872
+ },
873
+ },
874
+ });
875
+
876
+ if (!userWithRole?.role_user) {
877
+ return false;
878
+ }
879
+
880
+ const userRoles = userWithRole.role_user.map((r) => r.role.slug);
881
+ return userRoles.some((role) => PERSON_USER_LINKER_ALLOWED_ROLE_SLUGS.includes(role));
882
+ }
883
+
853
884
  async getLinkedUserOptions(search?: string) {
854
885
  const where: Prisma.userWhereInput = search
855
886
  ? { name: { contains: search, mode: 'insensitive' } }
@@ -860,7 +891,6 @@ export class PersonService {
860
891
  select: {
861
892
  id: true,
862
893
  name: true,
863
- photo_id: true,
864
894
  user_identifier: {
865
895
  where: { type: 'email' },
866
896
  select: { value: true },
@@ -875,7 +905,6 @@ export class PersonService {
875
905
  id: user.id,
876
906
  name: user.name || `#${user.id}`,
877
907
  email: user.user_identifier[0]?.value || '',
878
- photo_id: user.photo_id ?? null,
879
908
  }));
880
909
  }
881
910
 
@@ -2171,7 +2200,7 @@ export class PersonService {
2171
2200
  };
2172
2201
  }
2173
2202
 
2174
- async create(data: CreateDTO, locale: string) {
2203
+ async create(data: CreateDTO, locale: string, user?: { id?: number; name?: string | null }) {
2175
2204
  const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
2176
2205
 
2177
2206
  await this.ensureCompanyRegistrationAllowed({
@@ -2179,6 +2208,20 @@ export class PersonService {
2179
2208
  locale,
2180
2209
  });
2181
2210
 
2211
+ // Validate user linking permission
2212
+ if (data.user_id !== undefined && data.user_id !== null) {
2213
+ const canLink = await this.canLinkPersonUser(data.user_id);
2214
+ if (!canLink) {
2215
+ throw new BadRequestException(
2216
+ getLocaleText(
2217
+ 'personUserLinkingNotAllowed',
2218
+ locale,
2219
+ 'You do not have permission to link a person to a user.',
2220
+ ),
2221
+ );
2222
+ }
2223
+ }
2224
+
2182
2225
  const result = await this.prismaService.$transaction(async (tx) => {
2183
2226
  const person = await tx.person.create({
2184
2227
  data: {
@@ -2226,7 +2269,16 @@ export class PersonService {
2226
2269
  user?: { id?: number; name?: string | null },
2227
2270
  ) {
2228
2271
  const allowCompanyRegistration = await this.isCompanyRegistrationAllowed();
2229
- const person = await this.prismaService.person.findUnique({ where: { id } });
2272
+ const person = await this.prismaService.person.findUnique({
2273
+ where: { id },
2274
+ include: {
2275
+ person_user: {
2276
+ select: {
2277
+ user_id: true,
2278
+ },
2279
+ },
2280
+ },
2281
+ });
2230
2282
  if (!person) {
2231
2283
  throw new BadRequestException(
2232
2284
  getLocaleText('personNotFound', locale, `Person with ID ${id} not found`),
@@ -2245,6 +2297,23 @@ export class PersonService {
2245
2297
  locale,
2246
2298
  });
2247
2299
 
2300
+ // Validate user linking permission
2301
+ if (data.user_id !== undefined && data.user_id !== person.person_user?.[0]?.user_id) {
2302
+ if (data.user_id !== null) {
2303
+ const canLink = await this.canLinkPersonUser(data.user_id);
2304
+ if (!canLink) {
2305
+ throw new BadRequestException(
2306
+ getLocaleText(
2307
+ 'personUserLinkingNotAllowed',
2308
+ locale,
2309
+ 'You do not have permission to link a person to a user.',
2310
+ ),
2311
+ );
2312
+ }
2313
+ }
2314
+ // Permitir remover vinculação (null) sem restrição
2315
+ }
2316
+
2248
2317
  const currentLifecycleStage = await this.getPersonLifecycleStage(id);
2249
2318
  const nextLifecycleStage = this.normalizeTextOrNull(data.lifecycle_stage);
2250
2319
  const stageTransitionParams =
@@ -3047,6 +3116,39 @@ export class PersonService {
3047
3116
  ]),
3048
3117
  );
3049
3118
 
3119
+ // Sync: if person has no avatar but has a linked user with a photo, copy it
3120
+ const personsWithoutAvatar = people.filter((p) => !p.avatar_id);
3121
+ const syncedAvatarById = new Map<number, number>();
3122
+ if (personsWithoutAvatar.length > 0) {
3123
+ const idsWithoutAvatar = personsWithoutAvatar.map((p) => p.id);
3124
+ const userLinks = await this.prismaService.person_user.findMany({
3125
+ where: {
3126
+ person_id: { in: idsWithoutAvatar },
3127
+ user: { photo_id: { not: null } },
3128
+ },
3129
+ select: {
3130
+ person_id: true,
3131
+ user: { select: { photo_id: true } },
3132
+ },
3133
+ orderBy: { created_at: 'desc' },
3134
+ });
3135
+ for (const link of userLinks) {
3136
+ if (!syncedAvatarById.has(link.person_id) && link.user?.photo_id) {
3137
+ syncedAvatarById.set(link.person_id, link.user.photo_id);
3138
+ }
3139
+ }
3140
+ if (syncedAvatarById.size > 0) {
3141
+ await Promise.all(
3142
+ Array.from(syncedAvatarById.entries()).map(([personId, photoId]) =>
3143
+ this.prismaService.person.update({
3144
+ where: { id: personId },
3145
+ data: { avatar_id: photoId },
3146
+ }),
3147
+ ),
3148
+ );
3149
+ }
3150
+ }
3151
+
3050
3152
  return people.map((person) => {
3051
3153
  const metadata = this.metadataArrayToMap(person.person_metadata);
3052
3154
  const companyData = companyById.get(person.id);
@@ -3067,7 +3169,7 @@ export class PersonService {
3067
3169
  name: person.name,
3068
3170
  type: person.type,
3069
3171
  status: person.status,
3070
- avatar_id: person.avatar_id ?? null,
3172
+ avatar_id: syncedAvatarById.get(person.id) ?? person.avatar_id ?? null,
3071
3173
  created_at: person.created_at,
3072
3174
  updated_at: person.updated_at,
3073
3175
  birth_date: individualData?.birth_date ?? null,